危险漫步博客
新鲜的“黑客思维”就是从全新的角度看待黑客技术,从更高的层面去思考;专注于黑客精神及技术交流分享的独立博客。
文章2289 浏览18787734

Windows shell 扩展编程教程详解

所谓的Shell扩展就是能够添加某种功能到Windows Shell的COM对象。Windows里有着各种各样的扩展,但关于Shell扩展的原理以及如何编写Shell扩展的文档却很少。如果你想深入地了解Shell各方面的细节,危险漫步特别推荐Dino Esposito的著作《Visual C++ Windows Shell Programming》。不过这些是英文的书籍,当然你可以参照词典进行阅读。

笔者仅对Shell扩展进行了概括性的代码阐述,并给出了一个上下文菜单的扩展实例让读者理解这个过程,由于shell编程需要涉及到com的知识,所以如果读者对shell编程感兴趣的话建议自学一下com的相关知识。

那Shell扩展到底是什么东西?

Shell扩展从字面上分两个部分,Shell与Extensiono Shell指Windows Explorer,而Extension则指由你编写的当某一预先约定好的事件(如在以.doc为后缀的文件图标上单击右键)发生时由Explorer调用执行的代码。因此一个Shell移展就是AI为Explorer添加功能的COM对象。Shell扩展是个进程内服务器(运行在Explorer进程内),它实现了一些接口来处理与Explorer的通信。ATL在编程人员看来是设计Shell扩展最简单最快捷的方法,如果没有它,你就不得不一遍又一遍地编写繁琐的Querylnterface0及AddRef()代码.另外,在WindowsNT和2000调试Shell扩展相对比较容易一些。

Shell扩展有很多种类型,每种类型都在各自不同的事件发生时被调用运行,但也有一些扩展的类型和调用情形是非常相似的。

下面开始介绍相关的知识:

Context menu扩展处理器,用户右键单击文件或文件夹对象时,或在一个文件夹窗口中的背景处单击右键时(添加菜单项到上下文菜单中。

Property sheet扩展处理器,要显示一个文件对象的属性框时添加定制属性页到属性表中。Drag and drop扩展处理器,用户用右键拖放文件对象到文件夹窗口或桌面时添加菜单项到上下文菜单中。

Drop扩展处理器,用户拖动Shell对象并将它放到一个文件对象上时任何想要的操作。

Querylnfo扩展处理器,用户将鼠标盘旋于文件或其他Shell对象的图标上时,返回一个浏览器用于显示在提示框中的字符串。

这里你可以简单的籽其理解为“额外添加的右键菜单”。

Shell扩展的类型很多,而且微软也正不断地在每一新版本的Windows中加入更多的扩展类型。由于菜单扩展的编程方法比较简单,也容易理解,所以笔者这里就结婚扫扩展菜单的编程方法。

在我们编写代码之前,先说一下一些简化编码及调试工作的技巧。当shell扩展被Explorer凋用后,它会在内存中呆上一段时间,这会使你无法重新编译并生成Shell扩展DLL文件。

要让Explorer更迅速地卸载Shell扩展执行文件,需要创建如下注册表项:H KCU\Software\Microsoft/Windows、CurrentVersion\Explorer并创建一个名为DesktopProcess的DWORD值1.这会使桌面和任务栏运行在同一个进程中,而其他每一个Explorer窗口都运行在它自己的每一个进程内。也就是说,你可以在单个的Explorer窗口内进行调试,而后只要你关闭该窗口,你的DLL就会被马上D载,这就避免了因为DLL正被Windows使角而无法替换更新,而如果不幸出现这种情况,你就不得不注销登录后再重新登录进Windows从而强制卸载使用中的Shell扩展DLL。

开始编写上下文菜单一它该做些什么?

基本的知识有了,下面危险漫步就可以开始编写代码了。首先我们先从简单的开始,只弹出一个对话框以表明当前的扩展能够正常地工作。我们将这个shell扩展关联到,TXT文件,因此当用户右键单击文本文件对象时扩展就会被调用。

我们采用的平台依然是VC6.0。

运行AppWizard,生成一个名为SimpleExt的ATL COM工程,保留所有默认的设置选项,点击”完成”。现在我们已经有了一个空的.ATL工程,它可以编译并生成一个DLL,但我们还需要添加Shell扩展的COM对象。

在ClassView中,右击SimpleExt classes条目,选择New ATL Object。在ATL Object Wizard里,第一页默认已经选择了Simple Object,所以单击Next即可,在第二页中,在Short Name文本框里输入SimpleShlExr,点击OK.(其余的文本框会自动填充完)这样就创建了一个名为CSimpleShlExt的类,其包含了实现COM对象最基本的代码,我们将在这个类中加入我们自己的代码。

基本的工作已经完成了,下面我们就进行下一步操作:初始化接口。当我们的shell扩展被加载时,Explorer将调用我们所实现的COM对象的Querylnterface()函数以取得一个IShellExtlnit接口指针。

Explorer使用该方法传递给我们各种各样的信息,PidlF older是用户所选择操作的文件所在的文件夹的PIDL变量。(一个PIDL[指向ID列表的指针)是一个数据结构,它唯一地标识了在Shell命名空间的任何对象,一个Shell命名空间中的对象可以是也可以不是真实的文件系统中的对象。)

pDataObj是一个IDataObject接口指针,通过它我们可以获取用户所选择操作的文件名。bProgID是一个HKEY注册表键变量,可以用它获取我们的DLL的注册数据。

在这个简单的扩展例子中,我们将只使用到pDataObj参数。

COM—MAP是ATL实现Querylnterface()机制的宏,它包含的列表告诉ATL其它外部程序用Q1lerylnterface()能从我们的COM对象获取哪些接口。接着,在类声明里,加入Initialize()的函数原型。 另外我们需要一个变量来保存文件名:我们要做的是取得被右击选择的文件名,再把该文件名显示在弹出消息框中。可能会有多个文件同时被选择右击,你可以用pDataObj接口指针获取所有的文件名,但现在为简单起见,我们只获取第一个文件名。文件名的存放格式与你拖放文件到带WS_EX__ ACCEPTFILES风格的窗口时使用的文件名格式是一样的。这就是说我们可以使用同样的API来获取文件名:DragQueryFile()。

请注意错误检查,特别是指针的检查。由于我们的扩展运行在Explorer进程内,要是我们的代码崩溃了,Explorer也会随之崩溃。现在我们有了一个HDROP句柄,我们就可以获取我们需要的文件名了。

要是我们返回ElNVALIDARG,Explorer将不会继续调用以后的扩展代码。要是返回S—OK,Explorer将再一次调用Querylnterface()获取另一个我们下面就要添加的接口指针:IContextMenu。

下面开始按几个步骤进行我们的代码。

A:与上下文菜单交互的接口

一旦Explorer初始化了扩展,它就会接着调用IContextMenu的方法让我们添加菜单项,提

供状态栏上的提示,并响应执行用户的选择。添加IContextMenu接口到Shell扩展类似于上面IsheIIExtlnit接口的添加,打开SimpleShlExt.h。

修改上下文菜单IContextMenu有三个方法,第一个是QueryContextMenu()它让我们可以修改上下文菜单。

修改上下文菜单IContextMenu有三个方法,第一个是QueryContextMenu0,它让我们可以修改上下文菜单。

hmenu上下文菜单句柄。

uMenulndex是我们应该添加菜单项的起始位置。

uidFirstCmd和uidLastCmd是我们可以使用的菜单命令ID值的范围。

uFlags标识了Explorer调用QueryContextMenu()的原因。

首先我们检查uFlags,你可以在MSDN中找到所有标志的解释,但对于上下文菜单扩展而言,只有一个值是重要的:CMF DEFAULTONLY该标志告诉Shell命名空间扩展保留默认的菜单项,这时我们的Shell扩展就不应该加入任何定制的菜单项,这也是为什么此时我们要返回O的原因。如果该标志没有被设置,我们就可以修改菜单了(使用hmenu句柄),并返回1告诉Shell我们添加了一个菜单项。

B:在状态栏上显示提示帮助

下一个要被调用的IContextMenu方法是GetCommandString()。如果用户是在浏览器窗口中右击文本文件,或选中一个文本文件后单击文件菜单时,状态栏会显示提示帮助。我们的

GetCommandString()函数将返回一个帮助字符串供浏览器显示。

idCmd是一个以O为基数的计数器,标识了哪个菜单项被选择。

因为我们只有一个菜单项,所以idCmd总是0.但如果我们添加了3个菜单项,idCmd可能是0,1,或2。

uFlags是另一组标志。

PwReserved可以被忽略。

pszName指向一个由Shell拥有的缓冲区,我们将把帮助字符串拷贝进该缓冲区。

cchMax是该缓冲区的大小。

返回值是S_OK或E_FAIL。

GetCommandString()也可以被调用以获取菜单项的动作("verb")。

verb是个语言无关性字符串,它标识一个可以加于文件对象的操作。

ShellExecute()的文档中有详细的解释,而有关verb的内容足以再写一篇文章,简单的解释是:verb可以直接列在注册表中(如“open”和“print”等字符串),也可以由上下文菜单扩展创建。这样就可以通过调用ShellExecute0执行实现在Shell扩展中的代码。如果Explorer要求一个帮助字符串,我们就提供给它;如果Explorer要求一个verb,我们就忽略它;这就是uFlags参数的作用。如果uFlags设置了GCS HELPTEXT位,则ExpLorer是在要求帮助字符串,而且如果GCS_UNICODE被设置,我们就必须返回一个UIlicode字符串。

这里没有什么特别的代码;代码中使用了硬编码的字符串并把它转换为相应的字符集。笔者在上面的代码中使用了T2CW和T2CA将TCHAR字符串分别转化为Unicode和ANSI字符串。函数开头处的USES_CONVERSION宏其实声明了一个将被转化宏使用的局部变量。要注意的一个问题是:lstrcpyn()保证了目标字符串将以null为结束符。这与C运行时(CRT)函数stmcpy()不同,当要拷贝的源字符串的长度大于或等于cchMax时stmcpy0不会添加一个null结束符。建议使用lstrcpyn(),这样你就不必在每一个stncpy()后加入检查保证字符串以null为结束符的代码。

C:执行用户的选择

IC ont ex tMenu接口的最后一个方法是InvokeCom_mand()。当用户点击我们添加的菜单项时

CMINVOKECOMMANDINFO结构带有大量的信息,但我们只关心lpVerb和hwnd这两个成员。

lpVerb参数有两个作用一它或是可被激发的verb(动作)名,或是被点击的菜单项的索引值。

hwnd是用户激活我们的菜单扩展时所在的浏览器窗口的句柄。

因为我们只有一个扩展的菜单项,我们只要检查lpVerb参数,如果其值为0,我们可以认定我们的菜单项被点击了。

我能想到的最简单的代码就是弹出一个信息框,这里的代码也就做了这么多。信息框显示所选的文件的文件名以证实代码正确地工作。

D:注册Shell扩展

现在我们已经实现了所有需要的COM接口,可是我们怎样才能让explorer使用我们的扩展昵?ATL自动生成注册COM DLL服务器的代码,但这只是让其它程序可以使用我们的DLL。为了告诉explorer使用我们的扩展,我们需要在文本文件类型的注册表键下注册扩展:

HKEY_CLASSES- ROOT\txtfile在这个键下,有个名为ShellEx的键保存了一个与文本文件关联的Shell扩展的列表。在SheIIEx键下,ContextMenuHandlers键保存了上下文菜单扩展的列表。每个扩展都在ContextMenuHandlers下创建了一个子键并将其默认值设为扩展COM的GUID。

并将其默认值设为我们的GUID:”{5E2121EE-0300-11D4_8D3B-444553540000,”。

我们不必写任何代码就可以完成注册操作,如果你看一下Fileview页的文件列表,你会看到SimpleShlExt.rgs。

该文本文件将被ATL分析,并指导ATL在该COM服务器注册时添加附加的注册键,而注销时又该删除哪些键。

每一行代表一个注册表键,”HKCR”是HKEY_CLASSES_ ROOT的缩写,NoRemove关键字表示当该COM服务器注销时该键不用被删除。最后一行有些复杂,ForceRemove关键字表示如果该键已存在,那么在新键添加之前该键先应被删除。这行脚本的余下部分指定一个字符串,它将被存为SimpleShIExt键的默认值。

E:调试Shell扩展

打开你的工程设置,在Debug页”Executable for debug session”编辑框中输入浏览器程序的全路径,如:”c:\windows\explorer.exe”。只要你在这个窗口内完成你所有的工作,当你关闭该窗口时扩展同时会被卸出内存,这样就不会防碍我们重建DLL了。

相关推荐