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

编写命名空间扩展的技巧 (I) - 实现子文件夹

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (32投票s)

2005 年 10 月 8 日

13分钟阅读

viewsIcon

760463

downloadIcon

3554

本文介绍如何开发带子文件夹的命名空间扩展。

目录

引言

命名空间扩展(缩写为 **NSE**)实际上是一个 COM 服务器,它实现了若干必需的 Shell 接口。Explorer 使用这些 COM 接口来允许用户访问和操作内部命名空间数据,并以图形化的形式显示它们。如何开发 NSE 是一个大课题,在本文中,我将重点介绍如何在 NSE 中实现子文件夹。

在与 NSE 斗争了大约半年后,我终于找到了出路。可用的 NSE 文章非常糟糕,我不得不逐行阅读源代码来寻找解决方案。我通过阅读 MSDN 的示例项目 RegView(它完全用 C++ 编写)来自学 ATL。感谢 **Bjarke Viksoe**,他贡献了 AdfView 的源代码,它教授了许多处理 NSE 的重要技巧。

在这里,我想分享一些开发 NSE 的技巧。我将重点介绍如何解决您可能遇到的问题以及在编写自己的 NSE 时您会感兴趣的事情。由于要写一篇文章来涵盖所有这些方面几乎是不可能的(天知道这样的文章要写多久),我计划出一系列文章,涵盖子文件夹的实现、删除和创建文件夹、拖放等。

本文假定您了解 C++、ATL、COM,并熟悉 NSE 的基本知识。在阅读本文之前,请阅读 **Michael Dunn** 的《编写命名空间扩展完全白痴指南 - 第一部分》,因为它使用的术语与本文相同。

在本文中,重点是实现子文件夹。提供的示例项目中实现的 NSE 完成了一个简单的任务:从配置文件读取描述 NSE 根文件夹(带子文件夹)结构的数据,并使根文件夹在 Explorer 中表现得像一个拥有子文件夹的真实文件夹。当然,您知道这个文件夹不是文件系统的一部分,它是一个虚拟文件夹。

示例项目是使用 ATL COM AppWizard 创建的。

管理 NSE 的数据

配置文件

以下是我们示例 NSE 的配置文件内容,文件的格式参考初始化文件的格式。

[ROOT]
dir=src;doc
[src]
dir=VB;VC
file=note1.txt;note2.txt
[src\VB]
file=vb.vbp
[src\VC]
file=test.cpp

注释:

  • 文件中的节代表我们 NSE 中的文件夹,[ROOT] 节代表我们 NSE 的根文件夹。
  • 每个节中 "dir" 键的值代表包含在此文件夹中的子文件夹,用字符 ';' 分隔。
  • 每个节中 "file" 键的值代表包含在此文件夹中的文件,用字符 ';' 分隔。

在此文件中列出的所有文件夹和文件都将被创建到我们示例 NSE 中的相应对象中,如上图所示。

CComModule 派生类

如果您使用 ATL COM AppWizard 创建您的 NSE,该向导会自动生成 _Module,这是 CComModule 的一个全局实例。如果您有一些全局资源供整个模块共享,请将它们放在 CComModule 派生类的成员变量中。像这样:

管理全局资源的成员变量

CShellMalloc m_Allocator;      //class instance to manage 
                               //  Shell's IMalloc interface 
                               //  pointer
CShellImageLists m_ImageLists; //class instance to manage NSE 
                               //  used image/icon resources
LPITEMIDLIST m_pidlNSFROOT;    //the PIDL uniquely specifies our 
                               //  NSE's root folder all the 
                               //  across the entire namespace
TCHAR m_szInstallPath[MAX_PATH]; //the absolute path of NSE Dll, 
                                 //  use to locate the cfg file

如果您定义了自己的 CComModule 派生类,为了使其正常工作,您只需将 _Module 的声明类型从 CComModule 更改为您实现的 CComModule 派生类。

PIDL 管理类

我们 NSE 中的每个命名空间对象都由其 PIDL 唯一标识——指向其项 ID(实际上是 SHITEMID 结构)列表的指针。如果 PIDL 描述的对象相对于桌面文件夹(因此在整个 Shell 命名空间中唯一标识该对象),例如存储在 _Module.m_pidlNSFROOT 中的 PIDL,我们称之为完全限定 PIDL 或完整 PIDL。否则,它被称为相对 PIDL 或部分 PIDL。

本文讨论的 PIDL 大多是相对 PIDL,分为两种子类型:

  • 复杂 PIDL(多级 PIDL)- PIDL 可以包含多个 SHITEMID 结构,并标识父文件夹以下一个或多个级别的对象。本文提到的复杂 PIDL 都指相对于我们 NSE 根文件夹的 PIDL。
  • 简单 PIDL(单级 PIDL)- 相对于其父文件夹的 PIDL,仅包含一个 SHITEMID 结构(对象的项 ID)和一个终止的 NULL SHITEMID

我们的 PIDL 数据

将存储在 PIDL 中的数据完全由 NSE 的实现者决定。对于我们的 NSE 而言,仅仅创建一个包含文件和子文件夹的对象的文件夹,一个区分文件夹或文件的标志以及相对于其父文件夹的名称就足够了。下面定义的 PIDLDATA 结构用于保存项 ID 的值——这些值将存储在项 ID 的 SHITEMID.abID 字段中。

typedef    enum tagITEM_TYPE
{
   NWS_FOLDER = 0x00000001, 
   NWS_FILE   = 0x00000002,
}ITEM_TYPE; 

typedef struct tagPIDLDATA
{
   ITEM_TYPE type;      //used to mark folder or file
   TCHAR     szName[1]; //store the object's name 
                        // (relative to its parent folder)
}PIDLDATA, FAR *LPPIDLDATA;

由于实现我们的 NSE 涉及大量的 PIDL 操作,例如:创建、删除、连接、复制,并且还需要一些方法来从 PIDL 获取信息,因此需要一个 PIDL 管理类来完成所有这些工作。

class CNWSPidlMgr
{
public:
    LPITEMIDLIST Create(ITEM_TYPE iItemType,LPTSTR szName);
    LPITEMIDLIST GetNextItem(LPCITEMIDLIST pidl);
    LPITEMIDLIST Concatenate(LPCITEMIDLIST, LPCITEMIDLIST);

    //get current pidl item's name
    HRESULT GetName(LPCITEMIDLIST,LPTSTR);
    //concat the name of the pidl(from the 
    //pointer to the list end) together
    HRESULT GetFullName(LPCITEMIDLIST pidl,
                 LPTSTR szFullName,DWORD *pdwLen); 

    ITEM_TYPE GetItemType(LPCITEMIDLIST pidl);
    
    //Decide whether current folder has subfolder
    BOOL  HasSubFolder(LPTSTR pszPath); 

    .... //Other public methods
private:
    LPPIDLDATA GetDataPointer(LPCITEMIDLIST);
};

我将介绍一些支持子文件夹的重要方法,其他方法请参阅我的示例项目代码及其注释。

创建新的 PIDL

如前所述,命名空间中的每个项 ID 的值包含两部分:项类型和名称。这里我们创建了一个简单的 PIDL。

LPITEMIDLIST CNWSPidlMgr::Create(ITEM_TYPE iItemType,
                                        LPTSTR pszName)
{
    USHORT TotalSize = sizeof(ITEMIDLIST) + 
                         sizeof(ITEM_TYPE) + 
                         (_tcslen(pszName)+1)*sizeof(TCHAR);

    // Also allocate memory for the final null SHITEMID.
    LPITEMIDLIST pidlNew = 
        (LPITEMIDLIST) _Module.m_Allocator.Alloc(
                            TotalSize + sizeof(ITEMIDLIST));
    if (pidlNew)
    {
        ::ZeroMemory(pidlNew,TotalSize + sizeof(ITEMIDLIST));

        LPITEMIDLIST pidlTemp = pidlNew;
        
        // Prepares the PIDL to be filled with actual data
        pidlTemp->mkid.cb = (USHORT)TotalSize;

        LPPIDLDATA  pData;
        pData = GetDataPointer(pidlTemp); //viz. pidlTemp->mkid.abID
        
        // Fill the PIDL
        pData->type = iItemType;
        ::CopyMemory(pData->szName, pszName, 
                       (_tcslen(pszName)+1) * sizeof(TCHAR));

        // Set an empty PIDL at the end and 
        // set the NULL terminator to 0
        pidlTemp = GetNextItem(pidlTemp);
        pidlTemp->mkid.cb = 0;
        pidlTemp->mkid.abID[0] = 0;
    }
    return pidlNew;
}

获取 PIDL 的类型

此函数对我们的 NSE 实现子文件夹非常有用。在本文的另一部分 - 检索文件夹中对象的属性中,在返回给 Explorer 之前,调用此方法来确定 PIDL 代表的类型是文件夹还是文件。

ITEM_TYPE CNWSPidlMgr::GetItemType(LPCITEMIDLIST pidl)
{
    LPITEMIDLIST  pidlTemp = GetLastItem(pidl); //it is the last item 
                                                //determine the pidl 
                                                //represent what type
    LPPIDLDATA pData = GetDataPointer(pidlTemp);
    return pData->type;
}

有子文件夹吗?

如果当前文件夹有子文件夹,NSE 应告知 Explorer(IShellFolder::GetAttributesOf 返回 SFGAO_HASSUBFOLDER 给 Explorer),然后 Explorer 将在树视图的文件夹旁边显示一个 '+' 符号。要实现这一点,我们只需引用配置文件,以找出文件夹对应的节是否具有 "dir" 键即可?

BOOL CNWSPidlMgr::HasSubFolder(LPCITEMIDLIST pidl)
{
    TCHAR szPath[MAX_PATH]=_TEXT("");
    TCHAR tmpStr[MAX_PATH]=_TEXT("");
    TCHAR szCfgFile[MAX_PATH]=_TEXT("");
    DWORD dwLen=MAX_PATH;
    
    GetFullName(pidl,szPath,&dwLen);
    
    if( dwLen>0)
    {
        dwLen = MAX_PATH;
        _tcscpy(szCfgFile,g_szInstallPath);
        _tcscat(szCfgFile,_T("\\NSExtWithSubFld.cfg"));
        
        GetPrivateProfileString( szPath,
                    _T("dir"),_T("NotFound"),tmpStr,
                    dwLen, szCfgFile);
        
        if( (_tcscmp(tmpStr,_T("NotFound"))==0 ) || 
                               (_tcslen(tmpStr)==0 ) )
            return FALSE;       
        else 
            return TRUE;
    }
}

连接 PIDL

如果 NSE 具有多级文件夹,有时您需要将对象的简单 PIDL 与其父文件夹的复杂 PIDL 相结合,以获得相对于 NSE 根目录的合格 PIDL,或者将其父文件夹的完整 PIDL 相结合,以获得相对于桌面文件夹的完整 PIDL。

LPITEMIDLIST CNWSPidlMgr::Concatenate(
         LPCITEMIDLIST pidl1, LPCITEMIDLIST pidl2)
{
    LPITEMIDLIST   pidlNew;
    UINT           cb1 = 0, cb2 = 0;

    //are both of these NULL?
    if(!pidl1 && !pidl2)
       return NULL;

    //if pidl1 is NULL, just return a copy of pidl2
    if(!pidl1)
    {
       pidlNew = Copy(pidl2);
       return pidlNew;
    }

    //if pidl2 is NULL, just return a copy of pidl1
    if(!pidl2)
    {
       pidlNew = Copy(pidl1);
       return pidlNew;
    }

    cb1 = GetByteSize(pidl1) - sizeof(ITEMIDLIST);
    cb2 = GetByteSize(pidl2);

    //create the new PIDL
    pidlNew = 
      (LPITEMIDLIST)_Module.m_Allocator.Alloc(cb1 + cb2);
    if(pidlNew)
    {
        ::ZeroMemory(pidlNew,cb1+cb2);

        //copy the first PIDL
        ::CopyMemory(pidlNew, pidl1, cb1);
        //copy the second PIDL
        ::CopyMemory(((LPBYTE)pidlNew) + cb1, pidl2, cb2);
    }

    return pidlNew;
}

接口与 Explorer 的交互

IEnumIDList

要实现带子文件夹的 NSE,您必须提供一个子文件夹的枚举器,该枚举器负责枚举所有 NSE 文件夹的内容,从顶部(NSE 的根文件夹)到底部(NSE 最深的子文件夹)。当调用文件夹的 IShellFolder::EnumObjects 方法时,它会创建一个枚举对象,并将指向对象 IEnumIDList 接口的指针传回给调用者。调用者可以使用此接口枚举文件夹对象(子文件夹和文件)内的内容。请参阅 获取文件夹的枚举接口

在 NSE 中,IEnumIDList 的常见用途是:

  1. IShellView 使用,以枚举和显示 Shell 视图中文件夹的内容。
  2. 由 Explorer 使用,以显示文件夹的子文件夹在树视图中。

在我们的 NSE 中,要为文件夹创建一个枚举器来枚举其内容,我们引用配置文件并找到该文件夹的对应节,然后根据其 "dir" 和 "file" 键值,为每个子文件夹和文件对象创建 PIDL(这将是枚举列表的项),并将它们添加到 enum 列表中。我们还提供了一些标准和自定义方法来处理这个 PIDL enum 列表。

有时 IEnumIDList 用于仅列出文件夹对象。例如,当用户在树形视图窗格中单击您的 NSE 文件夹旁边的 '+' 号以展开该文件夹中的子文件夹时,Explorer 将调用 IShellFolder::EnumObjects 方法并将 grfFlags 参数设置为 SHCONTF_FOLDERS,以创建枚举器对象并获取 IEnumIDList 接口指针来仅列出子文件夹。因此,当创建我们的枚举器对象时,我们使用 grfFlags 的值作为初始化条件。

自定义结构和数据成员

   typedef struct tagENUMLIST
   {   struct tagENUMLIST   *pNext;
       LPITEMIDLIST         pidl;
   }ENUMLIST, FAR *LPENUMLIST;

   LPENUMLIST m_pFirst; //pointer to the head 
                        //  of the enum list
   LPENUMLIST m_pLast;  //pointer to the tail 
                        //  of the enum list
   LPENUMLIST m_pCurrent; //pointer to the current 
                          //  pidl in the enum list
   CNWSPidlMgr m_PidlMgr; //class instance use to 
                          //  handle pidl operations

成员函数

    //member function of IEnumIDList interface, 
    //standard method
    STDMETHOD (Next) (DWORD, LPITEMIDLIST*, LPDWORD);
    STDMETHOD (Skip) (DWORD);
    STDMETHOD (Reset) (void);
    STDMETHOD (Clone) (LPENUMIDLIST*); 
    
    //Self-defined functions
    BOOL DeleteList(void);   //called when the 
                             //  instance be deleted
    BOOL AddToEnumList(LPITEMIDLIST pidl); //add a pidl 
                                           //  to the enum list     
    //called when init the enumlist
    HRESULT _Init(LPCITEMIDLIST pidlRoot,DWORD dwFlags);  
    HRESULT _AddPidls(ITEM_TYPE iItemType, LPTSTR pszPath);

创建枚举列表

在初始化 enum 列表时,我们对 IEnumIDList 的实现必须同时支持 SHCONTF_FOLDERSSHCONTF_NONFOLDERS 标志。

HRESULT CPidlEnum::_Init(LPCITEMIDLIST pidlRoot,DWORD dwFlags)
{
    TCHAR  tmpPath[MAX_PATH]=_TEXT("");
    DWORD  dwLen=MAX_PATH;

    if((NULL==pidlRoot)||(0 == pidlRoot->mkid.cb))//Current folder 
                                                  //is NSE's root
        _tcscpy(tmpPath,_T("ROOT"));
    else //Current folder is NSE's subfolder
    {
        //Fetch pidlRoot correspond path name 
        //relative to NSE's root
        m_PidlMgr.GetFullName(pidlRoot,tmpPath,&dwLen);
    }
    
    if(dwFlags & SHCONTF_FOLDERS)//add subfolders objects
        _AddPidls(NWS_FOLDER,tmpPath);
        
    if(dwFlags & SHCONTF_NONFOLDERS)//add files objects
        _AddPidls(NWS_FILE,tmpPath); 
           
    Reset();
    return S_OK;
}
HRESULT  CPidlEnum::_AddPidls(ITEM_TYPE iItemType, 
                                         LPTSTR pszPath)
{
    TCHAR  tmpStr[MAX_PATH]=_TEXT("");
    TCHAR  tmpName[MAX_PATH]=_TEXT("");
    TCHAR  szCfgFile[MAX_PATH]=_TEXT("");
    DWORD  dwLen=MAX_PATH;
    LPITEMIDLIST   pidl=NULL;
    
    _tcscpy(szCfgFile,g_szInstallPath);
    _tcscat(szCfgFile,T("\\NSExtWithSubFld.cfg"));

    if( iItemType == NWS_FOLDER )
        GetPrivateProfileString( pszPath,_T("dir"),
                       _T(""),tmpStr,dwLen, szCfgFile);
    else if( iItemType == NWS_FILE )
        GetPrivateProfileString( pszPath,_T("file"),
                       _T(""),tmpStr,dwLen, szCfgFile);

    if( _tcslen(tmpStr)==0 )
        return S_OK;

    TCHAR *pChr,*pszHandle;
    pszHandle=tmpStr;
    pChr=pszHandle;

    while( ( pChr = _tcschr(pszHandle,_T(';') ) )!=NULL)
    {
        _tcsnset(tmpName,0,MAX_PATH);
        _tcsncpy(tmpName,pszHandle,pChr-pszHandle);

        //create relative pidl
        pidl=m_PidlMgr.Create(iItemType,tmpName);
        if(pidl)
        {
            if(!AddToEnumList(pidl))
                return E_FAIL;
        }
        else
            return E_FAIL;

        if(pszHandle[0] == _T('\0'))
            break;
        pszHandle = pChr+1;
    }

    _tcsnset(tmpName,0,MAX_PATH);
    _tcscpy(tmpName,pszHandle);

    if(_tcslen(tmpName)==0)
        return E_FAIL;

    //create pidl for the last item
    pidl=m_PidlMgr.Create(iItemType,tmpName);
    if(pidl)
    {
        if(!AddToEnumList(pidl))
            return E_FAIL;
    }
    else
        return E_FAIL;

    return S_OK;
}
BOOL CPidlEnum::AddToEnumList(LPITEMIDLIST pidl)
{
    LPENUMLIST  pNew = 
      (LPENUMLIST)_Module.m_Allocator.Alloc(sizeof(ENUMLIST));

    if(pNew)
    {
        //set the next pointer
        pNew->pNext = NULL;
        pNew->pidl = pidl;

        //is this the first item in the list?
        if(!m_pFirst)
        {
            m_pFirst = pNew;
            m_pCurrent = m_pFirst;
        }

        if(m_pLast)
            m_pLast->pNext = pNew; //add the new item 
                                   //to the end of the list

        //update the last item pointer
        m_pLast = pNew;

        return TRUE;
    }
    return FALSE;
}

处理 NSE 的文件夹

您知道,NSE 实际上是一个实现了一些必需 Shell 接口的 COM 服务器。Explorer 充当 COM 客户端与 NSE 合作。

以下是当您在树形视图中单击我们 NSE 根文件夹(带子文件夹)的图标时,Explorer 执行的初始步骤:

  • 首先,调用 CoCreateInstance 来创建我们 NSE 的一个实例。
  • 其次,查询实现 IPersistFolderIShellFolder 的文件夹对象的指针。
  • 然后,Explorer 检索指向文件夹对象 IPersistFolder 接口的指针,并调用其 Initialize 函数(同时传入我们 NSE 根文件夹的完整 PIDL)。
  • 文件夹对象成功初始化后,Explorer 将查询 IShellFolder 接口,并调用其函数以在树视图和 Shell 视图中显示根文件夹的内容。
    • Explorer 将调用 IShellFolder::EnumObjectsgrfFlags 将包含 SHCONTF_FOLDERS)来获取指向 IEnumIDList 接口的指针,并连续调用 IEnumIDList::Next 来检索根文件夹的子文件夹列表,这些子文件夹将显示在树视图中。
    • 然后,Explorer 将调用 IShellFolder::CreateViewObject 来创建文件夹视图对象,并返回指向其 IShellView 接口的指针。最后,Explorer 将调用 IShellView::CreateViewWindow 来在 Windows Explorer(Shell View)的右侧窗格中创建一个视图窗口,该窗口将包含一个列表视图,并填充根文件夹中对象的内容。

当您单击我们 NSE 根文件夹下的子文件夹条目时,将在我们的 DLL 中为每个子文件夹创建一个新的文件夹对象实例,方法是调用我们的 IShellFolder::BindToObject 函数。

IPersistFolder

文件夹初始化

如上所述,我们 NSE 根文件夹的完整 PIDL 必须保存,如果我们想提供更复杂的 UI 功能,例如在我们 NSE 中删除或创建新文件夹,并且希望我们的 NSE 在树视图和 Shell 视图中都能正确运行。当我们实现 NSE 时,我们使用 _Module.m_pidlNSFROOT 来保存它。

因为 Explorer 使用 IPersistFolder::Initialize 函数通知 Shell 文件夹对象进行初始化,如果返回错误值,Explorer 将认为 NSE 初始化有问题并立即停止加载扩展。因此,即使此函数什么也不做,它也**不**应返回 E_NOTIMPL,而是返回 S_OK(或 NOERROR)。

IShellFolder

此接口用于管理我们 NSE 中的文件夹。它是实现 NSE 的必需接口。我们可以通过调用 IShellFolder::GetUIObjectOf 函数通过此接口检索 NSE 实现的、可用于对指定文件对象或文件夹执行操作(如拖放)的 OLE 接口。

自定义数据成员

    CNWSPidlMgr m_PidlMgr;   //class instance use 
                             // to handle pidl operations  
    LPITEMIDLIST m_pidlPath; //store the complex PIDL 
                             // of current folder object

与子文件夹相关的成员函数

此接口中与子文件夹相关的函数包括:

  • BindToObject() - 当我们 NSE 中的子文件夹被浏览时调用。它的任务是为子文件夹创建一个新的 IShellFolder 对象,对其进行初始化,然后将新对象返回给 Shell。
  • EnumObjects() - 创建一个实现 IEnumIDList 的 COM 对象(枚举器),并将接口指针返回给调用者。调用者可以使用此接口枚举我们 NSE 中当前文件夹的内容。
  • GetAttributesOf() - 检索当前文件夹中一个或多个对象(文件或子文件夹)的属性(例如是否为文件夹)。像 Explorer 这样的调用者使用此函数的返回值来决定如何在树视图中显示当前文件夹中的对象。

绑定到子文件夹

STDMETHODIMP CMyVirtualFolder::BindToObject(LPCITEMIDLIST pidl,
                                            LPBC pbcReserved, 
                                            REFIID riid, 
                                            LPVOID *ppRetVal)
{
    *ppRetVal = NULL;

    HRESULT hr = S_OK;

    if( riid != IID_IShellFolder) 
        return E_NOINTERFACE;

    CComObject<CMyVirtualFolder> *pVFObj=0;

    hr = CComObject<CMyVirtualFolder>::CreateInstance(&pVFObj);
    if(FAILED(hr))
        return hr;

    pVFObj->AddRef();
    
    //cause relative pidl doesn't include path info,
    //so use Concatenate to make a absolute pidl
    LPITEMIDLIST pidlNew = m_PidlMgr.Concatenate(m_pidlPath,pidl);
    pVFObj->m_pidlPath = m_PidlMgr.Copy(pidlNew);   
    m_PidlMgr.Delete(pidlNew);

    hr = pVFObj->QueryInterface(riid, ppRetVal);

    ATLASSERT(pVFObj);
    if(pVFObj == NULL)
    {
        MessageBox(NULL,
           _T("CMyVirtualFolder::BindToObject() pVFObj=NULL"),
           _T("NSF"),MB_OK);
        return E_FAIL;
    }
    pVFObj->Release();

    return hr;
}

获取文件夹的枚举接口

如果您对 IEnumIDList 不了解,请回到本文的 IEnumIDList 部分。

STDMETHODIMP CMyVirtualFolder::EnumObjects(HWND hWnd, 
                                           DWORD grfFlags, 
                                           LPENUMIDLIST* ppEnumIDList)
{   
    HRESULT Hr;                                      
    CComObject<CPidlEnum> *pEnum;
    Hr = CComObject<CPidlEnum>::CreateInstance(&pEnum);
    if (SUCCEEDED(Hr))
    {
        // AddRef() the object while we're using it.
        pEnum->AddRef();

        // Init the enumerator. create all the items in 
        // this folder and add it to the enumerator's list.
        Hr = pEnum->_Init(m_pidlPath, grfFlags); //grfFlags will passed in 
                                                 //as the initialization 
                                                 //condition

        // Return an IEnumIDList interface to the caller.
        if (SUCCEEDED(Hr))
            Hr = pEnum->QueryInterface(IID_IEnumIDList, 
                                        (void**)ppEnumIDList);

        pEnum->Release();
    }
    return Hr;
}

检索文件夹中对象的属性

文件夹对象在系统调用 IShellFolder::GetAttributesOf 方法检索其属性时,应返回包括 SFGAO_FOLDER 在内的属性。如果文件夹有子文件夹,返回值将包括 SFGAO_HASSUBFOLDER

Explorer 将调用此函数来决定如何在树视图中显示当前对象(如果是文件夹对象,则在树视图中显示;如果文件夹对象有子文件夹,则在其文件夹图标旁边添加 '+' 符号)。

STDMETHODIMP CMyVirtualFolder::GetAttributesOf(UINT uCount, 
                                      LPCITEMIDLIST pidls[], 
                                      LPDWORD pdwAttribs)
{
    HRESULT Hr;
    *pdwAttribs = (DWORD)-1;

    if(( uCount==0 )||(pidls[0]->mkid.cb == 0)) 
    {
        // Can happen on Win95. Queries the view folder.
        DWORD dwAttr;
        dwAttr |= SFGAO_FOLDER | SFGAO_HASSUBFOLDER 
                  |SFGAO_HASPROPSHEET |SFGAO_DROPTARGET;
        *pdwAttribs &= dwAttr;
    }
    else
    {
        TCHAR szText[MAX_PATH]=_TEXT("");
        TCHAR szTmp[MAX_PATH]=_TEXT("");
        DWORD dwLen=MAX_PATH;

        for( UINT i=0; i<uCount; i++ ) 
        {
            DWORD dwAttr = 0;

            switch( m_PidlMgr.GetItemType(pidls[i]) ) 
            {
            case NWS_FOLDER:

                dwAttr |=SFGAO_FOLDER;

                //get the full pidl
                LPITEMIDLIST   tmpPidl;
                tmpPidl = m_PidlMgr.Concatenate(m_pidlPath, pidls[i]);

                _tcsnset(szText,0,MAX_PATH);
                _tcsnset(szTmp,0,MAX_PATH);

                m_PidlMgr.GetFullName(tmpPidl,szTmp,&dwLen);
                if( dwLen >0 )
                    _tcscat(szText,szTmp);

                m_PidlMgr.Delete(tmpPidl);

                //if current folder include subfolder, in 
                //Explorer's left treeView panel this folder 
                //has + can to expand
                if( TRUE == m_PidlMgr.HasSubFolder(szText) )
                    dwAttr |= SFGAO_HASSUBFOLDER;       

                break;                
            }
            *pdwAttribs &= dwAttr;
        }
    }
    
    return S_OK;
}

IShellView

我们对视图对象的实现是创建一个简单的 ListView 窗口,并处理我们用户界面的部分消息,这些消息有助于实现子文件夹(有一个窗口过程来处理消息)。实际上,当您在系统命名空间中选择一个文件夹时,Explorer 的内部视图对象将负责创建和管理一个 ListView 窗格。

为了使我们的 NSE 支持子文件夹,也就是说,当用户在我们的 ListView 中双击一个文件夹时,应该打开该子文件夹;在 Explorer 的树视图中,应该聚焦并展开被单击的子文件夹。要实现这一点,我们应该执行以下操作:

  • 要用当前文件夹的内容填充 ListView,我们应包含子文件夹和文件。
  • 我们必须处理 LVN_ITEMACTIVATE 通知,当用户在我们的 ListView 中双击文件夹对象时将触发该通知。
  • 我们必须保存作为 IShellView::CreateViewWindow **输入参数**传递的 IShellBrowser 指针,这允许我们的 NSE 与 Windows Explorer 窗口通信。在处理 LVN_ITEMACTIVATE 消息时,我们需要使用此接口的 BrowseObject 函数来指示 Explorer 浏览到被单击的文件夹。

为了简单起见,当用户双击我们 ListView 中的文件对象时,我们不做任何操作。

自定义主数据成员

public:
    HWND m_hWnd;     //handle of right panel 
                     //window (Shell view window) 
    HWND m_hwndList; //handle of listview
    //keep the IShellBrowser interface pointer to 
    //allow communication with the Windows Explorer 
    //window. 
    LPSHELLBROWSER m_pShellBrowser; 
    
protected:
    CMyVirtualFolder *m_pFolder; //pointer to current 
                                 //  folder object
    LPITEMIDLIST  m_pidlRoot;    //complex PIDL of 
                                 //  current folder
    CNWSPidlMgr m_PidlMgr;

填充 ListView

我们在处理 WM_CREATE 消息时填充 ListView。调用 IShellFolder::EnumObjects 枚举当前文件夹中的对象时,必须将 SHCONTF_NONFOLDERSSHCONTF_FOLDERS 都分配给 grfFlags

HRESULT CNSFShellView::_FillListView()
{
    DWORD dwFlags = SHCONTF_NONFOLDERS | SHCONTF_FOLDERS;
    LPENUMIDLIST pEnumIDList;
    HRESULT Hr;
    
    HR(m_pFolder->EnumObjects(m_hWnd, 
                             dwFlags, &pEnumIDList));
    {
        // Turn the listview's redrawing off
        ::SendMessage(m_hwndList, WM_SETREDRAW, FALSE, 0L);
      
        DWORD dwFetched = 0;
        LPITEMIDLIST pidl = NULL;

        while((S_OK == pEnumIDList->Next(1, &pidl, &dwFetched)) 
                                              && (dwFetched!=0))
        {
            LV_ITEM lvi = { 0 };
            
            //set the mask
            lvi.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_PARAM;
            
            //item will be add to the end of the list
            lvi.iItem = ListView_GetItemCount(m_hwndList);
            
            //set the object's simple PIDL to lvi.lParam
            lvi.lParam = (LPARAM)m_PidlMgr.Copy(pidl);
            
            //set icon
            if( m_PidlMgr.GetItemType(pidl) == NWS_FOLDER)
                lvi.iImage = ICON_INDEX_FOLDER;
            else
                lvi.iImage = ICON_INDEX_FILE;
            
            //Column 0:Name
            TCHAR szName[MAX_PATH]=TEXT("");
            m_PidlMgr.GetName(pidl,szName);
            lvi.pszText = szName;

            ListView_InsertItem(m_hwndList, &lvi);
            
            //Set Column 1:type text
            TCHAR szTemp[MAX_PATH]=TEXT("");
            HR(m_PidlMgr.GetItemAttributes(pidl,ATTR_TYPE,szTemp));
            ListView_SetItemText(m_hwndList,lvi.iItem, 
                                                  COL_TYPE,szTemp); 
        }

        pEnumIDList->Release();

        CListSortInfo sort = { m_pFolder, COL_NAME, TRUE };
        ListView_SortItems(m_hwndList, ListViewSortFuncCB, 
                                       (LPARAM) &sort );

        // Turn the listview's redrawing back 
        // on and force it to draw
        ::SendMessage(m_hwndList, WM_SETREDRAW, TRUE, 0L);
        ::InvalidateRect(m_hwndList, NULL, TRUE);
        ::UpdateWindow(m_hwndList);
        
    }
    return S_OK;
}

处理 LVN_ITEMACTIVATE 通知

首先,您必须在我们的 IShellView 实现的消息映射中声明一个 LVN_ITEMACTIVATE 的处理函数,如下所示:

BEGIN_MSG_MAP(CNSFShellView)
    ...... //declare other msg handler functions
    NOTIFY_CODE_HANDLER(LVN_ITEMACTIVATE, OnItemActivated)
    ...... //declare other msg handler functions  
END_MSG_MAP()

以下是我们处理浏览到文件夹的操作:

LRESULT CNSFShellView::OnItemActivated(UINT CtlID, 
                          LPNMHDR lpnmh, BOOL& bHandled)
{
    LV_ITEM   lvItem;
    ZeroMemory(&lvItem, sizeof(lvItem));

    lvItem.mask = LVIF_PARAM;

    LPNMLISTVIEW   lpnmlv = (LPNMLISTVIEW)lpnmh;
    lvItem.iItem = lpnmlv->iItem;

    if(ListView_GetItem(m_hwndList, &lvItem))
    {
        //folders to be activated
        if(NWS_FOLDER == 
           m_PidlMgr.GetItemType((LPITEMIDLIST)lvItem.lParam))
        {
       //Tells Windows Explorer to browse to another folder
            m_pShellBrowser->BrowseObject(
                     (LPITEMIDLIST)lvItem.lParam,            
                     SBSP_DEFBROWSER | SBSP_RELATIVE);            
        }
    }
    return 0;
}

保存 Shell 传递的 IShellBrowser 指针

STDMETHODIMP CNSFShellView::CreateViewWindow(
    LPSHELLVIEW lpPrevView,
    LPCFOLDERSETTINGS lpFS, 
    LPSHELLBROWSER pSB,
    LPRECT prcView, 
    HWND* phWnd)
{    
    ......
    m_pShellBrowser = pSB;
    ......
}

如何调试

要调试您的扩展,您需要从调试器中执行 Shell。请按照以下步骤操作:

  1. 将扩展项目加载到调试器中,但不要运行它。
  2. 在 Microsoft® Windows® 任务栏的“开始”菜单中,选择“关闭”。
  3. 按住 CTRL+ALT+SHIFT,然后在“关闭 Windows”对话框中单击“否”。在 Windows 2000 中,单击“取消”而不是“否”。Shell 现在已关闭,但所有其他应用程序仍在运行,包括调试器。
  4. 将调试器设置为使用 Windows 目录下的 Explorer.exe 来运行扩展 DLL。
  5. 从调试器中运行项目。Shell 将像往常一样启动,但调试器将附加到 Shell 的进程。

结论

编写 NSE 仍然是一项复杂的工作。本文的目的是帮助您开发带子文件夹的 NSE。在我的下一篇文章中,我将告诉您如何在自己的 NSE 中创建或删除子文件夹,并使 Explorer 的树视图和 Shell 视图以同步的方式反映这些更改。

© . All rights reserved.