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

编写命名空间扩展的完整入门指南 - 第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (54投票s)

2001年12月7日

27分钟阅读

viewsIcon

1663348

downloadIcon

9381

关于编写自己的 Explorer 命名空间扩展的详细教程。

目录

引言

从 Shell 的角度来看,您计算机的内容——硬盘驱动器、CD-ROM、映射的网络驱动器、桌面等等——被组织在一个大的树形结构中,桌面是最高节点,称为 _shell namespace_。Explorer 提供了一种通过 _namespace extensions_ 将自定义对象插入命名空间的方法。在本文中,我将介绍创建一个基本、简单的命名空间扩展所涉及的步骤。我们的扩展将创建一个虚拟文件夹,列出计算机上的驱动器,类似于下图所示的“我的电脑”列表。

本文假定您了解 C++、ATL 和 COM。熟悉 Shell 扩展也会有所帮助。

我知道这是一篇 _非常长的_ 文章,但是命名空间扩展极其复杂,我找到的最好的文档是 MSDN 中 RegView 示例(67K)中的注释。该示例功能齐全,但并没有解释命名空间中事件的内部顺序。Dino Esposito 的优秀书籍 _Visual C++ Windows Shell Programming_ 阐明了一些问题,并包含了一个 WinView 示例(下载源,1 MB),该示例基于 RegView。我从这两个来源中提取了信息,添加了大量跟踪消息来观察逻辑流程,并将所有内容整合到本文中。

本文附带的示例项目是一个基础扩展;它功能很少,但完全可用。(即使是“简单”的扩展也需要本文中看到的所有内容。)我故意避开了一些主题——例如命名空间中的子文件夹,以及与命名空间其他部分的交互——因为这只会使文章更长,代码更复杂。我可能会在未来的文章中介绍这些主题。

Explorer 的结构

Explorer 熟悉的双窗格视图实际上由几个部分组成,这些部分对命名空间扩展都很重要。各部分下图所示

 [Parts of Explorer - 32K]

在上图中,诸如 _控制面板_ 和 _注册表视图_ 等项目是 _虚拟文件夹_。它们不显示文件系统的部分,而是显示文件夹状的 UI,以公开命名空间扩展提供的某种功能。扩展在右窗格中显示其 UI,称为 _Shell 视图_。扩展还可以使用 Explorer 提供的 COM 接口来操作 Explorer 的菜单、工具栏和状态栏。Explorer 管理树视图,显示命名空间,而扩展对树的控制仅限于显示子文件夹。

命名空间扩展的结构

命名空间扩展的内部结构当然取决于您使用的编译器和编程语言。但是,有一个重要的共同元素,即 _PIDL_。PIDL(与“fiddle”押韵)代表 **p**ointer to an **ID** **l**ist,它是 Explorer 用于组织在树视图中显示的项和子文件夹的数据结构。虽然数据的确切格式由扩展定义,但有关数据在内存中的组织方式有一些规则。这些规则定义了 PIDL 的通用格式,以便 Explorer 可以处理来自任何扩展的 PIDL,而无需考虑其内部结构。

我知道这相当模糊,但目前,只要知道 PIDL 是扩展存储其自身有意义数据的方式就足够了。我将在本文的后面详细介绍 PIDL 的所有细节以及如何构建它们。

扩展的另一个主要部分是它必须实现的 COM 接口。必需的接口是

  • IShellFolder:在 Explorer 和实现虚拟文件夹的代码之间提供通信通道。
  • IEnumIDList:一个 COM 枚举器,允许 Explorer 或 Shell 视图枚举虚拟文件夹的内容。
  • IShellView:管理出现在 Explorer 右窗格中的窗口。

更复杂的扩展还可以实现自定义 Explorer 树视图部分的接口,但由于此处介绍的扩展故意保持简单,因此本文不介绍这些接口。

PIDL

什么是 PIDL

Explorer 命名空间中的每个项,无论是文件、目录、控制面板小程序,还是扩展公开的对象,都可以由其 PIDL 唯一指定。对象的 _绝对 PIDL_ 类似于文件的完全限定路径;它是对象本身的 PIDL 及其所有父文件夹的 PIDL 的连接。因此,例如,指向系统控制面板小程序(System Control Panel applet)的绝对 PIDL 可以看作是 `[Desktop]\[My Computer]\[Control Panel]\[System applet]`。

_相对 PIDL_ 只是对象本身的 PIDL,相对于其父文件夹。这样的 PIDL 仅对包含该对象的虚拟文件夹有意义,因为该文件夹是唯一可以理解 PIDL 中数据的对象。

本文中的扩展处理相对 PIDL,因为与其他命名空间部分没有通信。(这样做需要构建绝对 PIDL。)

PIDL 的结构

PIDL 是一个类似于单向链表的结构,但没有指针。PIDL 由一系列 `ITEMIDLIST` 结构组成,这些结构紧密排列在连续的内存块中。`ITEMIDLIST` 只有一个成员,即 `SHITEMID` 结构。

typedef struct _ITEMIDLIST
{
    SHITEMID  mkid;
} ITEMIDLIST;

`SHITEMID` 的定义如下:

typedef struct _SHITEMID
{
    USHORT cb;       // Size of the ID (including cb itself)
    BYTE   abID[1];  // The item ID (variable length)
} SHITEMID;

`cb` 成员保存整个 `struct` 的大小,并作为单向链表中的“下一个”指针工作。`abID` 成员是命名空间扩展存储其私有数据的地方。此成员的长度可以是任意长度;`cb` 的值指示其确切大小。例如,如果一个扩展存储了 12 字节的数据,`cb` 将是 14(12 + `sizeof(USHORT)`)。`abID` 处存储的数据可以是命名空间有意义的任何内容,但是,文件夹中没有两个对象可以具有相同的数据,就像目录中没有两个文件可以具有相同的文件名一样。

PIDL 的末尾由一个 `SHITEMID` `struct` 指示,其 `cb` 设置为 0,就像链表使用 NULL 的下一个指针指示列表末尾一样。

这是一个仅包含一个数据块的示例 PIDL,变量 `pPidl` 指向列表的开头。

 [Simple PIDL - 2K]

注意,我们如何通过将每个 `struct` 的 `cb` 值添加到指针来从一个 `SHITEMID` `struct` 移动到下一个。

现在,您可能会问,如果 Explorer 不知道数据格式,`SHITEMID` 或 PIDL 有什么用?答案是,Explorer 将 PIDL 视为不透明数据类型,它只将它们传递给命名空间。在这方面,它们非常类似于句柄。当您有一个 `HWND` 时,您不会关心窗口背后的内部数据结构,但您知道通过将窗口的句柄传递回操作系统,您可以对窗口进行所有操作。PIDL 则相反——Explorer 不知道 PIDL 底层的数据,但它可以将 PIDL 传递给命名空间来与之交互。

我们的命名空间的 PIDL 数据

如上所述,用于标识命名空间文件夹中项的数据在文件夹内必须是唯一的。幸运的是,驱动器字母已经是驱动器的唯一标识符,因此我们只需将字母存储在 `abID` 字段中。我们的 PIDL 数据定义为 `PIDLDATA` `struct`。

struct PIDLDATA
{
    TCHAR chDriveLtr;
};

命名空间扩展接口

IEnumIDList

IEnumIDList 是一个 COM 枚举器的实现,它枚举 PIDL 集合。COM 枚举器实现允许对集合进行顺序访问的函数,这与 STL 集合中的 `iterator` 非常相似。ATL 提供了实现枚举器的类,因此我们只需提供数据集合并告诉 ATL 如何复制 PIDL。

IEnumIDList 在两种情况下使用:

  1. Shell 视图需要枚举文件夹的内容才能知道显示什么。
  2. Explorer 需要枚举文件夹的子文件夹才能填充树视图。

由于我们的扩展不包含子文件夹,因此我们只遇到第一种情况。

IShellFolder, IPersistFolder

IShellFolder 是 Explorer 用于初始化和与扩展通信的接口。当需要扩展创建其视图窗口时,Explorer 会调用 `IShellFolder` 方法。`IShellFolder` 还有用于枚举扩展的虚拟文件夹内容以及比较文件夹中的两个项以进行排序的方法。

IPersistFolder 有一个 `Initialize()` 方法,该方法在调用时允许扩展执行任何启动初始化任务。

IShellView, IOleCommandTarget

IShellView 是 Explorer 通过它通知扩展 UI 相关事件的接口。`IShellView` 具有告诉扩展创建和销毁视图窗口、刷新显示等的函数。Explorer 使用 `IOleCommandTarget` 将命令发送到视图,例如在用户按下 F5 时的刷新命令。

IShellBrowser

IShellBrowser 是 Explorer 公开的一个接口,它允许扩展操作 Explorer 窗口。`IShellBrowser` 具有更改菜单、工具栏和状态栏以及向 Explorer 中的控件发送通用消息的函数。

我们的实现

PIDL 管理器类

为了简化 PIDL 的处理,我们的扩展使用一个名为 `CPidlMgr` 的辅助类来执行 PIDL 操作。我将在此处介绍重要部分,即创建 PIDL、返回我们在 PIDL 中存储的数据以及返回 PIDL 的文本描述。这是该类的相关部分声明

class CPidlMgr  
{
public:
   // Create a relative PIDL that stores a drive letter.
   LPITEMIDLIST Create ( const TCHAR );
   // Get the drive letter from a PIDL.
   TCHAR GetData ( LPCITEMIDLIST );
   // Create a text description of a PIDL.
   DWORD GetPidlPath ( LPCITEMIDLIST, LPTSTR );
 
private:
   // The shell's memory allocator.
   CComPtr<IMalloc> m_spMalloc;
};

创建新的 PIDL

Create()` 函数接受一个驱动器字母并创建一个包含该驱动器字母作为其数据的相对 PIDL。我们首先计算 PIDL 中第一个项所需的内存。

LPITEMIDLIST CPidlMgr::Create ( const TCHAR chDrive )
{
UINT uSize = sizeof(ITEMIDLIST) + sizeof(PIDLDATA);

请记住,PIDL 中的一个节点是 `ITEMIDLIST` `struct`,它包含我们的 `PIDLDATA` `struct`。接下来,我们使用 Shell 的内存分配器为第一个节点分配内存,以及一个将标记 PIDL 末尾的第二个 `ITEMIDLIST`。

LPITEMIDLIST pidlNew = 
              (LPITEMIDLIST) m_spMalloc->Alloc(uSize + sizeof(ITEMIDLIST));

现在,我们必须填充 PIDL 的内容。为了设置第一个节点,我们设置 `SHITEMID` `struct` 的成员。`cb` 成员设置为 `uSize`,即第一个节点的大小。

   if ( pidlNew )
       {
       LPITEMIDLIST pidlTemp = pidlNew;
       pidlTemp->mkid.cb = uSize;

然后我们将 PIDL 数据存储在 `abID` 成员中(`struct` 末尾的可变长度内存块)。

       PIDLDATA* pData = (PIDLDATA*) pidlTemp->mkid.abID;
       pData->chDriveLtr = chDrive;

接下来,我们将 `pidlTemp` 高速缓存到第二个节点,并将其成员设置为零以标记 PIDL 的末尾。

       // GetNextItem() is a CPidlMgr helper function.
       pidlTemp = GetNextItem ( pidlTemp );
       pidlTemp->mkid.cb = 0;
       pidlTemp->mkid.abID[0] = 0;
       }
 
    return pidlNew;
}

从 PIDL 获取驱动器号

GetData()` 函数读取 PIDL 并返回存储在 PIDL 中的驱动器字母。

TCHAR CPidlMgr::GetData ( LPCITEMIDLIST pidl )
{
PIDLDATA* pData;
 
    pData = (PIDLDATA*)( pidl->mkid.abID );
    return pData->chDriveLtr;
}

获取 PIDL 的文本描述

我将在此处介绍的最后一个方法 `GetPidlDescription()` 返回 PIDL 的文本描述。

void CPidlMgr::GetPidlDescription ( LPCITEMIDLIST pidl, LPTSTR szDesc )
{
TCHAR chDrive = GetData ( pidl );
 
    if ( '\0' != chDrive )
        wsprintf ( szDesc, _T("Drive %c:"), chDrive );
    else
        *szDesc = '\0';
}

`GetPidlDescription()` 使用 `GetData()` 从 PIDL 读取驱动器字母,然后返回一个字符串,如“Drive A:”,该字符串可以在用户界面中显示。

IEnumIDList

当我们的扩展收到枚举器请求时,我们会创建一个驱动器字母集合,代表要在 Shell 视图中显示的驱动器。然后,我们使用 ATL 的 `CComEnumOnSTL` 类来创建枚举器。

使用 `CComEnumOnSTL` 的要求

`CComEnumOnSTL` 需要我们提供四样东西:

  1. 要实现的枚举器的接口,在我们的例子中是 `IEnumIDList`。
  2. 从枚举器返回的数据类型,在我们的例子中是 `LPITEMIDLIST`。
  3. 保存数据的集合类型。
  4. 一个 _复制策略类_。

保存数据的集合必须是 STL 容器,例如 `vector` 或 `list`。我们的扩展将使用 `vector<TCHAR>` 来保存驱动器字母。

ATL 在需要初始化、复制或销毁元素时调用复制策略类中的方法。复制策略类的通用形式是:

// SRCTYPE is the type of the objects in the collection.
// DESTTYPE is the type being returned from the enumerator.
class CopyPolicy
{
public:
    // initialize an object before copying into it
    static void init ( DESTTYPE* p );
    // copy an element
    static HRESULT copy ( DESTTYPE* p1, SRCTYPE* p2 );
    // destroy an element
    static void destroy ( DESTTYPE* p );
};

这是我们的复制策略类:

class CCopyTcharToPidl  
{
public:
    static void init ( LPITEMIDLIST* p ) 
    {
        // No init needed.
    }
 
    static HRESULT copy ( LPITEMIDLIST* pTo, const TCHAR* pFrom )
    {
        *pTo = m_PidlMgr.Create ( *pFrom );
        return (NULL != *pTo) ? S_OK : E_OUTOFMEMORY;
    }
 
    static void destroy ( LPITEMIDLIST* p ) 
    {
        m_PidlMgr.Delete ( *p ); 
    }
 
private:
    static CPidlMgr m_PidlMgr;
};

这相当直接;我们使用 `CPidlMgr` 来完成创建和删除 PIDL 的工作。我们还需要最后一个 `typedef`,将所有这些组合成一个类。

typedef 
  // name and IID of enumerator interface
  CComEnumOnSTL<IEnumIDList, &IID_IEnumIDList,
  // type of object to return
  LPITEMIDLIST,
  // copy policy class
  CCopyTcharToPidl,
  // type of collection holding the data
  std::vector<TCHAR> >
  CEnumIDListImpl;

IShellFolder

当 Explorer 创建我们的命名空间扩展时,它首先实例化一个 `IShellFolder` 对象。`IShellFolder` 具有浏览到新虚拟文件夹、创建 Shell 视图窗口以及对文件夹内容执行操作的方法。重要的 `IShellFolder` 方法是:

  • GetClassID() - 从 `IPersist` 继承。将我们的对象 CLSID 返回给 Explorer。
  • Initialize() - 从 `IPersistFolder` 继承。让我们有机会执行一次性初始化。
  • BindToObject() - 当我们命名空间的一部分中的文件夹正在被浏览时调用。它的作用是创建一个新的 `IShellFolder` 对象,用正在浏览的文件夹的 PIDL 初始化它,并将新对象返回给 Shell。
  • CompareIDs() - 负责比较两个 PIDL 并返回它们的相对顺序。
  • CreateViewObject() - 当 Explorer 希望我们创建 Shell 视图时调用。它创建一个新的 `IShellView` 对象并将其返回给 Explorer。
  • EnumObjects() - 创建一个新的 PIDL 枚举器,它可以枚举虚拟文件夹的内容。
  • GetAttributesOf() - 返回虚拟文件夹中项的属性(例如只读)。
  • GetUIObjectOf() - 返回实现与虚拟文件夹中的项相关联的 UI 元素(例如上下文菜单)的 COM 对象。

我将在下面详细介绍两个重要的方法:`CreateViewObject()` 和 `EnumObjects()`。

创建 Shell 视图

当 Explorer 希望我们的扩展在 Shell 视图窗格中创建窗口时,它会调用 `CreateViewObject()`。`CreateViewObject()` 的原型是:

STDMETHODIMP IShellFolder::CreateViewObject (
    HWND hwndOwner,
    REFIID riid,
    void** ppvOut );

`hwndOwner` 是 Explorer 中将成为我们视图窗口父级的窗口。`riid` 和 `ppvOut` 是 Explorer 请求的接口的 IID(在我们的示例中是 `IID_IShellView`)和一个我们将存储请求的接口指针的输出参数。我们的 `CreateViewObject()` 方法创建一个新的 `CShellViewImpl` COM 对象(我们实现 `IShellView` 的类,我将在后面介绍)。

STDMETHODIMP CShellFolderImpl::CreateViewObject ( HWND hwndOwner, 
                                           REFIID riid, void** ppvOut )
{
HRESULT hr;
CComObject<CShellViewImpl>* pShellView;
 
    // Create a new CShellViewImpl COM object.
    hr = CComObject<CShellViewImpl>::CreateInstance ( &pShellView );
 
    if ( FAILED(hr) )
        return hr;

这使用了 `CComObject` 来创建一个新的 `CShellViewImpl` 对象。接下来,我们调用 `CShellViewImpl` 中的一个私有初始化函数,并将文件夹对象的指针传递给它。该视图稍后将在调用 `EnumObjects()` 和 `CompareIDs()` 时使用此指针。

    // AddRef() the object while we're using it.
    pShellView->AddRef();
 
    // Object initialization - pass the object its containing folder (this).
    hr = pShellView->_init ( this );
 
    if ( FAILED(hr) )
        {
        pShellView->Release();
        return hr;
        }

最后,我们查询 `CShellViewImpl` 对象以获取 Explorer 请求的接口。

    // Return the requested interface back to the shell.
    hr = pShellView->QueryInterface ( riid, ppvOut );
 
    pShellView->Release();
    return hr;
}

(此方法不将 `hwndOwner` 传递给视图对象,但视图对象会自行检索父窗口,因此这没问题。)

枚举我们虚拟文件夹中的对象

在我们的简单扩展中,当视图对象需要知道它正在显示的文件夹的内容时,它会调用 `EnumObjects()`。请注意此处功能的清晰分离:Shell Folder 知道内容,但没有 UI 代码;Shell View 处理 UI,但本身不了解文件夹的内容。

`EnumObjects()` 的原型是:

STDMETHODIMP IShellFolder::EnumObjects (
    HWND hwndOwner,
    DWORD dwFlags,
    LPENUMIDLIST* ppEnumIDList );

`hwndOwner` 是一个窗口,可以作为该方法可能需要显示的任何对话框或消息框的父窗口。`dwFlags` 用于告诉方法返回哪种类型的对象(例如,仅子文件夹或仅非文件夹)。我们的扩展没有子文件夹,因此我们无需检查标志。`ppEnumIDList` 是一个输出参数,我们在其中存储指向方法创建的枚举器对象的 `IEnumIDList` 接口。

我们的 `EnumObjects()` 方法创建一个新的 `CEnumIDListImpl` 对象,并用系统上的驱动器字母填充 `vector<TCHAR>`。枚举器对象使用 `vector` 和我们的复制策略类(如上文“使用 `CComEnumOnSTL` 的要求”部分所述)来返回 PIDL。

这是我们的 `EnumObjects()` 的开头。我们首先填充 `vector`(它是成员 `m_vecDriveLtrs`)。

STDMETHODIMP CShellFolderImpl::EnumObjects ( HWND hwndOwner, 
                        DWORD dwFlags, LPENUMIDLIST* ppEnumIDList )
{
HRESULT hr;
DWORD   dwDrives;
int     i;
 
    // Enumerate all drives on the system
    // and put the letters of the drives into a vector.
    m_vecDriveLtrs.clear();
 
    for ( i = 0, dwDrives = GetLogicalDrives(); i <= 25; i++ )
         if ( dwDrives & (1 << i) )
             m_vecDriveLtrs.push_back ( 'A' + i );

接下来,我们创建一个 `CEnumIDListImpl` 对象。

    // Create an enumerator with CComEnumOnSTL<> and our copy policy class.
CComObject<CEnumIDListImpl>* pEnum;
 
    hr = CComObject<CEnumIDListImpl>::CreateInstance ( &pEnum );
 
    if ( FAILED(hr) )
        return hr;
 
    // AddRef() the object while we're using it.
    pEnum->AddRef();

接下来,我们初始化枚举器,将文件夹的 `IUnknown` 接口和对 `vector` 的引用传递给它。`CComEnumOnSTL` 调用 `AddRef` 来确保文件夹 COM 对象在枚举器使用它时保留在内存中。

    hr = pEnum->Init ( GetUnknown(), m_vecDriveLtrs );

最后,我们将 `IEnumIDList` 接口返回给调用者。

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

IShellView

我们的 `IShellView` 实现以报表模式创建列表控件(这是命名空间扩展显示数据最常见的方式,因为它遵循 Explorer 本身的做法)。`CShellViewImpl` 类还派生自 ATL 的 `CWindowImpl` 类,这意味着 `CShellViewImpl` 是一个窗口并具有消息映射。`CShellViewImpl` 创建自己的窗口,然后将列表控件创建为子窗口。这样,`CShellViewImpl` 的消息映射就会收到来自列表控件的通知消息。`CShellViewImpl` 还派生自 `IOleCommandTarget`,以便它可以接收来自 Explorer 的命令。

重要的 `IShellView` 方法是:

  • GetWindow() - 从 `IOleWindow` 继承。返回我们的 Shell 视图的窗口句柄。
  • CreateViewWindow() - 创建一个新的 Shell 视图窗口。
  • DestroyViewWindow() - 销毁 Shell 视图窗口,并允许我们执行任何清理任务。
  • GetCurrentInfo() - 在 `FOLDERSETTINGS` `struct` 中返回我们视图的当前视图设置。`FOLDERSETTINGS` 如下所述。
  • Refresh() - 在我们必须刷新 Shell 视图内容时调用。
  • UIActivate() - 在我们的视图获得或失去焦点时调用。此方法是视图可以修改 Explorer UI 以添加自定义命令的时候。

我将在本文中详细介绍 `CreateViewWindow()` 和 `UIActivate()`,因为这是大部分 UI 操作发生的地方。

CShellViewImpl 类列表

管理 UI 需要保存大量状态信息,因此我将此处的数据与类声明一起列出。

class ATL_NO_VTABLE CShellViewImpl : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CShellViewImpl, &CLSID_ShellViewImpl>,
    public IShellView,
    public IOleCommandTarget,
    public CWindowImpl<CShellViewImpl>
{
public:
DECLARE_NO_REGISTRY()
DECLARE_WND_CLASS(NULL)
 
BEGIN_COM_MAP(CShellViewImpl)
    COM_INTERFACE_ENTRY(IShellView)
    COM_INTERFACE_ENTRY(IOleWindow)
    COM_INTERFACE_ENTRY(IOleCommandTarget)
END_COM_MAP()
 
BEGIN_MSG_MAP(CShellViewImpl)
    MESSAGE_HANDLER(WM_CREATE, OnCreate)
    MESSAGE_HANDLER(WM_SIZE, OnSize)
    // ...
END_MSG_MAP()

到目前为止,这是标准的东西。请注意 `DECLARE_NO_REGISTRY()` 宏 - 这告诉 ATL 此 COM 对象不需要注册,也没有对应的 _.RGS_ 文件。跳到私有数据,我们首先有一些变量来保存各种 UI 状态。

private:
    CPidlMgr     m_PidlMgr;
    UINT         m_uUIState;
    int          m_nSortedColumn;
    bool         m_bForwardSort;
    FOLDERSETTINGS m_FolderSettings;

`m_uUIState` 保存以下列表中的一个常量:

  • SVUIA_ACTIVATE_FOCUS - 我们的视图窗口具有焦点。
  • SVUIA_ACTIVATE_NOFOCUS - 我们的视图窗口在 Explorer 中可见,但其他窗口(树视图或地址栏)当前具有焦点。
  • SVUIA_DEACTIVATE - 我们的视图窗口即将失去焦点并被隐藏或销毁(例如,刚刚在树视图中选择了另一个文件夹)。

在添加或删除我们自己的命令到 Explorer 菜单时使用此成员。接下来是 `m_nSortedColumn` 和 `m_bForwardSort`,它们描述列表控件的内容当前是如何排序的。最后是 `m_FolderSettings`,Explorer 将其传递给我们。它包含关于视图窗口建议外观的各种标志。

窗口和 UI 对象句柄如下:

    HWND         m_hwndParent;
    HMENU        m_hMenu;
    CContainedWindowT<ATLControls::CListViewCtrl> m_wndList;

`m_hwndParent` 是 Explorer 中用作我们自己窗口父级的窗口。`m_hMenu` 是 Explorer 和我们扩展共享的菜单的句柄。最后,`m_wndList` 是来自 _atlcontrols.h_(包含在源 zip 文件中)的列表控件包装器,我们用它来管理我们的列表控件。

接下来是几个接口指针:

    CShellFolderImpl*      m_psfContainingFolder;
    CComPtr<IShellBrowser> m_spShellBrowser;

`m_psfContainingFolder` 是创建视图的 `CShellFolderImpl` 对象的接口。`m_spShellBrowser` 是 Explorer 传递给视图的 `IShellBrowser` 接口指针,它允许视图操作 Explorer 窗口(例如,修改菜单)。

最后是一些成员函数。`FillList()` 填充列表控件。`CompareItems()` 是对列表内容进行排序时使用的回调。`HandleActivate()` 和 `HandleDeactivate()` 是修改 Explorer 菜单以便我们的自定义命令出现在菜单中的辅助函数。

    void FillList();
    static int CALLBACK CompareItems ( LPARAM l1, LPARAM l2, LPARAM lData );
    void HandleActivate(UINT uState);
    void HandleDeactivate();
};

视图是如何创建的

这是我们的 Shell 视图创建时发生的事件顺序:

  1. CShellFolderImpl::CreateViewObject() 创建一个 `CShellViewImpl` 并调用 `_init()`(这是设置 `m_psfContainingFolder` 的方式)。
  2. Explorer 调用 `CShellViewImpl::CreateViewWindow()`。
  3. CShellViewImpl::CreateViewWindow() 创建一个容器窗口。
  4. CShellViewImpl::OnCreate() 处理在上一步中发送的 `WM_CREATE` 消息,并将列表控件作为容器窗口的子窗口创建。
CreateViewWindow()

`CreateViewWindow()` 负责创建 Shell 视图窗口并将其句柄返回给 Explorer。原型是:

STDMETHODIMP IShellView::CreateViewWindow (
    LPSHELLVIEW pPrevView, 
    LPCFOLDERSETTINGS lpfs,
    LPSHELLBROWSER psb, 
    LPRECT prcView,
    HWND* phWnd );

`pPrevView` 是一个指向正在被替换的先前 Shell 视图的指针,如果存在的话。我们的扩展不使用它。`lpfs` 指向一个 `FOLDERSETTINGS` `struct`,我在上一节中已对其进行了描述。`psb` 是 Explorer 提供的 `IShellBrowser` 接口。我们使用它来修改 Explorer UI。`prcView` 指向一个 `RECT`,它保存了我们的容器窗口应该占据的坐标。最后,`phWnd` 是一个输出参数,我们将在此处返回容器的窗口句柄。

我们的 `CreateViewWindow()` 首先初始化一些成员数据:

STDMETHODIMP CShellViewImpl::CreateViewWindow ( 
    LPSHELLVIEW pPrevView,
    LPCFOLDERSETTINGS lpfs,
    LPSHELLBROWSER psb, 
    LPRECT prcView,
    HWND* phWnd )
{
    // Init member variables.
    m_spShellBrowser = psb;
    m_FolderSettings = *lpfs;
 
    // Get the parent window from Explorer.
    m_spShellBrowser->GetWindow( &m_hwndParent );

然后我们创建容器窗口(请记住 `CShellViewImpl` 继承自 `CWindowImpl`)。

    // Create a container window, which will be the parent of the list control.
    if ( NULL == Create ( m_hwndParent, *prcView ) )
       return E_FAIL;
 
    // Return our window handle to the browser.
    *phWnd = m_hWnd;
 
    return S_OK;
}
OnCreate()

上面的 `CWindowImpl::Create()` 调用会生成一个 `WM_CREATE` 消息,`CShellViewImpl` 的消息映射将其路由到 `CShellViewImpl::OnCreate()`。`OnCreate()` 创建一个列表控件并将 `m_wndList` 附加到它上面。

LRESULT CShellViewImpl::OnCreate ( UINT uMsg, WPARAM wParam, 
                                   LPARAM lParam, BOOL& bHandled )
{
HWND hwndList;
DWORD dwListStyles = WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER |
                       LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_SHAREIMAGELISTS;
DWORD dwListExStyles = WS_EX_CLIENTEDGE;
DWORD dwListExtendedStyles = LVS_EX_FULLROWSELECT | LVS_EX_HEADERDRAGDROP;
 
    // Set the list view's display style (large/small/list/report) based on
    // the FOLDERSETTINGS we were given in CreateViewWindow().
    switch ( m_FolderSettings.ViewMode )
        {
        case FVM_ICON:      dwListStyles |= LVS_ICON;      break;
        case FVM_SMALLICON: dwListStyles |= LVS_SMALLICON; break;
        case FVM_LIST:      dwListStyles |= LVS_LIST;      break;
        case FVM_DETAILS:   dwListStyles |= LVS_REPORT;    break;
        DEFAULT_UNREACHABLE;
        }

这会设置列表控件的窗口样式。接下来,我们创建列表控件并将 `m_wndList` 附加到它上面。

    // Create the list control.  Note that m_hWnd (inherited from CWindowImpl)
    // has already been set to the container window's handle.
    hwndList = CreateWindowEx ( dwListExStyles, WC_LISTVIEW, NULL, dwListStyles,
                                0, 0, 0, 0, m_hWnd, (HMENU) sm_uListID, 
                                _Module.GetModuleInstance(), 0 );
 
    if ( NULL == hwndList )
        return -1;
 
    m_wndList.Attach ( hwndList );
 
    // omitted - set up columns & image lists.
 
    FillList();
    return 0;
}

填充列表控件

CShellViewImpl::FillList() 负责填充列表控件。它首先调用其包含的 Shell Folder 的 `EnumObjects()` 方法来获取文件夹内容的枚举器。

void CShellViewImpl::FillList()
{
CComPtr<IEnumIDList> pEnum;
LPITEMIDLIST pidl = NULL;
HRESULT hr;
 
    // Get an enumerator object for the folder's contents.  Since this simple
    // extension doesn't deal with subfolders, we request only non-folder
    // objects.
    hr = m_psfContainingFolder->EnumObjects ( m_hWnd, SHCONTF_NONFOLDERS, &pEnum );
 
    if ( FAILED(hr) )
        return;

然后我们开始枚举文件夹的内容,并为每个驱动器添加一个列表项。我们复制每个 PIDL 并将其存储在每个列表项的数据区域中以供以后使用。

DWORD dwFetched;
 
    while ( pEnum->Next(1, &pidl, &dwFetched) == S_OK )
        {
        LVITEM lvi = {0};
        TCHAR szText[MAX_PATH];
 
        lvi.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_PARAM;
        lvi.iItem = m_wndList.GetItemCount();
        lvi.iImage = 0;
        
        // Store a PIDL for the drive letter,
        // using the lParam member for each item
        TCHAR chDrive = m_PidlMgr.GetData ( pidl );
        lvi.lParam = (LPARAM) m_PidlMgr.Create ( chDrive );

至于项的文本,我们使用 `CPidlMgr::GetPidlDescription()` 来获取字符串。

        // Column 1: Drive letter
        m_PidlMgr.GetPidlDescription ( pidl, szText );
        lvi.pszText = szText;
 
        m_wndList.InsertItem ( &lvi );

我省略了填充其他列的代码,因为这只是直接的列表控件调用。最后,我们按第一列对列表进行排序。`CListSortInfo` 是一个 `struct`,它保存了 `CompareItems()` 回调所需的 Suo 信息。第二个成员(`SIMPNS_SORT_DRIVELETTER`)指示要按哪一列排序。

    // Sort the items by drive letter initially.
CListSortInfo sort = { m_psfContainingFolder, SIMPNS_SORT_DRIVELETTER, true };
 
    m_wndList.SortItems ( CompareItems, (LPARAM) &sort );
}

这是生成的列表的外观:

 [Extension drive list - 31K]

处理窗口激活和停用

Explorer 调用 `CShellViewImpl::UIActivate()` 来通知我们何时窗口获得或失去焦点。当这些事件发生时,我们可以添加或删除 Explorer 菜单和工具栏上的命令。在本节中,我将介绍我们如何处理激活消息;下一节将介绍修改 UI。

`UIActivate()` 相当简单,它比较新状态与上次保存状态,然后将调用委托给 `HandleActive()` 辅助函数。

STDMETHODIMP CShellViewImpl::UIActivate ( UINT uState )
{
    // Nothing to do if the state hasn't changed since the last call.
    if ( m_uUIState == uState )
        return S_OK;
    
    // Modify the Explorer menu and status bar.
    HandleActivate ( uState );
 
    return S_OK;
}

HandleActivate() 将在下一节中介绍。在处理窗口焦点时有几个棘手的情况。我们的容器窗口具有 `WS_TABSTOP` 样式,意味着用户可以通过 Tab 键访问该窗口。由于容器窗口本身没有 UI,它只是将焦点设置到列表控件。

LRESULT CShellViewImpl::OnSetFocus ( UINT uMsg, WPARAM wParam, 
                                       LPARAM lParam, BOOL& bHandled )
{
    m_wndList.SetFocus();
    return 0;
}

另一个棘手的情况是当用户直接单击列表控件以使其获得焦点时。通常,Explorer 会跟踪哪个窗口拥有焦点。由于列表不被 Explorer 拥有或管理,因此当列表直接获得焦点时,它不会收到通知。结果,Explorer 会丢失对焦点窗口的跟踪。当我们在列表中收到 `NM_SETFOCUS` 消息,表示它获得了焦点时,我们会调用 `IShellBrowser::OnViewWindowActivate()` 来告诉 Explorer 我们的视图窗口现在拥有焦点。

LRESULT CShellViewImpl::OnListSetfocus ( int idCtrl, 
                              LPNMHDR pnmh, BOOL& bHandled )
{
    // Tell the browser that we have the focus.
    m_spShellBrowser->OnViewWindowActive ( this );
 
    HandleActivate ( SVUIA_ACTIVATE_FOCUS );
    return 0;
}

修改 Explorer 的菜单

命名空间扩展可以更改 Explorer 的菜单和工具栏以添加自己的命令。在开发过程中,我无法可靠地修改工具栏,因此示例扩展仅修改菜单。我们的扩展在修改菜单时使用两个辅助函数:`HandleActivate()` 进行修改,`HandleDeactivate()` 进行移除。我们有两种不同的菜单,一种是当列表控件具有焦点时,另一种不是。它们在此处展示。

 [Extension menus - 4K]

此弹出菜单插入到 Explorer 的 _帮助_ 菜单之前。_浏览驱动器_ 项打开另一个 Explorer 窗口指向所选驱动器。_系统属性_ 项运行系统控制面板小程序。我们还在帮助菜单中添加了一个项,用于显示我们自己的关于框。

`HandleActivate()` 接受一个参数,即 Explorer 即将进入的 UI 状态。它首先调用 `HandleDeactivate()` 来撤消之前的菜单修改并销毁旧菜单。

void CShellViewImpl::HandleActivate ( UINT uState )
{
    // Undo our previous changes to the menu.
    HandleDeactivate();

我将很快介绍 `HandleDeactivate()`。接下来,如果我们的窗口正在被激活,我们可以开始修改菜单。我们首先创建一个新的、空的菜单。

    // If we are being activated, add our stuff to Explorer's menu.
    if ( SVUIA_DEACTIVATE != uState )
        {
        // First, create a new menu.
        ATLASSERT(NULL == m_hMenu);
        m_hMenu = CreateMenu();

下一步是调用 `IShellBrowser::InsertMenusSB()`,它允许 Explorer 将其菜单项放入新创建的菜单中。`InsertMenusSB()` 从 OLE 容器获取逻辑,这些容器也具有共享菜单。我们的扩展创建一个 `OLEMENUGROUPWIDTHS` `struct` 并将该结构以及菜单句柄传递给 `InsertMenusSB()`。该 `struct` 有一个包含六个 `LONG` 的数组,代表菜单内的六个“组”。容器(在此例中为 Explorer)使用组 0、2 和 4;而包含的对象(我们的扩展)使用组 1、3 和 5。Explorer 将数组的索引 0、2 和 4 填充为它放入每个组的顶级菜单项的数量。正常情况下,数组返回 {2, 0, 3, 0, 1, 0},代表第一个组中的两个菜单( _文件_、_编辑_),第三个组中的三个( _视图_、_收藏夹_、_工具_),以及第五个组中的一个( _帮助_)。我们的扩展可以使用这些数字来计算标准菜单的位置,以及在哪里可以插入自己的顶级菜单项。

现在,幸运的是,Explorer 不是一个通用的 OLE 容器。它的标准菜单始终相同,并且有一些预定义的常量我们可以使用来访问标准菜单,并避免进行易出错的组宽度计算。它们在 _shlobj.h_ 中定义为 `FCIDM_*`,例如,`FCIDM_MENU_EDIT` 用于标准 _编辑_ 菜单的位置。我们的扩展使用 `FCIDM_MENU_HELP` 来定位标准 _帮助_ 菜单,并在 _帮助_ 之前插入上图所示的弹出菜单。

以下是设置共享菜单并插入“帮助”之前的弹出菜单的代码。

        if ( NULL != m_hMenu )
            {
            // Let the browser insert its standard items first.
            OLEMENUGROUPWIDTHS omw = { 0, 0, 0, 0, 0, 0 };
 
            m_spShellBrowser->InsertMenusSB ( m_hMenu, &omw );
            
            // Insert our SimpleExt menu before the Explorer Help menu.
            HMENU hmenuSimpleNS;
 
            hmenuSimpleNS = LoadMenu ( ... );
 
            if ( NULL != hmenuSimpleNS )
                {
                    InsertMenu ( m_hMenu, FCIDM_MENU_HELP, 
                             MF_BYCOMMAND | MF_POPUP,
                             (UINT_PTR) GetSubMenu ( hmenuSimpleNS, 0 ),
                             _T("&SimpleNSExt") );
                }

接下来,我们添加“关于”框项。我们首先使用 `GetMenuItemInfo()` 获取 _帮助_ 菜单的句柄,然后插入一个新菜单项。

            MENUITEMINFO mii = { sizeof(MENUITEMINFO), MIIM_SUBMENU };
 
            if ( GetMenuItemInfo ( m_hMenu, FCIDM_MENU_HELP, FALSE, &mii ))
                {
                InsertMenu ( mii.hSubMenu, -1, MF_BYPOSITION,
                             IDC_ABOUT_SIMPLENS, _T("About &SimpleNSExt") );
                }

我们最后要做的就是删除标准 _编辑_ 菜单,如果我们的视图窗口有焦点的话。在这种情况下,标准 _编辑_ 菜单是空的,所以没有必要保留它。

            if ( SVUIA_ACTIVATE_FOCUS == uState )
                {
                // The Edit menu created by Explorer
                // is empty, so we can nuke it.
                DeleteMenu ( m_hMenu, FCIDM_MENU_EDIT, MF_BYCOMMAND );
                }

最后,我们调用 `IShellBrowser::SetMenuSB()` 让 Explorer 使用菜单。然后我们保存新的 UI 状态并返回。

            // Set the new menu.
            m_spShellBrowser->SetMenuSB ( m_hMenu, NULL, m_hWnd );
            }
        }

    m_uUIState = uState;
}

`HandleDeactivate()` 简单得多。它调用 `SetMenuSB()` 和 `RemoveMenusSB()` 来从 Explorer 的框架中移除我们的菜单,然后销毁菜单。

void CShellViewImpl::HandleDeactivate()
{
    if ( SVUIA_DEACTIVATE != m_uUIState )
        {
        if ( NULL != m_hMenu )
            {
            m_spShellBrowser->SetMenuSB ( NULL, NULL, NULL );
            m_spShellBrowser->RemoveMenusSB ( m_hMenu );
            
            DestroyMenu ( m_hMenu ); // also destroys the SimpleNSExt submenu
            m_hMenu = NULL;
            }
 
        m_uUIState = SVUIA_DEACTIVATE;
        }
}

需要检查的一件重要事情是,您的菜单项 ID 必须落在 `FCIDM_SHVIEWFIRST` 和 `FCIDM_SHVIEWLAST`(在 _shlobj.h_ 中定义为 0 和 0x7FFF)之间,否则 Explorer 将无法正确地将消息路由到我们的扩展。

处理消息

我们的视图窗口处理几个标准消息和列表控件通知消息。它们是:

  • WM_CREATE:在首次创建视图窗口时发送。
  • WM_SIZE:在视图大小调整时发送。处理程序将列表控件的大小调整为匹配。
  • WM_SETFOCUS, NM_SETFOCUS:如前所述。
  • WM_CONTEXTMENU:处理列表控件中的右键单击,并在单击列表项时显示上下文菜单。
  • WM_INITMENUPOPUP:在首次单击菜单时发送,并在未选择驱动器时禁用 _浏览驱动器_ 项。
  • WM_MENUSELECT:在选择了新菜单项时发送,并在 Explorer 的状态栏中显示飞行提示帮助字符串。
  • WM_COMMAND:在选择了我们的菜单项时发送。
  • LVN_DELETEITEM:在删除列表项时发送。处理程序会删除与每个项关联的 PIDL。
  • HDN_ITEMCLICK:在单击列表标题时发送,并按该列对列表进行排序。

我将在本文中介绍一些更有趣的处理程序,即 `WM_MENUSELECT`、`HDN_ITEMCLICK` 和 `WM_COMMAND` 的处理程序。

WM_MENUSELECT

当选定的菜单项更改时,我们的窗口会收到 `WM_MENUSELECT`。我们的处理程序会验证选定的项是否与我们的菜单 ID 之一匹配,如果是,则在 Explorer 的状态栏中显示帮助字符串。

LRESULT CShellViewImpl::OnMenuSelect(UINT uMsg, WPARAM wParam, 
                                      LPARAM lParam, BOOL& bHandled)
{
WORD wMenuID = LOWORD(wParam);
WORD wFlags = HIWORD(wParam);
 
    // If the selected menu item is one of ours, show a flyby help string
    // in the Explorer status bar.
    if ( !(wFlags & MF_POPUP) )
        {
        switch ( wMenuID )
            {
            case IDC_EXPLORE_DRIVE:
            case IDC_SYS_PROPERTIES:
            case IDC_ABOUT_SIMPLENS:
                {
                CComBSTR bsHelpText;
 
                if ( bsHelpText.LoadString ( wMenuID ))
                    m_spShellBrowser->SetStatusTextSB ( bsHelpText.m_str );
 
                return 0;
                }
            break;
            }
        }
 
    // Otherwise, pass the message to the default handler.
    return DefWindowProc();
}

我们使用 `IShellBrowser::SetStatusTextSB()` 来更改状态栏文本。

HDN_ITEMCLICK

当用户单击列标题时发送 `HDN_ITEMCLICK`。我们首先检查当前排序的列。如果单击的是同一列,则 `m_bForwardSort` 会被切换以反转排序方向。否则,新的列被保存为当前的排序列。

LRESULT CShellViewImpl::OnHeaderItemclick ( int idCtrl, 
                                   LPNMHDR pnmh, BOOL& bHandled )
{
NMHEADER* pNMH = (NMHEADER*) pnmh;
int nClickedItem = pNMH->iItem;
 
    // Set the sorted column to the column that was just clicked.  If we're
    // already sorting on that column, reverse the sort order.
    if ( nClickedItem == m_nSortedColumn )
        m_bForwardSort = !m_bForwardSort;
    else
        m_bForwardSort = true;
 
    m_nSortedColumn = nClickedItem;

接下来,我们设置一个 `CListSortInfo` 数据包,其中包含指向视图所包含的 Shell Folder 的指针(这是知道如何排序 PIDL 的对象)、要排序的列和方向。然后我们调用列表控件的 `SortItems` 方法(这最终归结为一个 `LVM_SORTITEMS` 消息)。

    // Set up a CListSortInfo for the sort function to use.
const ESortedField aFields[] = 
    { SIMPNS_SORT_DRIVELETTER, SIMPNS_SORT_VOLUMENAME,
    SIMPNS_SORT_FREESPACE, SIMPNS_SORT_TOTALSPACE };
CListSortInfo sort = { m_psfContainingFolder, 
         aFields[m_nSortedColumn], m_bForwardSort };
 
    m_wndList.SortItems ( CompareItems, (LPARAM) &sort );
    return 0;
}

为了说明排序的工作原理,这里是 `CShellViewImpl::CompareItems()`:

int CALLBACK CShellViewImpl::CompareItems ( LPARAM l1, 
                                       LPARAM l2, LPARAM lData )
{
CListSortInfo* pSort = (CListSortInfo*) lData;
 
    return (int) pSort->pShellFolder->CompareIDs ( lData, 
                            (LPITEMIDLIST) l1, (LPITEMIDLIST) l2 );
}

这只是调用 `CShellFolderImpl::CompareIDs()`。参数是正在比较的两个项的 `LPARAM` 数据值(`l1` 和 `l2`),以及 `SortItems()` 的第二个参数(`lData`),即我们在 `OnHeaderItemclick()` 中设置的 `CListSortInfo` `struct`。

这是 `CompareIDs()`。它接受与 `CompareItems()` 相同的三个参数,只是顺序不同。返回值类似于 `strcmp()`(-1、0 或 1,表示 PIDL 的顺序)。我们首先使用 `CPidlMgr::GetData()` 从 PIDL 中检索两个驱动器字母。

STDMETHODIMP CShellFolderImpl::CompareIDs ( LPARAM lParam, 
                        LPCITEMIDLIST pidl1, LPCITEMIDLIST pidl2 )
{
TCHAR chDrive1 = m_PidlMgr.GetData ( pidl1 );
TCHAR chDrive2 = m_PidlMgr.GetData ( pidl2 );
CListSortInfo* pSortInfo = (CListSortInfo*) lParam;
HRESULT hrRet;

接下来,我们检查要排序的字段。我在这里将展示按驱动器字母排序。

    switch ( pSortInfo->nSortedField )
        {
        case SIMPNS_SORT_DRIVELETTER:
            {
            // Sort alphabetically by drive letter.
            if ( chDrive1 == chDrive2 )
                hrRet = 0;
            else if ( chDrive1 < chDrive2 )
                hrRet = -1;
            else
                hrRet = 1;
            }
        break;
        ...
        }

其他情况类似;它们只是获取不同的信息(卷名、可用空间等)并基于该信息设置 `hrRet`。最后一步是检查排序顺序,并在必要时反转它。

    // If the sort order is reversed (z->a or highest->lowest),
    // negate the return value.
    if ( !pSortInfo->bForwardSort )
        hrRet *= -1;
 
    return hrRet;
}
WM_COMMAND

CShellViewImpl 的消息映射中有针对我们每个菜单命令的 `COMMAND_ID_HANDLER` 条目:

BEGIN_MSG_MAP(CShellViewImpl)
    ...
    COMMAND_ID_HANDLER(IDC_SYS_PROPERTIES, OnSystemProperties)
    COMMAND_ID_HANDLER(IDC_EXPLORE_DRIVE, OnExploreDrive)
    COMMAND_ID_HANDLER(IDC_ABOUT_SIMPLENS, OnAbout)
END_MSG_MAP()

这是 `OnExploreDrive()` 的代码。我们首先获取选定的项,然后检索其 `LPARAM` 数据,即相应的 PIDL。

LRESULT CShellViewImpl::OnExploreDrive(WORD wNotifyCode, 
                       WORD wID, HWND hWndCtl, BOOL& bHandled)
{
LPCITEMIDLIST pidlSelected;
int           nSelItem;
TCHAR         chDrive;
TCHAR         szPath[] = _T("?:\\");
 
    nSelItem = m_wndList.GetNextItem ( -1, LVIS_SELECTED );
 
    pidlSelected = (LPCITEMIDLIST) m_wndList.GetItemData ( nSelItem );
    chDrive = m_PidlMgr.GetData ( pidlSelected );

然后我们在 `szPath` 中填入驱动器字母,并调用 `ShellExecute()` 来浏览该驱动器。

    *szPath = chDrive;
 
    ShellExecute ( NULL, _T("explore"), szPath, NULL, NULL, SW_SHOWNORMAL );
    return 0;
}

IOleCommandTarget

Explorer 与我们的扩展通信的另一种方式是 `IOleCommandTarget` 接口。它有两个方法:

  • QueryStatus():Explorer 调用此方法以确定我们的扩展支持哪些标准命令。
  • Exec():当用户在 Explorer 中执行我们必须处理的命令时调用。

关于命令、它们的作用,甚至它们的 ID,文档很少。我唯一能看到的有意义的命令是 _刷新_,当用户按下 F5 或单击 _视图_ 菜单上的 _刷新_ 时发送。在接下来的部分中,我将演示这两个处理 _刷新_ 命令的方法的最小实现。示例项目中的实际代码包含跟踪消息,以便您可以看到查询和发送了哪些命令。

QueryStatus()

`QueryStatus()` 的参数是命令组和一个或多个命令。如果 `QueryStatus()` 返回 `S_OK`,则表示我们的扩展支持这些命令,然后 Explorer 可以调用 `Exec()` 来让我们响应这些命令。在我测试期间,我看到使用了三组:`NULL`、`CGID_Explorer` 和 `CGID_ShellDocView`。_刷新_ 命令在 `NULL` 组中,ID 为 `OLECMDID_REFRESH`。我们的 `QueryStatus()` 只检查命令,如果找到 `OLECMDID_REFRESH`,则在 `OLECMD` `struct` 中设置标志并返回 `S_OK`。否则,它返回错误代码以指示我们不支持该命令。

STDMETHODIMP CShellViewImpl::QueryStatus ( const GUID* pguidCmdGroup, 
                       ULONG cCmds, OLECMD prgCmds[], OLECMDTEXT* pCmdText )
{
    if ( NULL == pguidCmdGroup )
        {
        for ( UINT u = 0; u < cCmds; u++ )
            {
            switch ( prgCmds[u].cmdID )
                {
                case OLECMDID_REFRESH:
                    prgCmds[u].cmdf = OLECMDF_SUPPORTED | OLECMDF_ENABLED;
                break;
                }
            }
 
        return S_OK;
        }
 
    return OLECMDERR_E_UNKNOWNGROUP;
}
Exec()

我们的 `Exec()` 方法再次检查 `NULL` 命令组和 _刷新_ 命令 ID,如果参数匹配这些值,则调用 `Refresh()` 来重新填充列表控件。

STDMETHODIMP CShellViewImpl::Exec ( const GUID* pguidCmdGroup, DWORD nCmdID,
                                    DWORD nCmdExecOpt, VARIANTARG* pvaIn,
                                    VARIANTARG* pvaOut )
{
HRESULT hrRet = OLECMDERR_E_UNKNOWNGROUP;
 
    if ( NULL == pguidCmdGroup )
        {
        if ( OLECMDID_REFRESH == nCmdID )
            {
            Refresh();
            hrRet = S_OK;
            }
        }
 
    return hrRet;
}

注册扩展

注册有两个部分:常规的 COM 服务器内容,以及告诉 Explorer 使用我们扩展的条目。GUID 键的默认值(GUID 是 `CShellViewImpl` 的 GUID,因为它是 Shell 直接实例化的 coclass)是要用于扩展项的文本。`InfoTip` 值包含当鼠标悬停在扩展项上时显示的信息提示的文本。`DefaultIcon` 键指定要用于该项的图标的位置。`Attributes` 值保存 `SFGAO_*` 标志的组合(在 _shlobj.h_ 中定义)。至少,它必须是 671088640(0x28000000),即 `SFGAO_FOLDER|SFGAO_BROWSABLE`。我们的扩展还包括 `SFGAO_CANRENAME|SFGAO_CANDELETE`,总计为 671088688(0x28000030)。添加这些标志允许用户通过 Explorer 上下文菜单或键盘重命名或删除命名空间项。(如果您不包括 `SFGAO_DELETE`,则用户必须手动编辑注册表才能删除扩展。)

HKCR
{
    NoRemove CLSID
    {
        ForceRemove {4145E10E-36DB-4F2C-9062-5DE1AF40BB31} = s 'Simple NSExt'
        {
            InprocServer32 = s '%MODULE%'
            {
                val ThreadingModel = s 'Apartment'
            }
            val InfoTip = 
                s 'A simple sample namespace extension from CodeProject'
            DefaultIcon = s '%MODULE%,0'
            ShellFolder
            {
                val Attributes = d '671088688'
            }
        }
    }
}

这是带有信息提示的命名空间扩展项:

 [Extension infotip - 3K]

RGS 文件的另一部分创建了一个 _junction point_(连接点),这是我们告诉 Explorer 使用扩展以及它在命名空间中出现的位置的方式。这类似于 Shell 扩展,它们为此目的使用 ShellEx 键。

HKLM
{
  NoRemove Software
  {
    NoRemove Microsoft
    {
      NoRemove Windows
      {
        NoRemove CurrentVersion
        {
          NoRemove Explorer
          {
            NoRemove Desktop
            {
              NoRemove NameSpace
              {
                ForceRemove {4145E10E-36DB-4F2C-9062-5DE1AF40BB31}
                {
                  val 'Removal Message' = 
                    s 'Your custom "Don''t delete me!" text goes here.'
                }
              }
            }
          }
        }
      }
    }
  }
}

您可以更改 _Desktop_ 键来更改命名空间扩展出现的位置; _My Computer_ 是一个常见的选择,它使扩展出现在与您的驱动器和控制面板相同的级别。GUID 仍然是 `CShellViewImpl` 的 GUID。`Removal Message` 字符串会在用户启用了删除确认并尝试删除扩展项时显示。

 [Delete confirmation msg - 12K]

结论

是的,编写命名空间扩展需要 _大量_ 工作!本文仅涵盖了基础知识。我已有未来文章的想法;第二部分将介绍如何创建一个带有子文件夹的扩展,以及如何处理 Explorer 树视图中的事件。

© . All rights reserved.