Part I: A step-by-step tutorial on writing shell extensions
第一节:Windows shell扩展初步:上下文菜单扩展
作者:Michael Dunn
译者:yesaidu
目录
● README
● 系列绪言
● 第一部分绪言
● 从AppWizard开始
● 初始化接口
● 上下文菜单交互接口
○ 更改上下文菜单
○ 在状态栏显示拉线式(fly-by)帮助
○ 执行用户选择
○ 其他代码细节
● 注册Shell扩展
● 调试Shell扩展
● 所有的外观
● 版权与许可
● 修订历史
README
我想,你在行动之前,或者你在本手册的讨论板发帖之前应该阅读这份材料。
本手册最初是用VC 6编写的。现在,VC8都出来了,我感觉是时候对本手册进行升级到VC7.1了。(通过VC7.1自动升级VC6项目,并不一定会完全地完成代码转换;因此,VC7.1用户可能碰到这样的现象,即在转换、编译示例代码后,运行时可能没有效果或出错。)只要我仔细检查并更新本手册,本手册将体现VC7.1的新特点。我将会提供VC7.1项目的源码下载。
VC2005用户要注意了:VC2005体验版(Express edition)没有一同发布ATL或MFC。既然本手册用到了ATL,有时还使用了MFC,因此,你不能用VC2005体验版来编译示例代码。
如果你正使用VC6,那么,你应该设法取得最新的平台SDK。你可以使用WEB安装版(web install version),或者下载CAB文件或者ISO镜像包,安装它们到本地。确认把SDK的INCLUDE和LIB目录添加到了VC的搜索路径中。你能在PSDK程序组中找到Visual Studio Registration目录。这是一个好主意,无论你使用VC7,还是用VC8,你都能取得最新的PSDK头文件和库文件。
VC7用户注意了:如果你没有更新PSDK,必须改变默认的INCLUDE路径。确信“VC++目录”-“包含文件”列表的第一项是$(VCInstallDir)PlatformSDK\include
,它在($VCInstallDir)include
前面,如下图:
由于一直没有使用过VC 8,因此我不确定示例代码在VC 8上是否可以通过编译。只是希望,把VC7项目升级到VC8的自动转换功能比从VC6到VC7的要好些。如果你使用VC8编译示例时遇到了任何疑惑,请在讨论板发帖。
手册绪言
所谓shell扩展就是能增加某些功能到Windows资源管理器的COM对象。Shell扩展有很多内容,但关于它们的文档资料却非常少见。(自从我最先发表这份手册的六年来,我相信情况要好多了。)如果你想深入Windows shell的内部,极力推荐Dino Esposito的巨作Visual C++ Windows Shell Programming (ISBN 1861001843)。对于没有这本书的人,或者仅仅对shell 扩展感兴趣的朋友,我将给你一个惊喜:一本有关shell 扩展编程的傻瓜手册。即使本手册并未让你感到惊喜,那么,对你理解如何编写shell扩展也会提供很好的帮助。本手册假定你理解并掌握了COM和ATL的基本原理和应用。如果你还需要学习COM基本原理,请参考Intro to COM。
第一节介绍了shell扩展的概要,并提供了一个上下文菜单扩展的示例,使你对后面的章节充满兴趣。
从字面上看,shell扩展包括两个方面:shell和扩展。所谓shell,就是资源管理器Explorer;而扩展就是指在预定的事件发生时由Explorer调用执行的代码(比如,在.DOC文件上右击)。因此,shell扩展就是为Explorer增添功能的COM对象。
shell扩展是一个进程内服务器,它实现了跟Explorer通信的接口。ATL是设计一个shell扩展,并使之运行的最简单办法;这样你就不用为一遍又一遍的编写QueryInterface()
和AddRef()
而大伤脑筋。在Windows NT下调试shell扩展要更容易些,这点,我在后面还会谈到。
Shell扩展有很多种类型,每一类型都有其被调用的时机:即每种类型在不同的事件发生时被调用执行。下表列出了一些较常见的类型,以及它们被调用的情况:
类型 | 被调用的时机 | 它可以做什么 |
Context menu扩展处理器 | 用户在文件对象或文件夹对象或目录窗口背景(需要shell v 4.71+以上)单击右键 | 在上下文菜单中添加菜单项 |
Property sheet扩展处理器 | 文件属性对话框显示时 | 在属性对话框中定制属性页 |
Drag and drop扩展处理器 | 用户用右键拖放文件到文件夹窗口或桌面时 | 在上下文菜单中添加菜单项 |
Drop handler扩展处理器 | 用户拖对象并将其放到文件上时 | 任何你想做的 |
QueryInfo 扩展处理器 | 用户在文件、“我的电脑”等其他shell对象的图标上悬停时 | 返回一个Explorer显示在工具提示中的字符串 |
第一节绪言
现在,你可能有很多的疑问:为什么扩展看起来像Explorer?它到底是什么样的?一个例子就是WinZip(或者WinRAR,我没安装WinZip ^_^――译者)——它包含了多种shell扩展,其中之一就是上下文扩展。下图是WinZip(其实是WinRA的 ^_^――译者)为压缩文件在上下文菜单中添加的菜单项:
WinZip编写了增加菜单项的代码,提供了Explorer状态栏上的菜单项帮助提示(fly-by help),并在用户选择一个WinZip菜单命令时执行相应的操作。
WinZip还提供了拖曳扩展处理,此类型跟上下文菜单扩展非常相似,但它是在用户通过右键拖曳文件时才被触发。下图是WinZip(也是WinRA的 ^_^――译者)拖曳文件弹出的菜单项:
还有很多的shell扩展类型,Microsoft不断向每一个新的Windows版本中增加更多的类型。现在,让我们把注意力放到上下文菜单扩展上,因为它易于编写,效果也很明显(能够立即让你满意)。
在动手编码之前,有一些便于编码和调试的小技巧:当Explorer调用shell扩展(由用户触发)后,shell扩展暂时驻于内存中;此时,你无法重新编译此扩展的DLL文件。为让Explorer更迅速卸载扩展,可以在注册表中创建下面的键:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Explorer\AlwaysUnloadDLL
并设置其默认值为“1”。在Win9x平台上,这是最好的办法。在WinNT上,可以在下面的键
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer
创建一个DWORD 值DesktopProcess
,也设置它的值为1。(译者:如下图,Win9x系统太少见了)这使得“桌面”和“任务栏”运行于一个进程,其他的Explorer窗口运行在其独立进程。这意味着,你可以调试单个Explorer窗口,当你关闭该窗口时,相关的扩展DLL就会被自动卸载,这样就避免了DLL文件正被Windows使用而无法替换的问题。要使注册表修改生效,需要注销后重新登录。
稍后,我将说明Win9x下如何进行调试。
使用AppWizard开始
我们先做一个简单的扩展,它仅仅弹出一个消息框以表明工作正常。我们把它关联到文本文件,这样,当我们在一个文本文件上右击时,该扩展就会被调用。
好了,让我们开始吧!什么?我还没有告诉你如何使用那些神秘的shell扩展接口?别着急,我会边进行边解释。我觉得,给出一个概念,紧跟着一个示例代码,这样做有助于理解。当然,我也可以先解释所有的概念,然后列出示例代码,不过这样很难吸引注意力。不管怎样,开启你的VC,我们要开始了。
运行AppWizard,生成一个名为“SimpleExt”的ATL COM工程:
去掉属性化,保留其它默认选项,点击“完成”。
现在,我们有了一个空的ATL项目,它可以编译生成一个DLL,但我们还需要添加shell扩展COM对象。在类视图中,右击“SimpleExt”项,选择“新建ATL 对象” (VC7,选择“添加”→“添加类”,下图。本文的环境是Windows XP+VC7.1,因此附图都是VC7的)。
在ATL对象向导中,第一页已经选择了“简单对象”,点击“下一步”。
在第二页,在“简称”编辑框中输入“SimpleShlExt”(其他编辑框会自动完成):
在默认情况下,向导将创建以C和脚本客户端为基础的OLE自动化兼容的COM对象。我们的扩展仅仅由Explorer调用,因此我们去掉自动化支持。在“属性”页,选择“接口”类型为“自定义”,并且选择“聚合”为“否”:
点击“完成”,就创建了CSimpleShlExt
类,它包含了实现COM对象的最基本代码。我们将向这个类加入代码。
初始化接口
当我们的shell扩展被加载时,Explorer将调用QueryInterface()
函数,以取得IShellExtInit
接口指针。该接口仅有一个方法Initialize()
,其函数原型如下:
HRESULT IShellExtInit::Initialize (
LPCITEMIDLIST pidlFolder,
LPDATAOBJECT pDataObj,
HKEY hProgID )
Explorer通过该方法向我们传递各种各样的信息。pidlFolder
是用户所操作文件所在的文件夹的PIDL(PIDL [pointer to an ID list],指向ID列表的指针,是一个数据结构,它唯一标识了在shell空间的任何对象,这个对象是或者不是文件系统的对象。) pDataObj
是一个IDataObj
接口变量,通过它可以取得用户正操作的文件名。hProgID
是一个HKEY
接口变量,通过它可以取得扩展DLL的注册信息。在本例中,仅仅需要pDataObj
参数。
要添加这一方法到我们的COM对象,打开文件SimpleShlExt,加入下列粗体的行。AppWizard生成了一些不必需的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中,添加Initialize
的实现代码:
STDMETHODIMP CSimpleShlExt::Initialize (
LPCITEMIDLIST pidlFolder,
LPDATAOBJECT pDataObj,
HKEY hProgID)
我们要做的是取得右键单击选中的文件名,并把它显示在消息框中。如果选中了多个文件,可以通过pDataObj
接口指针来访问它们,不过为了保持例子的简单,我们只获取第一个文件名。
文件名的格式和拖曳文件到WS_EX_ACCEPTFILES
风格的窗口是的文件名格式一致,这样说来,我们可以通过同样的API:DragQueryFile
来取得文件名。我们先取得包含在IDataObject
中的数据句柄:
void CSimpleShlExt::Initialize (LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hProgID)
{
FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM stg = { TYMED_HGLOBAL };
HDROP hDrop;
// 在数据对象内查找CF_HDROP类型数据。
// 如果没有数据,返回一个错误(“无效参数”)给Explorer。
if ( FAILED( pDataObj->GetData ( &fmt, &stg ) ))
return E_INVALIDARG;
// 取得指向实际数据的指针。
hDrop = (HDROP) GlobalLock ( stg.hGlobal );
// 确保非NULL
if ( NULL == hDrop )
return E_INVALIDARG;
注意,错误检查是极其重要的,尤其是对指针的检查。因为我们的扩展运行于Explorer进程空间,如果我们的程序挂了,Explorer会跟着挂。在Win9x系统上,这样的崩溃可能导致需要重启系统。
现在,我们有了HDROP
句柄,可以取得所需的文件名了:
// 有效性检查,至少有一个文件名
UINT uNumFiles = DragQueryFile ( hDrop, 0xFFFFFFFF, NULL, 0 );
HRESULT hr = S_OK;
if ( 0 == uNumFiles )
{
GlobalUnlock ( stg.hGlobal );
ReleaseStgMedium ( &stg );
return E_INVALIDARG;
}
// 取得第一个文件名,保存到 m_szFile
if ( 0 == DragQueryFile ( hDrop, 0, m_szFile, MAX_PATH ) )
hr = E_INVALIDARG;
GlobalUnlock ( stg.hGlobal );
ReleaseStgMedium ( &stg );
return hr;
}
如果返回E_INVALIDARG
,Explorer不会在右键事件时再调用我们的扩展。如果返回S_OK
,Explorer将再次调用QueryInterface()
,以取得另一接口:IContextMenu
。
与上下文菜单交互的接口
一旦Explorer初始化了我们的扩展,它将调用IContextMenu
方法来增加菜单项、提供状态栏帮助(fly-by help),以及响应用户的选择。
添加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()
修改上下文菜单。它的原型如下:
HRESULT IContextMenu::QueryContextMenu (
HMENU hmenu,
UINT uMenuIndex,
UINT uidFirstCmd,
UINT uidLastCmd,
UINT uFlags );
hmenu
是上下文菜单句柄。uMenuIndex
是我们要添加菜单项的开始位置。uidFirstCmd
和uidLastCmd
是菜单命令ID值范围。uFlags
表明Explorer调用QueryContextMenu()
的缘由,这个后面还会谈到。
关于此方法的返回值,你翻阅不同的文档,可能得到不同的答案。Dino Esposito在他的书中认为这个返回值是所添加的菜单项的数目。但VC6的MSDN却说它是最后一个菜单项的命令ID加1;然而,联机MSDN却说:
如果函数成功,返回的HRESULT值就是分配的菜单项命令ID的最大差值加1。例如,uidFirstCmd
是5,你添加了3个菜单项,它们的命令ID分别是5、7、8。那么,返回值应该是MAKE_HRESULT(SEVERITY_SUCCESS, 0, 8 - 5 + 1)。否则,返回一个OLE错误。
一直以来,我都按照Dino的解释来编写代码,这些代码工作地很好。实际上,他的解释与联机MSDN是一致的,只要将uidFirstCmd
作为第一项菜单项ID,后续的菜单项依次累加1。
我们这里的扩展简单的加入一个菜单项,因此QueryContextMenu()
函数非常简单:
STDMETHODIMP CSimpleShlExt::QueryContextMenu (
HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd,
UINT uidLastCmd, UINT uFlags )
{
// 如果标识包含了 CMF_DEFAULTONLY,那么,我们啥都不做
if ( uFlags & CMF_DEFAULTONLY )
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );
InsertMenu ( hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, _T("简单SHELL扩展测试") );
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 1 );
}
首先,我们检查uFlags的值。在MSDN内,你能找到所有的标识和它们的解释,但对于上下文菜单扩展而言,仅仅一个值是有意思的:即CMF_DEFAULTONLY。该标识告诉shell命名空间扩展保留默认的菜单项;(如果设置了它的话,)shell扩展将不增加任何的菜单项。这也是我们为什么返回0的原因。如果未设置它,我们就可以通过句柄hmenu来修改菜单,并返回1告诉shell增加了一个菜单项。
在状态栏显示提示帮助(fly-by help)
下一个要被调用的IContextMenu
方法是GetCommandString()
。当用户在Explorer窗口中右击文本文件,或者选中文本文件后点击“文件”菜单,鼠标指到我们添加的菜单项时,状态栏将显示提示信息。GetCommandString()
函数返回一个字符串供Explorer显示。
GetCommandString()
原型如下:
HRESULT IContextMenu::GetCommandString (
UINT idCmd, UINT uFlags, UINT* pwReserved,
LPSTR pszName, UINT cchMax );
idCmd是基于0的计数器,它表明了被选中的菜单项。由于我们只有一个菜单项,所以idCmd总为0。不过,如果我们添加了,比如说,3个菜单项,idCmd就是0、1、2。uFlags是另外的一组标识,这个留待后面再讨论。pwReserved可以被忽略。pszName是shell所有的缓冲区,用于显示的帮助信息将拷贝到它中。cchMax是上述缓冲区的尺寸。返回值是HRESULT常量,比如说S_OK或E_FAIL。
GetCommandString()
也能用来取得菜单项的“动作”(verb)。动作是与语言无关的串,它标识了能作用于文件对象的动作。关于这点,ShellExecute()
的文档中有更详细的说明;有关动作的内容最好留待另外的文章(可以就这方面的内容另外一篇文章),这里简要的说,是列在注册表中的动作(比如“打开”和“打印”),或者有上下文菜单扩展动态创建的动作。这可以通过ShellExecute()
来调用shell扩展的动作。
总之,我7li8li的说了这么多,就是为了解释清楚GetCommandString()
的作用。如果Explorer要提示信息,我们就给它;如果Explorer请求一个动作,就忽视它。这就是uFlags的作用。如果uFlags设置了GCS_HELPTEXT
位,Explorer请求提示信息。另外,如果uFlags设置了GCS_UNICODE
,我们必须给它一个UNICODE串。
本例中GetCommandString()
如下:
#include <atlconv.h> // ATL串转换宏
STDMETHODIMP CSimpleShlExt::GetCommandString (
UINT idCmd, UINT uFlags, UINT* pwReserved, LPSTR pszName, UINT cchMax )
{
USES_CONVERSION;
// 由于这里只有一个菜单项,所以idCmd 必须为0
if ( 0 != idCmd )
return E_INVALIDARG;
// 如果Explorer请求提示信息,拷贝串到提供的缓冲区
if ( uFlags & GCS_HELPTEXT )
{
LPCTSTR szText = _T("简单的SHEEL扩展帮助(fly-by help)");
if ( uFlags & GCS_UNICODE )
{
// 这里,需要把 pszName 转换为 UNICODE
lstrcpynW ( (LPWSTR) pszName, T2CW(szText), cchMax );
}
else
{
// ANSI版本
lstrcpynA ( pszName, T2CA(szText), cchMax );
}
return S_OK;
}
return E_INVALIDARG;
}
这没什么奇怪的;我使用了硬编码并将它转换为合适的字符集。如果你从没用过ATL转换宏,你最好去学一下。它们在你向COM方法和OLE函数传递参数时,十分有用。
一个需要注意的重要事项是,lstrcpyn()
函数保证字符串是以NULL结束的。这和CRT(C运行时)函数strncpy()
不同,后者在源串的长度大于等于cchMax时并不在串最后插入NULL。我建议总是使用lstrcpyn()
,这样就无需在调用strncpy()
后总是检查以确保串是否以NULL结束。
执行用户的选择
IContextMenu
接口最后的方法是InvokeCommand()
。此方法在用户点击我们增加的菜单项后被调用,其函数原型如下:
HRESULT IContextMenu::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo );
结构CMINVOKECOMMANDINFO有9个成员,就我们的目的而言,仅仅需要关注lpVerb
和hwnds
。lpVerb
有两个用途:它既可以是被引发的动作名,也可以是被点击的菜单向索引。hwnds
是用户引发我们的扩展时所在的Explorer窗口句柄;我们可以将其作为我们用来显示信息的窗口的父窗口。
由于我们只有一个菜单项,所以只需要检查lpVerb
:如果它为0,那么我们的菜单项就被选中了。最简单的事情是弹出消息框,这里的代码也就能干这事儿。消息框显示了被选中的文件名,这表明代码工作正常。
STDMETHODIMP CSimpleShlExt::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo )
{
// 如果 lpVerb 指向一个实际串,忽略此次调用并退出
if ( 0 != HIWORD( pCmdInfo->lpVerb ) )
return E_INVALIDARG;
// 取得命令索引,这里,唯一有效的值为0
switch ( LOWORD( pCmdInfo->lpVerb) )
{
case 0:
{
TCHAR szMsg [MAX_PATH + 32];
wsprintf ( szMsg, _T("被选中的文件:\n\n%s"), m_szFile );
MessageBox ( pCmdInfo->hwnd, szMsg, _T("SimpleShlExt"),
MB_ICONINFORMATION );
return S_OK;
}
break;
default:
return E_INVALIDARG;
break;
}
}
其它代码细节
这里,集中说明如何移除AppWizard生成的多余的OLE自动化特性方面的代码。首先,可以移除SimpleShlExt.rgs(这个文件的用途在下一节详述)中些注册表入口:
HKCR
{
SimpleExt.SimpleShlExt.1 = s 'SimpleShlExt Class'
{
CLSID = s '{1CE1EBEB-1254-4880-B807-809CC31E8D2C}'
}
SimpleExt.SimpleShlExt = s 'SimpleShlExt Class'
{
CLSID = s '{1CE1EBEB-1254-4880-B807-809CC31E8D2C}'
CurVer = s 'SimpleExt.SimpleShlExt.1'
}
NoRemove CLSID
{
ForceRemove {1CE1EBEB-1254-4880-B807-809CC31E8D2C} = s 'SimpleShlExt Class'
{
ProgID = s 'SimpleExt.SimpleShlExt.1'
VersionIndependentProgID = s 'SimpleExt.SimpleShlExt'
InprocServer32 = s '%MODULE%'
{
val ThreadingModel = s 'Apartment'
}
val AppID = s '%APPID%'
'TypeLib' = s '{172391D4-B01E-4EF5-AC3E-34C99889D8B0}'
}
}
}
我们也能移除DLL资源中的类型库。(VC7)在“资源视图”中,选中“SimpleExt.rc”,右击,选中“资源包括”:
移除类型库后,我们还需要修改两处代码,以告诉ATL,它不应通过类型库来处理。在文件SimpleExt.cpp中的DllRegisterServer()
/DllUnregisterServer()
函数,设置RegisterServer()
/UnregisterServer()
的参数为。
STDAPI DllRegisterServer (void)
{
// ...
return _Module.RegisterServer(TRUE FALSE);
}
STDAPI DllUnregisterServer (void)
{
// ...
return _Module.UnregisterServer(TRUE FALSE);
}
注册Shell扩展
现在,我们实现所有的COM接口。不过,怎么才能让Explorer使用我们的扩展呢?ATL自动生成注册COMD服务器DLL 的代码,但那是给其它程序使用。为了让Explorer知道扩展存在,需要在文本文件的下述注册表键下注册我们的扩展:
HKEY_CLASSES_ROOT\txtfile
在这个注册表键下,名为ShellEx
的键保存了有关文本文件的shell扩展列表;在它的下一级,名为ContextMenuHandlers
的键保存了上下文菜单扩展列表。每个扩展都拥有一个ContextMenuHandlers
的子键,其默认值为shell扩展的GUID。本文简单扩展的示例,将创建如下子键:
HKEY_CLASSES_ROOT\txtfile\ShellEx\ContextMenuHandlers\SimpleShlExt
并设置其默认值为扩展COM的GUID,如“{1CE1EBEB-1254-4880-B807-809CC31E8D2C }”。
不必编写代码来完成COM的注册。在“解决方案管理器”中有文件SimpleShlExt.rgs;这是一个文本文件,它被ATL解析,指导ATL在该服务器注册时添加哪些键,在卸载时删除哪些键。下面是注册该扩展所要添加的注册表键:
HKCR
{
NoRemove txtfile
{
NoRemove ShellEx
{
NoRemove ContextMenuHandlers
{
ForceRemove SimpleShlExt = s '{1CE1EBEB-1254-4880-B807-809CC31E8D2C}'
}
}
}
}
每一行都代表一个注册键名。“HKCR”是HKEY_CLASSES_ROOT
的缩写。关键字NoRemove
表明这个键在服务器卸载时不用删除。关键字ForceRemove
表明在写入新键之前,如果该键存在,那么就要先删除它;这行剩下的部分指定了一个字符串(这就是“s”的意思),它是SimpleShlExt
的默认值。
这里,我插入几句。我们的扩展注册在HKEY_CLASSES_ROOT\txtfile
下;然而,“txtfile
”并不是持久的或者预先定好的名字。如果你查看一下HKEY_CLASSES_ROOT\.txt
,它的默认值就是“txtfile
”。这样,有两个副作用:
1、 既然“txtfile
”可能不是正确的键名,因此RGS脚本并不可靠。
2、 某些文件编辑软件可能在安装到系统时,就把它自身关联到文本文件。如果它改变了HKEY_CLASSES_ROOT\.txt
的默认值,那么所有的shell扩展就不能用了。
在我看来,这确实是一个设计上的漏洞。Microsoft可能也这么看,因为最新的扩展,如QueryInfo扩展就注册在HKEY_CLASSES_ROOT\.txt
下。
好了,到此为止。还有一个注册的细节,即在WinNT上,为了让非管理员帐号也能使用我们的扩展,得把它放到“approved ”扩展列表中:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved
在这个键下,创建以扩展的GUID为名的串值,其内容任意。代码在DllRegisterServer()
和DllUnregisterServer()
中,都是一些简单的注册表访问,我就不罗列了。你可以在示例代码中找到。
调试Shell扩展
总有一天,你将写一个不是这么简单的shell扩展;那时,你不得不调试它。打开项目的属性对话框,在“调试”的“命令”编辑框输入“C:\windows\explorer.exe”。如果是WinNT系统,设置DesktopProcess
键(前述),当你按F5时就启动了一个新的Explorer窗口。只要是在这个窗口完成所有的工作,那么在关闭这个窗口时,扩展就会被卸载,这样就不影响后面的重建DLL。
在Win9x上,在运行调试器前,必须关闭shell:点击“开始”,点击“注销”。按下Ctlr+Alt+Shift,并点“取消”。这将关闭Explorer,任务栏(桌面)消失了。切换到MSVC,按F5开始调试。要中止调试,按下“Shift+F5”关闭Explorer。完成调试后,可以从“开始”“运行”“Explorer.exe”,让其正常启动。
扩展的外观
版权与许可
作者:Michael Dunn
译者:Yesaidu