65.9K
CodeProject 正在变化。 阅读更多。
Home

编写命名空间扩展的技巧(II)- 实现创建和删除对象操作

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (13投票s)

2005年10月19日

10分钟阅读

viewsIcon

177948

downloadIcon

2158

本文介绍如何实现 IContextMenu 接口,以允许用户在命名空间扩展中创建或删除对象。

目录

引言

命名空间扩展(简称 NSE)实际上是一个实现了某些必需 Shell 接口的 COM 服务器。资源管理器使用这些 COM 接口来让用户访问和操作内部命名空间数据,并以图形形式显示。在阅读本文之前,强烈建议先阅读以下文章:

在开发自己的 NSE 时,为了让你的 NSE 文件夹对象表现得像一个“真实”的文件夹,仅仅提供让用户在 NSE 文件夹对象之间导航的功能(该主题已在文章二中讨论)是不够的。我们还应该提供创建或删除对象的其他基本功能,尤其是在我们自己的 NSE 中创建或删除文件夹对象,就像我们在系统命名空间中所做的那样。

解决这个问题涉及两个方面:

  • 首先,你应该实现与 UI 相关的 Shell 接口,例如 IContextMenu,它使用户能够发送命令。
  • 其次,我们应该处理所有命令,并通知所有的 Shell 视图和树视图以反映变化。

本文假设您了解 C++、ATL、COM,并熟悉 NSE 的基本知识。所提供示例项目中的 NSE 模拟了用户在系统命名空间中创建或删除文件夹的操作,即用户可以通过选择我们实现的上下文菜单中提供的相应菜单项来在我们的 NSE 中创建或删除对象。该示例项目是使用 ATL COM AppWizard 创建的。

在树视图和 Shell 视图中提供上下文菜单

我们的 NSE 在树视图和 Shell 视图中都提供了上下文菜单,以使用户能够删除或创建对象。以下图片展示了四种不同情况下的上下文菜单:

在树视图中右键单击所选文件夹。

在 Shell 视图中右键单击所选文件夹。

在 Shell 视图中未选择任何对象时右键单击。

右键单击 NSE 的根文件夹。

要在树视图中提供上下文菜单,您必须创建一个支持 IContextMenu 接口的 COM 对象。当资源管理器需要为您的某个子文件夹显示上下文菜单时,它会调用您的 IShellFolder::GetUIObjectOf,请求一个 IID_IContextMenu 接口。在我们实现中创建的菜单项将被合并到由资源管理器创建的文件夹对象的快捷菜单中。

当用户在我们的 NSE 的 Shell 视图中右键单击鼠标时,会向 Shell 视图发送一个 WM_CONTEXTMENU 消息。要处理此消息,您可以使用标准的 Win32 菜单命令来实现上下文菜单,或者您可以使用已实现的 IContextMenu COM 对象。在我们的示例项目中,我们选择了第二种方式。

注意:当您右键单击 NSE 的根文件夹时,显示的上下文菜单不是由我们的 NSE 创建的,而是由资源管理器创建的。但是,我们可以通过在注册 NSE 时为 NSE 的根文件夹分配一些属性(请参考 IShellFolder::GetAttributesOf)来控制这个上下文菜单的一部分(请参考文章一中的“注册扩展”部分)。例如,如果您想显示我们 NSE 根文件夹的属性,您应该在“ShellFolder”键的“Attributes”值中添加 SFGAO_HASPROPSHEET 属性。要使其真正起作用,您应该实现一个 PropertySheetHandler 并在“{NSE's CLSID}”键下的“shellex\PropertySheetHandlers”子键中注册它。在我们的示例项目中,我们为 NSE 根分配了 SFGAO_HASPROPSHEET,但没有实现 PropertySheetHandler。因此,您可以在根文件夹的上下文菜单中看到“属性”项,但当您选择它时,会显示一条消息:“此项目的属性不可用。”

在树视图中提供上下文菜单

在我们的示例项目中,我们向 IShellFolder::GetUIObjectOf 实现中添加了代码以支持 IID_IContextMenu。当传入 IID_IContextMenu 时,它会创建并实例化 CContextMenu 对象,并将指向 IContextMenu 接口的指针返回给调用者。

STDMETHODIMP CMyVirtualFolder::GetUIObjectOf(HWND hWnd, 
                                    UINT nCount, 
                                    LPCITEMIDLIST* pidls, 
                                    REFIID riid, LPUINT, 
                                    LPVOID* ppRetVal)
{
    HRESULT Hr;
    ...... // handle other interface requests

    if( riid == IID_IContextMenu ) 
    {
        if( nCount!=1 ) 
            return E_FAIL;

        CComObject<CContextMenu>* pContextMenu;
        HR( CComObject<CContextMenu>::CreateInstance(&pContextMenu));

        pContextMenu->AddRef();

        HR( pContextMenu->_Init(this, hWnd, *pidls) );
        Hr=pContextMenu->QueryInterface(IID_IContextMenu, ppRetVal);

        pContextMenu->Release();

        return Hr;
    }    
    ...... //handle other interface requests

    return E_NOINTERFACE;
}

在 Shell 视图中提供上下文菜单

在处理 WM_CONTEXTMENU 消息时,如果使用常规方法——调用 Win32 菜单函数来创建上下文菜单——我们将不得不维护一组额外的命令处理函数来处理菜单命令。然而,这已经在 CContextMenuInvokeCommand 函数中完成了;我们可以利用这一点,通过使用 CContextMenu 对象在 Shell 视图中创建上下文菜单。这样做的好处是它将使我们源代码的可维护性更好。

BEGIN_MSG_MAP(CNSFShellView)
    ...... //declare other msg handler functions
    MESSAGE_HANDLER(WM_CONTEXTMENU, OnContextMenu)
    ...... //declare other msg handler functions  
END_MSG_MAP()
LRESULT CNSFShellView::OnContextMenu(UINT uMsg, 
         WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    LPITEMIDLIST pidlSelected=NULL;
    LPITEMIDLIST *apidls;
    
    apidls = 
        (LPITEMIDLIST*)_Module.m_Allocator.Alloc(
                                1 * sizeof(LPITEMIDLIST));  
    if(apidls == NULL)
        return E_OUTOFMEMORY;

    ::ZeroMemory(apidls,sizeof(LPITEMIDLIST));

    ATLASSERT( NULL != apidls);

    UINT nCount = ListView_GetSelectedCount(m_hwndList);
    if( 0 == nCount) 
    {
        // indicate that no item in list view is selected, 
        // created context menu is bind to current folder
        pidlSelected = NULL;
    }
    else
    {
        LV_ITEM        lvItem;
        int nSelItem = ListView_GetNextItem( m_hwndList, 
                                        -1, LVIS_SELECTED );
        
        ZeroMemory(&lvItem, sizeof(lvItem));
        lvItem.mask = LVIF_PARAM;
        lvItem.iItem = nSelItem;

        if(ListView_GetItem(m_hwndList, &lvItem))
            pidlSelected=(LPITEMIDLIST)(lvItem.lParam);
    }

    apidls[0]=pidlSelected;

    LPCONTEXTMENU  pContextMenu = NULL; 
    m_pFolder->GetUIObjectOf(m_hWnd, 1, 
                            (LPCITEMIDLIST*)apidls, 
                            IID_IContextMenu,NULL,
                            (LPVOID*)&pContextMenu);  

    if(pContextMenu)
    {
        HMENU hMenu = ::CreatePopupMenu();
        if( hMenu && SUCCEEDED(pContextMenu->QueryContextMenu
                  (hMenu,0,MENU_OFFSET, MENU_MAX, CMF_NORMAL)))
        {
            UINT iSelCmdItem=0;

            POINT pt = { LOWORD(lParam), HIWORD(lParam) };
            iSelCmdItem = ::TrackPopupMenu(hMenu,
                      TPM_LEFTALIGN|TPM_LEFTBUTTON|TPM_RETURNCMD,
                      pt.x, pt.y,0,m_hWnd,NULL);
            if( iSelCmdItem > 0)
            {
                CMINVOKECOMMANDINFO  cmi;
                ZeroMemory(&cmi, sizeof(cmi));     
                cmi.cbSize = sizeof(cmi);        
                cmi.hwnd = m_hWnd;           
                cmi.lpVerb = 
                  (LPCSTR)MAKEINTRESOURCE(iSelCmdItem - MENU_OFFSET);       
                pContextMenu->InvokeCommand(&cmi);  
            }
        }
        ::DestroyMenu(hMenu);
        pContextMenu->Release();
    }
    _Module.m_Allocator.Free(apidls);

    return 0;
}

实现 IContextMenu

现在,我将描述如何实现我们的 ContextMenu 对象。

为了让用户能够在我们的 NSE 中删除或创建文件夹,我们在上下文菜单中创建的菜单项包括:属性、新建 NSEFolder、删除。我们上下文菜单的行为模拟了在系统命名空间中发生的情况:

  • 情况 1:当右键单击焦点在树视图中的文件夹对象(根文件夹除外)时,显示“删除”和“属性”,并且资源管理器会已经插入一个“展开”项。
  • 情况 2:当右键单击焦点在 Shell 视图中的对象(文件夹或文件)时,显示“删除”和“属性”。
  • 情况 3:当在 Shell 视图中右键单击且未选择任何对象时,显示“新建 NSEFolder”和“属性”。

注意:为简单起见,“属性”项仅用于演示,并未实现。

自定义数据成员和成员函数

    typedef enum 
    {
        IDM_PROPERTIES =0, //menu identifier offset of Properties
        IDM_CREATE_FOLDER, //menu identifier offset of New folder
        IDM_DELETE,        //menu identifier offset of Delete
        IDM_LAST,
    } MENUITEMS;
    
    CMyVirtualFolder *m_pFolder; //refer to the folder object which 
                                 // provided this ContextMenu object
    HWND m_hWnd;                 //handler of the window in which 
                                 // user right click the mouse
    LPITEMIDLIST m_pidl;         //simple PIDL of the object which 
                                 // you selected
    CNWSPidlMgr  m_PidlMgr;
    //function use to initialize data members
    HRESULT _Init(CMyVirtualFolder *pFolder, 
                    HWND hWnd, LPCITEMIDLIST pidl);

初始化上下文菜单对象

    HRESULT _Init(CMyVirtualFolder *pFolder, 
                      HWND hWnd, LPCITEMIDLIST pidl)
    {
        if(pFolder==NULL)
        {
            MessageBox(NULL,
              _T("CContextMenu()::_Init(pFolder==NULL)"),
              _T("NSExtAddDelFld"),MB_OK);
            return E_FAIL;
        }
        m_pFolder = pFolder;
        m_pFolder->AddRef();
        m_pidl = m_PidlMgr.Copy(pidl);
        m_hWnd = hWnd;

        return S_OK;
    }

向上下文菜单添加命令

这个标准成员函数用于向上下文菜单添加命令。

    STDMETHOD(QueryContextMenu)(HMENU hMenu,
                                UINT    iIndexMenu,
                                UINT    idCmdFirst,
                                UINT    idCmdLast,
                                UINT    uFlags)
    {       
        // add Menu item according to Explore's behavior
        if( NULL == m_pidl ) 
        {
            //no object is selected, context menu is created 
            //in Shell view and is belongs to current folder
            //enable to create new subfolder in current folder
            ::InsertMenu(hMenu, iIndexMenu++, 
                      MF_STRING | MF_BYPOSITION, 
                      idCmdFirst + IDM_CREATE_FOLDER, 
                      _TEXT("New NSE&Folder"));
            ::InsertMenu(hMenu, iIndexMenu++, 
                     MF_SEPARATOR | MF_STRING | MF_BYPOSITION, 
                     0, _T(""));
            ::InsertMenu(hMenu, iIndexMenu++, 
                     MF_STRING | MF_BYPOSITION, 
                     idCmdFirst + IDM_PROPERTIES, 
                     _TEXT("&Properties"));
        }
        else
        {
            //one object is selected in Shell view or Tree view
            //add delete and properties two commands
            ::InsertMenu(hMenu, iIndexMenu++, 
                         MF_STRING | MF_BYPOSITION, 
                         idCmdFirst + IDM_DELETE, 
                         _TEXT("&Delete"));
            ::InsertMenu(hMenu, iIndexMenu++, 
                MF_SEPARATOR | MF_STRING | MF_BYPOSITION, 
                0, _T(""));
            ::InsertMenu(hMenu, iIndexMenu++, 
                       MF_STRING | MF_BYPOSITION, 
                       idCmdFirst + IDM_PROPERTIES, 
                       _TEXT("&Properties"));
        }
        
        ::SetMenuDefaultItem(hMenu, 0, TRUE);

        return MAKE_HRESULT(SEVERITY_SUCCESS, 0, IDM_LAST);
    }

调用删除和创建命令

当调用创建文件夹命令时,由于创建文件夹的过程涉及 ListView 中的操作(用户需要指定所创建文件夹的名称),我们应该通过向当前 Shell 视图发送一个 ID_NEWITEM_FOLDER 命令,将创建操作传递给 Shell 视图对象。

如果调用删除命令,我们调用 IShellFolder::_DoDelete 来完成任务。

    STDMETHOD(InvokeCommand)(LPCMINVOKECOMMANDINFO pcmi)
    {        
        USES_CONVERSION;
        
        // The command is sent via a verb
        if( HIWORD(pcmi->lpVerb) ) 
        {
            LPCMINVOKECOMMANDINFOEX pcmix = NULL;
            if(pcmi->cbSize>=
                   sizeof(CMINVOKECOMMANDINFOEX)-sizeof(POINT)) 
                pcmix = (LPCMINVOKECOMMANDINFOEX)pcmi;
            
            LPCSTR pstr = pcmi->lpVerb;
            
            // If it's an UNICODE string, convert it to ANSI now
            // NOTE: No C++ block here because of W2CA stack-scope.
            if( (pcmix!=NULL) && 
                ((pcmix->fMask & CMIC_MASK_UNICODE)!=0) && 
                (pcmix->lpVerbW!=NULL) )    
                pstr = W2CA(pcmix->lpVerbW);
            
            if( strcmp(pstr, "Delete")==0 ) 
                pcmi->lpVerb = (LPCSTR)IDM_DELETE;
            else if( strcmp(pstr, "New NSEFolder")==0 ) 
                pcmi->lpVerb = (LPCSTR)IDM_CREATE_FOLDER;
            else 
                return E_INVALIDARG;
        }

        // Check that it's a valid command
        if( LOWORD(pcmi->lpVerb)>IDM_LAST ) 
            return E_INVALIDARG;
        
        // Process our command
        switch( LOWORD(pcmi->lpVerb) ) 
        {
            case IDM_DELETE:
            {
                //let IShellFolder accomplish this task
                m_pFolder->_DoDelete(m_pidl);          
            }
            break;
            case IDM_CREATE_FOLDER:
            {
                //this command is only support in Shell view
                //so send ID_NEWITEM_FOLDER to Shell view object 
                if(m_pFolder->_IsViewWindow(m_hWnd))
                    ::SendMessage(m_hWnd,WM_COMMAND,
                                         ID_NEWITEM_FOLDER,0);
                else
                    MessageBox(NULL,
                     _T("Don't support create new folder " + 
                                  "except in Shell View window"),
                     _T("Error"),MB_OK);
            }
            break;            
            default:
                return E_INVALIDARG;
        }
        return S_OK;
    }

刷新树视图和 Shell 视图

当用户在我们的 NSE 中删除或创建新文件夹时,树视图和 Shell 视图应该正确响应这些变化。

通知树视图

Shell 函数 SHChangeNotify 使我们能够通知 Shell 在我们的 NSE 中发生的变化。特别是,当我们创建或删除文件夹时,树视图必须正确反映这些变化。

为了使 SHChangeNotify 真正起作用,我们必须拥有受变化影响的每个项的完整 PIDL。到目前为止提到的所有 PIDL 都是相对 PIDL,但幸运的是,它们使用了我们 PIDL 管理类的 Concatenate 方法,所以我们可以从保存我们 NSE 根文件夹完整 PIDL 的 _Module.m_pidlNSFRoot 和该对象的复杂 PIDL 来计算我们 NSE 中每个对象的完整 PIDL。

请参考文章二中的“文件夹初始化”和“连接 PIDL”部分,以了解更多关于 _Module.m_pidlNSFRootConcatenate 方法的信息。

刷新 Shell 视图

众所周知,IShellView 接口提供了一个 Refresh 方法,用于刷新 Shell 视图。但这个方法只能从 IShellView 内部调用。

我们可以利用这个方法。在实现 IShellView 时,我们可以定义一个用户命令 ID_VIEW_REFRESH 并为其注册一个命令处理程序,在命令处理程序中我们可以简单地调用 IShellView::Refresh。因此,一旦我们有了 Shell 视图窗口的句柄,我们就可以通过向它发送 ID_VIEW_REFRESH 命令来刷新这个窗口。

当有多个资源管理器窗口共存时,在删除或创建文件夹时,我们必须刷新其中所有的 Shell 视图。当我们找到所有 Shell 视图窗口的句柄并向每个窗口发送 ID_VIEW_REFRESH 命令后,问题就解决了。

有时,我们需要刷新除当前视图之外的所有其他 Shell 视图。

BOOL CALLBACK RefreshShellView( HWND hWnd, LPARAM lParam )
{
    if( hWnd ) 
    {
        TCHAR szClassName[MAX_PATH]=_T("");
        DWORD dwLen=MAX_PATH;
        GetClassName(hWnd,szClassName,dwLen);
        if( (_tcscmp(szClassName,_T("ExploreWClass"))==0) ||
            (_tcscmp(szClassName,_T("CabinetWClass"))==0) )
        {
            HWND hwndShellView = 
                FindWindowEx(hWnd,NULL,_T("NSFViewClass"),NULL);
            if(hwndShellView !=NULL)
            {           
                HWND hwndExcept =(HWND)lParam;
                if((hwndExcept!=NULL && hwndExcept!=hwndShellView) ||
                    (hwndExcept==NULL))
                    ::SendMessage(hwndShellView,WM_COMMAND,
                                               ID_VIEW_REFRESH,0);
            }
        }
    }
    return( TRUE );
}

void RefreshShellViewWndsExcept(HWND hwndExcept)
{
    // continue looping until done
    for(; !EnumWindows((WNDENUMPROC) RefreshShellView,
                              (LPARAM) hwndExcept ); ); 
}

删除对象

在 NSE 中删除一个对象的实现包括三个部分:

  • 从 Shell 视图中删除相应的项——为实现这一点,我们首先更新我们的配置数据,然后根据更新后的配置数据刷新 Shell 视图。
  • 通知 Shell 有一个项被删除了——如果删除的是文件夹,则使用 SHCNE_RMDIR 调用 SHChangeNotify;如果删除的是文件,则使用 SHCNE_DELETE,以通知 Shell 并让资源管理器窗口中的所有树视图都反映出删除操作。
  • 管理你的 NSE 数据——即从配置文件中删除相应的项。

实际上,_DoDelete 可以是我们 NSE 中任何已实现的 COM 对象的成员函数,只要该对象可以访问当前文件夹对象的 IShellFolder 接口即可,例如 CContextMenu 对象。我通常选择 IShellFolder

_DoDelete

HRESULT CMyVirtualFolder::_DoDelete(LPITEMIDLIST pidl)
{
    HRESULT Hr;
    int ret;

    ret=MessageBox(NULL,
        _TEXT("This Delete operation will direct " + 
                  "to the deleted item can't be recover," + 
              "\nAre you sure to delete the selected item?"), 
              _TEXT("Notice"),MB_OKCANCEL|MB_ICONWARNING);
    if(ret==IDOK)
    {
        //1.Delete corresponding item from configuration file
        HR(DeleteItemInCfgFile(m_pidlPath,pidl));
        
        //2. Refresh the Tree view
        LPITEMIDLIST tmpPidl1,tmpPidl2;
        tmpPidl1=m_PidlMgr.Concatenate(
                    _Module.m_pidlNSFROOT,m_pidlPath);
        tmpPidl2=m_PidlMgr.Concatenate(tmpPidl1,pidl);

        if( NWS_FOLDER == (m_PidlMgr.GetItemType(pidl)) )
            ::SHChangeNotify( SHCNE_RMDIR, 
                SHCNF_IDLIST | SHCNF_FLUSH, tmpPidl2, NULL);
        else
            ::SHChangeNotify( SHCNE_DELETE, 
                SHCNF_IDLIST | SHCNF_FLUSH, tmpPidl2, NULL);

        // Ask Shell to refresh current directory
        ::SHChangeNotify(SHCNE_UPDATEDIR, 
                SHCNF_IDLIST | SHCNF_FLUSH, tmpPidl1, NULL);

        m_PidlMgr.Delete(tmpPidl1);
        m_PidlMgr.Delete(tmpPidl2);
        
        //3. Refresh Shell view Exist in All Explorer Windows.
        RefreshShellViewWndsExcept(NULL);
    }
    return S_OK;
}

创建新文件夹对象

IShellView

要使您的列表视图能够添加新项,必须为列表视图窗口分配 LVS_EDITLABELS 样式。

在我们的 NSE 中创建文件夹的过程可以分为以下几个步骤:

  1. 首先,在配置文件中添加一个名为“新建文件夹”的临时名称的文件夹项。
  2. 刷新 Shell 视图,以显示带有临时名称的已创建文件夹。
  3. 刷新树视图,以显示带有临时名称的已创建文件夹。
  4. 将 ListView 中新创建的项设置为编辑模式,以便用户可以为其指定名称。
  5. 编辑完成后,调用 IShellFolder::SetNameOf 来更新配置文件中相应项的名称,并用新名称创建一个新的 PIDL,然后调用 SHChangeNotify 通知 Shell,让所有树视图反映重命名操作。
  6. 在 ListView 中定位新创建的项,用新创建的 PIDL 更新该项的 PIDL(该 PIDL 绑定到项的 lParam 成员),并删除旧的 PIDL。
  7. 刷新除当前视图外的所有其他 Shell 视图。

以上所有步骤都可以借助三个 IShellView 的处理函数(OnNewFolderOnLabelEditBeginOnLabelEditEnd)和两个 IShellFolder 的标准方法(GetAttributesOfSetNameOf)来实现。

相关的消息和命令处理程序

BEGIN_MSG_MAP(CNSFShellView)
    ......
    COMMAND_ID_HANDLER(ID_NEWITEM_FOLDER, OnNewFolder)
    NOTIFY_CODE_HANDLER(LVN_BEGINLABELEDIT, OnLabelEditBegin)
    NOTIFY_CODE_HANDLER(LVN_ENDLABELEDIT, OnLabelEditEnd) 
    ......
END_MSG_MAP()

ID_NEWITEM_FOLDER 命令处理程序

此命令处理程序开始创建操作。它负责上述的前四个步骤。

LRESULT CNSFShellView::OnNewFolder(WORD /*wNotifyCode*/, 
          WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
    TCHAR szTempNewName[MAX_PATH]=_T("");
    
    //1.Add dir item to current folder's 
    //section in configuration file 
    CreateFolderInCfgFile(m_pFolder->m_pidlPath,szTempNewName);

    //2.Refresh current Shell view to show the new created folder
    Refresh();

    //3.Refresh Other Shell view to show new folder 
    RefreshShellViewWndsExcept(m_hWnd);

    //4.Refresh Tree view
    //4.1 Get new folder's PIDL structure
    LVFINDINFO fi = { 0 };
    fi.flags = LVFI_STRING;
    fi.psz = szTempNewName;
    int iItem = ListView_FindItem(m_hwndList, -1, &fi);
  
    LVITEM lvItem = { 0 };
    lvItem.mask = LVIF_PARAM;
    lvItem.iItem = iItem;
    ListView_GetItem(m_hwndList, &lvItem);
 
    //4.2 Send Notify To Shell
    LPITEMIDLIST tmpPidl1,tmpPidl2;
    tmpPidl1=m_PidlMgr.Concatenate(_Module.m_pidlNSFROOT,
                                    m_pFolder->m_pidlPath);
    tmpPidl2=m_PidlMgr.Concatenate(tmpPidl1,
                             (LPCITEMIDLIST)lvItem.lParam);
    ::SHChangeNotify(SHCNE_MKDIR, 
               SHCNF_IDLIST | SHCNF_FLUSH, tmpPidl2, NULL);
    m_PidlMgr.Delete(tmpPidl1);
    m_PidlMgr.Delete(tmpPidl2);
 
    //5.Put new created item in edit mode...
    ::SetFocus(m_hwndList);
    ListView_EditLabel(m_hwndList, iItem);

    return 0;
}

LVN_BEGINLABELEDIT 通知处理程序

此事件处理程序主要负责调用 IShellFolder::GetAttributesOf 来验证当前标签是否可编辑。

LRESULT CNSFShellView::OnLabelEditBegin(UINT /*CtlID*/, 
                          LPNMHDR lpnmh, BOOL& /*bHandled*/)
{
    NMLVDISPINFO* lpdi = (NMLVDISPINFO*) lpnmh;
    DWORD dwAttr = SFGAO_CANRENAME;
    m_pFolder->GetAttributesOf(1, 
             (LPCITEMIDLIST*)&lpdi->item.lParam, &dwAttr);
    if( (dwAttr & SFGAO_CANRENAME)==0 ) 
        return TRUE;
    return FALSE; //Return FALSE means allow to edit
}

LVN_ENDLABELEDIT 通知处理程序

当用户完成编辑时发生。用户指定的新名称将被传入,在检查其有效性后,我们继续执行创建新文件夹的剩余三个步骤。

LRESULT CNSFShellView::OnLabelEditEnd(UINT /*CtlID*/, 
                         LPNMHDR lpnmh, BOOL& /*bHandled*/)
{
    NMLVDISPINFO* lpdi = (NMLVDISPINFO*) lpnmh;
    
    //1.If user cancelled editing
    if((lpdi->item.pszText==NULL) || 
          (_tcscmp(lpdi->item.pszText,_T("New Folder")) == 0))
         return FALSE; 

    //2.validate the new name
    //2.1 Not include illegal character
    if( _tcspbrk(lpdi->item.pszText, _T("<>/?:\"\\")) != NULL )        
    {
         MessageBox(NULL,_T("File name can't include " + 
                        "following characters: \\/:*?\"<>|"), 
                    _T("Error"),MB_OK);
         ::SetFocus(m_hwndList);
         ListView_EditLabel(m_hwndList,lpdi->item.iItem);
         return FALSE;
    }
    
    //2.2 uniquness
    TCHAR szCfgFile[MAX_PATH]=_TEXT("");
    TCHAR szSection[MAX_PATH]=_TEXT("");
    OpenDirInCfgFile(m_pFolder->m_pidlPath,szSection,szCfgFile);

    DWORD dwLen=MAX_PATH;
    TCHAR szDirKeyValue[MAX_PATH]=_T("");
    GetPrivateProfileString(szSection,_T("dir"),_T(""),
                               szDirKeyValue, dwLen, szCfgFile);

    if(NotUniqueName(lpdi->item.pszText,szDirKeyValue)==TRUE)
    {
         MessageBox(NULL,_T("Rename error: Specified filename is " + 
            "equal to a file existed! Please specify another name."), 
            _T("Error"),MB_OK);
         ::SetFocus(m_hwndList);
         ListView_EditLabel(m_hwndList,lpdi->item.iItem);
         return FALSE;
    }

    //3.Get Old PIDL
    LVITEM lvItem = { 0 };
    lvItem.mask = LVIF_PARAM;
    lvItem.iItem = lpdi->item.iItem;
    ListView_GetItem(m_hwndList, &lvItem);
    LPITEMIDLIST pidlOldItem = (LPITEMIDLIST)lvItem.lParam;

    //4. Call IShellFolder::SetNameOf Create New PIDL for new Name...
    USES_CONVERSION;
    LPCWSTR pwstr = T2CW(lpdi->item.pszText);
    LPITEMIDLIST pidlNewItem = NULL;
    HRESULT Hr = m_pFolder->SetNameOf(NULL, pidlOldItem, 
                                     pwstr, 0, &pidlNewItem);
    if( FAILED(Hr) || (pidlNewItem==NULL) ) 
    {
        ::MessageBox(NULL,
          _T("IShellFolder::SetNameOf failed."),_T("Error"),MB_OK);
        return FALSE;
    }

    //5.Set the new PIDL to item
    lvItem.mask = LVIF_PARAM;
    lvItem.lParam = (LPARAM)pidlNewItem;
    ListView_SetItem(m_hwndList, &lvItem);

    m_PidlMgr.Delete(pidlOldItem);
    
    //6.Refresh Other Shell View to show new name
    RefreshShellViewWndsExcept(m_hWnd);

    return TRUE; // Accept rename
}

IShellFolder 相关函数

此文件夹是否可以重命名?

因为重命名操作涉及创建文件夹的过程,我们应该为所有文件夹对象分配 SFGAO_CANRENAME 属性来实现 IShellFolder::GetAttributesOf,像这样:

STDMETHODIMP CMyVirtualFolder::GetAttributesOf(UINT uCount, 
                                      LPCITEMIDLIST pidls[], 
                                      LPDWORD pdwAttribs)
{
    ......
    case NWS_FOLDER:
    {
        ......
        //enable rename to folder objects
        dwAttr |=SFGAO_CANRENAME; 
        ......
    }
    ......
}

设置对象的显示名称并更改其 PIDL

此函数的实现负责:

  • 用新名称创建一个 PIDL。
  • 用用户指定的名称更新配置文件中新创建的项。
  • 通知所有树视图以反映重命名。
STDMETHODIMP CMyVirtualFolder::SetNameOf(HWND, 
                                LPCITEMIDLIST pidlOld, 
                                LPCOLESTR pstrName, 
                                DWORD, 
                                LPITEMIDLIST* ppidlOut)
{
    USES_CONVERSION;
    if( ppidlOut!=NULL ) 
        *ppidlOut=NULL;
 
    DWORD dwAttr = SFGAO_CANRENAME;
    GetAttributesOf(1, &pidlOld, &dwAttr);
    if( (dwAttr & SFGAO_CANRENAME)==0 ) 
        return E_FAIL;
 
    TCHAR szNewName[MAX_PATH+1];
    if( wcslen(pstrName)>MAX_PATH ) 
        return E_FAIL;
    _tcscpy( szNewName, OLE2CT(pstrName));

    //1.Create new PIDL for the new display name
    LPITEMIDLIST  pidlNew = NULL;
    ITEM_TYPE iItemType = NWS_FOLDER;
    pidlNew=m_PidlMgr.Create(iItemType,szNewName);
    if(!pidlNew)
        return E_FAIL;

    //2.Replace name to szNewName in Configuration File
    HRESULT Hr;
    HR(ReplaceNameInCfgFile(m_pidlPath,szNewName));

    //3.Notify Shell so it can rename the item in the Explorer tree
    LPITEMIDLIST pidlFullPath;
    pidlFullPath=m_PidlMgr.Concatenate(
                        _Module.m_pidlNSFROOT,m_pidlPath);

    LPITEMIDLIST pidlFullOld,pidlFullNew;
    pidlFullOld=m_PidlMgr.Concatenate(pidlFullPath,pidlOld);
    pidlFullNew=m_PidlMgr.Concatenate(pidlFullPath,pidlNew);

    ::SHChangeNotify(SHCNE_RENAMEFOLDER, 
        SHCNF_IDLIST |SHCNF_FLUSH, pidlFullOld, pidlFullNew);
    ::SHChangeNotify(SHCNE_UPDATEDIR, 
        SHCNF_IDLIST | SHCNF_FLUSH, pidlFullPath, NULL);
    
    m_PidlMgr.Delete(pidlFullPath);
    m_PidlMgr.Delete(pidlFullOld);
    m_PidlMgr.Delete(pidlFullNew);

    *ppidlOut = pidlNew;
    return S_OK;
}

结论

本文介绍了如何在您自己的 NSE 中实现删除或创建对象的功能。我计划在下一篇文章中讨论 NSE 中最复杂的主题——拖放操作。

© . All rights reserved.