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

编写外壳扩展的 Complete Idiot's Guide - 第 I 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (176投票s)

2000年3月27日

viewsIcon

2734631

downloadIcon

15532

编写外壳扩展的分步教程

目录

README.TXT

在继续阅读或在本文讨论区发帖之前,我希望你先阅读这部分内容。

这一系列文章最初是为 VC 6 用户编写的。现在 VC 8 已经发布,我觉得是时候更新文章以涵盖 VC 7.1 了。;)(而且,VC 7.1 自动完成的从 6 到 7 的转换并不总是顺利,所以 VC 7.1 用户在尝试使用演示源代码时可能会遇到困难。)因此,在我更新这个系列的过程中,文章将会更新以反映新的 VC 7.1 特性,并且我会在源代码下载中提供 VC 7.1 的项目。

VC 2005 用户重要提示:VC 2005 的 Express 版本包含 ATL 或 MFC。由于本系列文章使用 ATL,有些还使用 MFC,所以你将无法使用 Express 版本来运行文章的示例代码。

如果你正在使用 VC 6,你应该获取一个更新的 Platform SDK。你可以使用网页安装版本,或者下载 CAB 文件ISO 镜像并在本地运行安装程序。请务必使用工具将 SDK 的 include 和 lib 目录添加到 VC 的搜索路径中。你可以在 Platform SDK 程序组的 Visual Studio Registration 文件夹中找到这个工具。即使你使用的是 VC 7 或 8,获取最新的 Platform SDK 也是一个好主意,这样你就能拥有最新的头文件和库。

VC 7 用户重要提示:如果你没有更新你的 Platform SDK,你必须更改默认的包含路径。请确保 $(VCInstallDir)PlatformSDK\include 在列表的最前面,位于 ($VCInstallDir)include 之上,如下所示:

由于我还没有使用过 VC 8,我不知道示例代码是否能在 8 上编译。希望 7 到 8 的升级过程会比 6 到 7 的过程更顺利。如果你在使用 VC 8 时遇到任何问题,请在本文的论坛上发帖。

系列介绍

外壳扩展是一个 COM 对象,它为 Windows 外壳(资源管理器)添加某种功能。市面上有很多种扩展,但关于它们是什么的易于理解的文档却很少。(不过我打赌,自我最初写下这句话以来的六年里,情况已经有所改善!)我强烈推荐 Dino Esposito 的巨著 《Visual C++ Windows Shell Programming》 (ISBN 1861001843),如果你想深入了解外壳的许多方面。但对于那些没有这本书,或者只关心外壳扩展的人,我写了这篇教程,它会让你大吃一惊、叹为观止,或者至少,让你很好地走上理解如何编写自己的扩展的道路。本指南假设你熟悉 COM 和 ATL 的基础知识。如果你需要复习 COM 基础,请查看我的COM 简介文章。

第一部分包含对外壳扩展的总体介绍,以及一个简单的上下文菜单扩展,以激发你对后续部分的兴趣。

“外壳扩展”这个术语包含两个部分。“外壳”指的是资源管理器,“扩展”指的是你编写的代码,当预定事件发生时(例如,右键单击 .DOC 文件),这段代码会被资源管理器运行。因此,外壳扩展是一个为资源管理器添加功能的 COM 对象

外壳扩展是一个进程内服务器,它实现了一些接口来处理与资源管理器的通信。ATL 是快速启动并运行扩展的最简单方法,因为没有它,你将不得不一遍又一遍地编写 QueryInterface()AddRef() 代码。在基于 Windows NT 的操作系统上调试扩展也容易得多,我稍后会解释原因。

外壳扩展有很多类型,每种类型在不同事件发生时被调用。以下是一些较常见的类型,以及它们被调用的情况:

类型

何时调用

它的作用

上下文菜单处理程序

用户右键单击文件或文件夹。在外壳版本 4.71+ 中,在目录窗口的背景上右键单击时也会调用。

向上下文菜单添加项目。

属性表处理程序

显示文件的属性对话框。

向属性表添加页面。

拖放处理程序

用户右键拖动项目并将其放置到目录窗口或桌面上。

向上下文菜单添加项目。

放置处理程序

用户拖动项目并将其放置到文件上。

任何期望的操作。

QueryInfo 处理程序(外壳版本 4.71+)

用户将鼠标悬停在文件或其他外壳对象(如“我的电脑”)上。

返回一个字符串,资源管理器会将其显示在工具提示中。

第一部分介绍

到目前为止,你可能想知道扩展在资源管理器中是什么样子。一个例子是 WinZip - 它包含多种类型的扩展,其中之一就是上下文菜单处理程序。以下是 WinZip 为压缩文件添加到上下文菜单的一些命令:

 [WinZip menu items - 4K]

WinZip 包含了添加菜单项、提供动态帮助(出现在资源管理器状态栏中的文本)以及在用户选择其中一个 WinZip 命令时执行相应操作的代码。

WinZip 还包含一个拖放处理程序。这种类型与上下文菜单扩展非常相似,但它是在用户使用鼠标右键拖动文件时被调用的。以下是 WinZip 的拖放处理程序添加到上下文菜单中的内容:

 [WinZip menu items - 2K]

还有许多其他类型(而且微软在每个 Windows 版本中都在不断增加!)。现在,我们只看上下文菜单扩展,因为它们编写起来非常简单,而且结果很容易看到(即时满足!)。

在我们开始编码之前,有一些技巧可以让工作变得更容易。当你使一个外壳扩展被资源管理器加载时,它会在内存中停留一段时间,这使得重新构建 DLL 变得不可能。要让资源管理器更频繁地卸载扩展,请创建这个注册表项:

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Explorer\AlwaysUnloadDLL

并将其默认值设置为 "1"。在 9x 系统上,这是你能做的最好的了。在 NT 系统上,转到这个键:

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer

并创建一个名为 DesktopProcessDWORD 值,其值为 1。这使得桌面和任务栏在一个进程中运行,而后续的资源管理器窗口则各自在自己的进程中运行。这意味着你可以用一个单独的资源管理器窗口进行调试,当你关闭它时,你的 DLL 会被自动卸载,从而避免任何文件被占用的问题。你需要注销并重新登录才能使这些更改生效。

稍后我将解释如何在 9x 系统上进行调试。

使用 AppWizard 开始

让我们从简单的开始,制作一个只弹出消息框来显示它正在工作的扩展。我们将把扩展挂钩到 .TXT 文件上,这样当用户右键单击文本文件时,我们的扩展就会被调用。

好了,是时候开始了!什么?我还没告诉你如何使用那些神秘的外壳扩展接口?别担心,我会边做边解释。我发现如果先解释概念,然后立即给出示例代码,会更容易理解。我也可以先解释所有内容,然后再写代码,但我发现那样更难吸收。不管怎样,启动 VC,我们开始吧。

运行 AppWizard 并创建一个新的 ATL COM 程序。我们称之为“SimpleExt”。保留 AppWizard 中的所有默认设置,然后点击完成。现在我们有了一个空的 ATL 项目,它将构建一个 DLL,但我们还需要添加我们的外壳扩展的 COM 对象。在 ClassView 树中,右键单击 SimpleExt classes 项,然后选择 New ATL Object。(在 VC 7 中,右键单击该项并选择 Add|Add Class。)

在 ATL 对象向导中,第一个面板已经选择了 Simple Object,所以只需点击下一步。在第二个面板中,在 Short Name 编辑框中输入 "SimpleShlExt"(面板上的其他编辑框将自动填充):

默认情况下,向导会创建一个可以通过 OLE Automation 从 C 和基于脚本的客户端使用的 COM 对象。我们的扩展将只由资源管理器使用,所以我们可以更改一些设置来移除 Automation 功能。转到 Attributes 页面,将 Interface 类型更改为 Custom,并将 Aggregation 设置更改为 No

当你点击确定时,向导会创建一个名为 CSimpleShlExt 的类,其中包含实现 COM 对象的基本代码,并将这个类添加到项目中。我们将把我们的代码添加到这个类中。

初始化接口

当我们的外壳扩展被加载时,资源管理器会调用我们的 QueryInterface() 函数来获取一个指向 IShellExtInit 接口的指针。这个接口只有一个方法,Initialize(),其原型是:

HRESULT IShellExtInit::Initialize (
  LPCITEMIDLIST pidlFolder,
  LPDATAOBJECT pDataObj,
  HKEY hProgID )

资源管理器使用此方法向我们提供各种信息。pidlFolder 是包含正在操作的文件的文件夹的 PIDL。(PIDL [pointer to an ID list] 是一个数据结构,它唯一地标识外壳中的任何对象,无论它是否是文件系统对象。)pDataObj 是一个 IDataObject 接口指针,我们通过它来检索正在操作的文件的名称。hProgID 是一个打开的 HKEY,我们可以用它来访问包含我们 DLL 注册数据的注册表项。对于这个简单的扩展,我们只需要使用 pDataObj 参数。

要将此添加到我们的 COM 对象中,打开 SimpleShlExt.h 文件,并添加下面以粗体列出的行。向导生成的一些与 COM 相关的代码是不需要的,因为我们没有实现自己的接口,所以我用删除线标出了可以移除的代码:

#include <shlobj.h>
#include <comdef.h>
 
class ATL_NO_VTABLE CSimpleShlExt :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CSimpleShlExt, &CLSID_SimpleShlExt>,
  public ISimpleShlExt,
  public IShellExtInit
{
  BEGIN_COM_MAP(CSimpleShlExt)
    COM_INTERFACE_ENTRY(ISimpleShlExt)
    COM_INTERFACE_ENTRY(IShellExtInit)
  END_COM_MAP()

COM_MAP 是 ATL 实现 QueryInterface() 的方式。它告诉 ATL 其他程序可以从我们的 COM 对象中检索哪些接口。

在类声明内部,添加 Initialize() 的原型。我们还需要一个缓冲区来存放文件名:

protected:
  TCHAR m_szFile[MAX_PATH];
 
public:
  // IShellExtInit
  STDMETHODIMP Initialize(LPCITEMIDLIST, LPDATAOBJECT, HKEY);

然后,在 SimpleShlExt.cpp 文件中,添加函数的定义:

STDMETHODIMP CSimpleShlExt::Initialize ( 
  LPCITEMIDLIST pidlFolder,
  LPDATAOBJECT pDataObj,
  HKEY hProgID )

我们要做的是获取被右键单击的文件的名称,并在一个消息框中显示该名称。如果有多个选定的文件,你可以通过 pDataObj 接口指针访问它们所有,但为了保持简单,我们只看第一个文件名。

文件名以与将文件拖放到具有 WS_EX_ACCEPTFILES 样式的窗口时使用的格式相同。这意味着我们使用相同的 API 来获取文件名:DragQueryFile()。我们将在函数开始时获取 IDataObject 中包含的数据的句柄:

HRESULT CSimpleShlExt::Initialize(...)
{
FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT,
                  -1, TYMED_HGLOBAL };
STGMEDIUM stg = { TYMED_HGLOBAL };
HDROP     hDrop;
 
  // Look for CF_HDROP data in the data object. If there
  // is no such data, return an error back to Explorer.
  if ( FAILED( pDataObj->GetData ( &fmt, &stg ) ))
    return E_INVALIDARG;
 
  // Get a pointer to the actual data.
  hDrop = (HDROP) GlobalLock ( stg.hGlobal );
 
  // Make sure it worked.
  if ( NULL == hDrop )
    return E_INVALIDARG;

请注意,对所有内容进行错误检查至关重要,尤其是指针。由于我们的扩展在资源管理器的进程空间中运行,如果我们的代码崩溃,我们也会使资源管理器崩溃。在 9x 系统上,这样的崩溃可能需要重新启动计算机。

现在我们有了 HDROP 句柄,我们可以获取我们需要的文件名。

  // Sanity check – make sure there is at least one filename.
UINT uNumFiles = DragQueryFile ( hDrop, 0xFFFFFFFF, NULL, 0 );
HRESULT hr = S_OK;
  
  if ( 0 == uNumFiles )
    {
    GlobalUnlock ( stg.hGlobal );
    ReleaseStgMedium ( &stg );
    return E_INVALIDARG;
    }
 
  // Get the name of the first file and store it in our
  // member variable m_szFile.
  if ( 0 == DragQueryFile ( hDrop, 0, m_szFile, MAX_PATH ) )
    hr = E_INVALIDARG;
 
  GlobalUnlock ( stg.hGlobal );
  ReleaseStgMedium ( &stg );
 
  return hr;
}

如果我们返回 E_INVALIDARG,资源管理器将不会再次为此次右键单击事件调用我们的扩展。如果我们返回 S_OK,那么资源管理器将再次调用 QueryInterface() 并获取另一个接口的指针:IContextMenu

与上下文菜单交互的接口

一旦资源管理器初始化了我们的扩展,它将调用 IContextMenu 的方法,让我们添加菜单项、提供动态帮助,并执行用户的选择。

向我们的外壳扩展添加 IContextMenu 的方法与添加 IShellExtInit 类似。打开 SimpleShlExt.h 并添加此处以粗体列出的行:

class ATL_NO_VTABLE CSimpleShlExt : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CSimpleShlExt, &CLSID_SimpleShlExt>,
    public IShellExtInit,
    public IContextMenu
{
  BEGIN_COM_MAP(CSimpleShlExt)
    COM_INTERFACE_ENTRY(IShellExtInit)
    COM_INTERFACE_ENTRY(IContextMenu)
  END_COM_MAP()

然后添加 IContextMenu 方法的原型:

public:
  // IContextMenu
  STDMETHODIMP GetCommandString(UINT, UINT, UINT*, LPSTR, UINT);
  STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO);
  STDMETHODIMP QueryContextMenu(HMENU, UINT, UINT, UINT, UINT);

修改上下文菜单

IContextMenu 有三个方法。第一个方法 QueryContextMenu() 让我们修改菜单。QueryContextMenu() 的原型是:

HRESULT IContextMenu::QueryContextMenu (
  HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd,
  UINT uidLastCmd, UINT uFlags );

hmenu 是上下文菜单的句柄。uMenuIndex 是我们应该开始添加项目的起始位置。uidFirstCmduidLastCmd 是我们可以用于菜单项的命令 ID 值的范围。uFlags 指示资源管理器调用 QueryContextMenu() 的原因,我稍后会讲到这一点。

关于返回值的文档,不同的人有不同的说法。Dino Esposito 的书说它是 QueryContextMenu() 添加的菜单项数量。VC 6 的 MSDN 文档说它我们添加的最后一个菜单项的命令 ID 加 1。在线 MSDN 是这样说的:

如果成功,返回一个 HRESULT 值,其严重性值设置为 SEVERITY_SUCCESS,其代码值设置为分配的最大命令标识符的偏移量加一。例如,假设 idCmdFirst 设置为 5,你向菜单添加了三个命令标识符为 5、7 和 8 的项目。返回值应为 MAKE_HRESULT(SEVERITY_SUCCESS, 0, 8 - 5 + 1)。否则,它返回一个 OLE 错误值。

到目前为止,在我写的代码中,我一直遵循 Dino 的解释,并且效果很好。实际上,他的返回值制作方法与在线 MSDN 的方法是等价的,只要你从 uidFirstCmd 开始编号你的菜单项,并为每个项目递增 1。

我们简单的扩展只有一个项目,所以 QueryContextMenu() 函数非常简单:

HRESULT CSimpleShlExt::QueryContextMenu (
  HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd,
  UINT uidLastCmd, UINT uFlags )
{
  // If the flags include CMF_DEFAULTONLY then we shouldn't do anything.
  if ( uFlags & CMF_DEFAULTONLY )
    return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );
 
  InsertMenu ( hmenu, uMenuIndex, MF_BYPOSITION,
               uidFirstCmd, _T("SimpleShlExt Test Item") );
 
  return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 1 );
}

我们做的第一件事是检查 uFlags。你可以在 MSDN 中查找完整的标志列表,但对于上下文菜单扩展,只有一个值是重要的:CMF_DEFAULTONLY。这个标志告诉命名空间扩展只添加默认菜单项。当这个标志存在时,外壳扩展不应添加任何项目。这就是为什么如果 CMF_DEFAULTONLY 标志存在,我们会立即返回 0。如果该标志不存在,我们就修改菜单(使用 hmenu 句柄),并返回 1 来告诉外壳我们添加了 1 个菜单项。

在状态栏显示动态帮助

下一个可以被调用的 IContextMenu 方法是 GetCommandString()。如果用户在资源管理器窗口中右键单击一个文本文件,或者选择一个文本文件然后单击文件菜单,当我们的菜单项被高亮显示时,状态栏将显示动态帮助。我们的 GetCommandString() 函数将返回我们希望资源管理器显示的字符串。

GetCommandString() 的原型是:

HRESULT IContextMenu::GetCommandString (
  UINT idCmd, UINT uFlags, UINT* pwReserved,
  LPSTR pszName, UINT cchMax );

idCmd 是一个从零开始的计数器,指示哪个菜单项被选中。由于我们只有一个菜单项,idCmd 将永远是零。但如果我们添加了,比如说,3个菜单项,idCmd 可能是 0、1 或 2。uFlags 是另一组标志,我稍后会描述。我们可以忽略 pwReservedpszName 是一个指向外壳拥有的缓冲区的指针,我们将把要显示的帮助字符串存储在这里。cchMax 是缓冲区的大小。返回值是通常的 HRESULT 常量之一,例如 S_OKE_FAIL

GetCommandString() 也可以被调用来为一个菜单项检索一个“动词”。动词是一个与语言无关的字符串,用于标识可以对文件执行的操作。ShellExecute() 的文档有更多相关内容,动词这个主题最适合在另一篇文章中讨论,但简而言之,动词可以是在注册表中列出的(例如 "open" 和 "print"),也可以是由上下文菜单扩展动态创建的。这使得在 shell 扩展中实现的操作可以通过调用 ShellExecute() 来调用。

不管怎样,我提到这些的原因是,我们必须确定 GetCommandString() 被调用的原因。如果资源管理器想要一个动态帮助字符串,我们就提供它。如果资源管理器请求一个动词,我们就忽略这个请求。这就是 uFlags 参数发挥作用的地方。如果 uFlags 设置了 GCS_HELPTEXT 位,那么资源管理器正在请求动态帮助。此外,如果设置了 GCS_UNICODE 位,我们必须返回一个 Unicode 字符串。

我们的 GetCommandString() 的代码如下:

#include <atlconv.h>  // for ATL string conversion macros
 
HRESULT CSimpleShlExt::GetCommandString (
  UINT idCmd, UINT uFlags, UINT* pwReserved,
  LPSTR pszName, UINT cchMax )
{
USES_CONVERSION;
 
  // Check idCmd, it must be 0 since we have only one menu item.
  if ( 0 != idCmd )
    return E_INVALIDARG;
 
  // If Explorer is asking for a help string, copy our string into the
  // supplied buffer.
  if ( uFlags & GCS_HELPTEXT )
    {
    LPCTSTR szText = _T("This is the simple shell extension's help");
 
    if ( uFlags & GCS_UNICODE )
      {
      // We need to cast pszName to a Unicode string, and then use the
      // Unicode string copy API.
      lstrcpynW ( (LPWSTR) pszName, T2CW(szText), cchMax );
      }
    else
      {
      // Use the ANSI string copy API to return the help string.
      lstrcpynA ( pszName, T2CA(szText), cchMax );
      }
 
    return S_OK;
    }
 
  return E_INVALIDARG;
}

这里没什么特别的;我只是硬编码了字符串,并将其转换为适当的字符集。如果你从未使用过 ATL 转换宏,请查看Nish 和我关于字符串包装类的文章;当需要向 COM 方法和 OLE 函数传递 Unicode 字符串时,它们让生活变得容易得多。

一个重要的事情要注意的是,lstrcpyn() API 保证目标字符串将以 null 结尾。这与 CRT 函数 strncpy() 不同,后者在源字符串长度大于或等于 cchMax 时不会添加终止 null。我建议总是使用 lstrcpyn(),这样你就不必在每次调用 strncpy() 后插入检查以确保字符串以 null 结尾。

执行用户的选择

IContextMenu中的最后一个方法是InvokeCommand()。如果用户点击了我们添加的菜单项,这个方法就会被调用。InvokeCommand()的原型是:

HRESULT IContextMenu::InvokeCommand (
  LPCMINVOKECOMMANDINFO pCmdInfo );

CMINVOKECOMMANDINFO 结构体里有很多信息,但对我们来说,我们只关心 lpVerbhwndlpVerb 有双重作用——它既可以是所调用动词的名称,也可以是一个索引,告诉我们点击了哪个菜单项。hwnd 是用户调用我们扩展的资源管理器窗口的句柄;我们可以使用这个窗口作为我们显示的任何UI的父窗口。

由于我们只有一个菜单项,我们将检查 lpVerb,如果它是零,我们就知道我们的菜单项被点击了。我能想到的最简单的事情就是弹出一个消息框,所以这段代码就是这么做的。消息框显示所选文件的文件名,以证明它确实在工作。

HRESULT CSimpleShlExt::InvokeCommand (
  LPCMINVOKECOMMANDINFO pCmdInfo )
{
  // If lpVerb really points to a string, ignore this function call and bail out.
  if ( 0 != HIWORD( pCmdInfo->lpVerb ) )
    return E_INVALIDARG;
 
  // Get the command index - the only valid one is 0.
  switch ( LOWORD( pCmdInfo->lpVerb ) )
    {
    case 0:
      {
      TCHAR szMsg[MAX_PATH + 32];
 
      wsprintf ( szMsg, _T("The selected file was:\n\n%s"), m_szFile );
 
      MessageBox ( pCmdInfo->hwnd, szMsg, _T("SimpleShlExt"),
                   MB_ICONINFORMATION );
 
      return S_OK;
      }
    break;
 
    default:
      return E_INVALIDARG;
    break;
    }
}

其他代码细节

我们还可以对向导生成的代码进行一些调整,以移除我们不需要的 OLE Automation 功能。首先,我们可以从 SimpleShlExt.rgs 文件中移除一些注册表项(这个文件的作用将在下一节解释):

HKCR
{
  SimpleExt.SimpleShlExt.1 = s 'SimpleShlExt Class'
  {
    CLSID = s '{5E2121EE-0300-11D4-8D3B-444553540000}'
  }
  SimpleExt.SimpleShlExt = s 'SimpleShlExt Class'
  {
    CLSID = s '{5E2121EE-0300-11D4-8D3B-444553540000}'
    CurVer = s 'SimpleExt.SimpleShlExt.1'
  }
  NoRemove CLSID
  {
    ForceRemove {5E2121EE-0300-11D4-8D3B-444553540000} = s 'SimpleShlExt Class'
    {
      ProgID = s 'SimpleExt.SimpleShlExt.1'
      VersionIndependentProgID = s 'SimpleExt.SimpleShlExt'
      InprocServer32 = s '%MODULE%'
      {
        val ThreadingModel = s 'Apartment'
      }
      'TypeLib' = s '{73738B1C-A43E-47F9-98F0-A07032F2C558}'
    }
  }
}

我们还可以从 DLL 的资源中移除类型库。点击视图|资源包含。在编译时指令框中,你会看到一行包含类型库的代码:

移除那一行,然后在 VC 警告修改包含文件时点击确定。

在 VC 7 中,该命令位于不同的位置。在资源视图选项卡上,右键单击 SimpleExt.rc 文件夹,然后在菜单上选择资源包含

现在我们已经移除了类型库,我们需要更改两行代码,告诉 ATL 它不应该对类型库做任何事情。打开 SimpleExt.cpp,转到 DllRegisterServer() 函数,并将 RegisterServer() 的参数更改为 FALSE

STDAPI DllRegisterServer()
{
//...
  return _Module.RegisterServer(TRUE FALSE);
}

DllUnregisterServer() 需要一个类似的更改:

STDAPI DllUnregisterServer()
{
//...
  return _Module.UnregisterServer(TRUE FALSE);
}

注册外壳扩展

现在我们已经实现了所有的 COM 接口。但是……我们如何让资源管理器使用我们的扩展呢?ATL 会自动生成代码来将我们的 DLL 注册为 COM 服务器,但这只是让其他应用程序可以使用我们的 DLL。为了告诉资源管理器我们的扩展存在,我们需要在保存有关文本文件信息的键下注册它:

HKEY_CLASSES_ROOT\txtfile

在该键下,一个名为 ShellEx 的键保存了一个将在文本文件上调用的外壳扩展列表。在 ShellEx 下,ContextMenuHandlers 键保存了一个上下文菜单扩展的列表。每个扩展都在 ContextMenuHandlers 下创建一个子键,并将该键的默认值设置为其 GUID。因此,对于我们的简单扩展,我们将创建这个键:

HKEY_CLASSES_ROOT\txtfile\ShellEx\ContextMenuHandlers\SimpleShlExt

并将其默认值设置为我们的 GUID:"{5E2121EE-0300-11D4-8D3B-444553540000}"。

不过,你不需要编写任何代码来完成这个操作。如果你查看 FileView 选项卡上的文件列表,你会看到 SimpleShlExt.rgs。这是一个由 ATL 解析的文本文件,它告诉 ATL 在服务器注册时要添加哪些注册表项,以及在服务器注销时要删除哪些注册表项。以下是我们指定要添加的注册表项,以便资源管理器了解我们的扩展的方式:

HKCR
{
  NoRemove txtfile
  {
    NoRemove ShellEx
    {
      NoRemove ContextMenuHandlers
      {
        ForceRemove SimpleShlExt = s '{5E2121EE-0300-11D4-8D3B-444553540000}'
      }
    }
  }
}

每一行都是一个注册表键名,其中“HKCR”是 HKEY_CLASSES_ROOT 的缩写。NoRemove 关键字意味着当服务器注销时,不应删除该键。最后一行有另一个关键字 ForceRemove,这意味着如果该键存在,它将在写入新键之前被删除。该行的其余部分指定了一个字符串(这就是“s”的意思),该字符串将存储在 SimpleShlExt 键的默认值中。

我需要在这里发表一点个人看法。我们注册扩展的键是 HKEY_CLASSES_ROOT\txtfile。然而,“txtfile”这个名字并不是一个永久的或预先确定的名字。如果你查看 HKEY_CLASSES_ROOT\.txt那个键的默认值才是存储这个名字的地方。这有几个副作用:

  • 我们不能可靠地使用 RGS 脚本,因为“txtfile”可能不是正确的键名。
  • 可能安装了其他文本编辑器,它将自己与 .TXT 文件关联起来。如果它更改了 HKEY_CLASSES_ROOT\.txt 键的默认值,所有现有的外壳扩展都将停止工作。

这对我来说确实像一个设计缺陷。我想微软也是这么认为的,因为最近创建的扩展,比如 QueryInfo 扩展,是注册在以文件扩展名命名的键下的。

好了,个人看法结束。还有一个最后的注册细节。在 NT 上,建议将我们的扩展放在一个“已批准”的扩展列表中。可以设置一个系统策略来阻止未在批准列表中的扩展被加载。该列表存储在:

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved

在这个键中,我们创建一个字符串值,其名称是我们的 GUID。字符串的内容可以是任何东西。执行此操作的代码放在我们的 DllRegisterServer()DllUnregisterServer() 函数中。我不会在这里展示代码,因为它只是简单的注册表访问,但你可以在文章的示例项目中找到代码。

调试外壳扩展

最终,你会编写一个不那么简单的扩展,并且你必须调试它。打开你的项目设置,在 Debug 选项卡上,在“Executable for debug session”编辑框中输入资源管理器的完整路径,例如“C:\windows\explorer.exe”。如果你正在使用 NT,并且你已经设置了前面描述的 DesktopProcess 注册表项,当你按 F5 开始调试时,会打开一个新的资源管理器窗口。只要你所有的工作都在那个窗口中完成,你以后重新构建 DLL 就不会有任何问题,因为当你关闭那个窗口时,你的扩展就会被卸载。

在 Windows 9x 上,你将不得不在运行调试器之前关闭外壳。点击开始,然后点击关闭。按住 Ctrl+Alt+Shift 并点击取消。这将关闭资源管理器,你会看到任务栏消失。然后你可以回到 MSVC 并按 F5 开始调试。要停止调试会话,请按 Shift+F5 关闭资源管理器。当你完成调试后,你可以从命令提示符运行资源管理器来正常重启外壳。

这一切看起来是怎样的?

这是我们添加项目后的上下文菜单的样子:

 [SimpleShlExt menu item - 3K]

我们的菜单项在那里!

这是资源管理器状态栏显示我们的动态帮助的样子:

 [SimpleShlExt help text - 3K]

这是消息框的样子,显示了所选文件的名称:

 [SimpleShlExt msg box - 9K]

版权和许可

本文为版权材料,©2003-2006 by Michael Dunn。我意识到这并不能阻止人们在网上到处复制它,但我还是必须这么说。如果你有兴趣翻译本文,请给我发电子邮件告知。我预计不会拒绝任何人翻译的许可,我只是想知道翻译的情况,以便我可以在这里发布一个链接。

本文附带的演示代码已发布到公共领域。我以这种方式发布它,以便代码能让每个人受益。(我没有将文章本身设为公共领域,因为文章只在 CodeProject 上可用,这有助于提高我自己的知名度和 CodeProject 网站。)如果你在自己的应用程序中使用了演示代码,如果能发邮件告诉我,我将不胜感激(只是为了满足我的好奇心,想知道人们是否从我的代码中受益),但这不是必需的。在你自己的源代码中注明出处也受赞赏,但不是必需的。

修订历史

2000年3月27日:文章首次发表。
2006年3月14日:更新以涵盖 VC 7.1 中的变化。从项目中移除了与 OLE Automation 相关的代码,以简化内容。

系列导航: » 第二部分

© . All rights reserved.