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






4.92/5 (176投票s)
2000年3月27日

2734631

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 包含了添加菜单项、提供动态帮助(出现在资源管理器状态栏中的文本)以及在用户选择其中一个 WinZip 命令时执行相应操作的代码。
WinZip 还包含一个拖放处理程序。这种类型与上下文菜单扩展非常相似,但它是在用户使用鼠标右键拖动文件时被调用的。以下是 WinZip 的拖放处理程序添加到上下文菜单中的内容:
还有许多其他类型(而且微软在每个 Windows 版本中都在不断增加!)。现在,我们只看上下文菜单扩展,因为它们编写起来非常简单,而且结果很容易看到(即时满足!)。
在我们开始编码之前,有一些技巧可以让工作变得更容易。当你使一个外壳扩展被资源管理器加载时,它会在内存中停留一段时间,这使得重新构建 DLL 变得不可能。要让资源管理器更频繁地卸载扩展,请创建这个注册表项:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Explorer\AlwaysUnloadDLL
并将其默认值设置为 "1"。在 9x 系统上,这是你能做的最好的了。在 NT 系统上,转到这个键:
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer
并创建一个名为 DesktopProcess 的 DWORD
值,其值为 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
是我们应该开始添加项目的起始位置。uidFirstCmd
和 uidLastCmd
是我们可以用于菜单项的命令 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
是另一组标志,我稍后会描述。我们可以忽略 pwReserved
。pszName
是一个指向外壳拥有的缓冲区的指针,我们将把要显示的帮助字符串存储在这里。cchMax
是缓冲区的大小。返回值是通常的 HRESULT 常量之一,例如 S_OK
或 E_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
结构体里有很多信息,但对我们来说,我们只关心 lpVerb
和 hwnd
。lpVerb
有双重作用——它既可以是所调用动词的名称,也可以是一个索引,告诉我们点击了哪个菜单项。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(TRUEFALSE); }
DllUnregisterServer()
需要一个类似的更改:
STDAPI DllUnregisterServer() { //... return _Module.UnregisterServer(TRUEFALSE); }
注册外壳扩展
现在我们已经实现了所有的 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 关闭资源管理器。当你完成调试后,你可以从命令提示符运行资源管理器来正常重启外壳。
这一切看起来是怎样的?
这是我们添加项目后的上下文菜单的样子:
我们的菜单项在那里!
这是资源管理器状态栏显示我们的动态帮助的样子:
这是消息框的样子,显示了所选文件的名称:
版权和许可
本文为版权材料,©2003-2006 by Michael Dunn。我意识到这并不能阻止人们在网上到处复制它,但我还是必须这么说。如果你有兴趣翻译本文,请给我发电子邮件告知。我预计不会拒绝任何人翻译的许可,我只是想知道翻译的情况,以便我可以在这里发布一个链接。
本文附带的演示代码已发布到公共领域。我以这种方式发布它,以便代码能让每个人受益。(我没有将文章本身设为公共领域,因为文章只在 CodeProject 上可用,这有助于提高我自己的知名度和 CodeProject 网站。)如果你在自己的应用程序中使用了演示代码,如果能发邮件告诉我,我将不胜感激(只是为了满足我的好奇心,想知道人们是否从我的代码中受益),但这不是必需的。在你自己的源代码中注明出处也受赞赏,但不是必需的。
修订历史
2000年3月27日:文章首次发表。
2006年3月14日:更新以涵盖 VC 7.1 中的变化。从项目中移除了与 OLE Automation 相关的代码,以简化内容。
系列导航: » 第二部分