第一节 一步步教你如何编写Shell扩展 | |||||||||||||||||||||||||||||||||||||||||||||||||||
所谓的Shell扩展就是能够添加某种功能到Windows Shell的COM对象。 Windows里有着各种各样的扩展,但关于Shell扩展的原理以及如何编写Shell扩展的文档却很少。 如果你想深入地了解Shell各方面的细节,我特别推荐Dino Esposito的著作《Visual C++ Windows Shell Programming》。 但对于那些没有这本书的,或只对Shell扩展本身感兴趣的朋友,我写了这个编程指南希望能够帮助你理解怎样编写Shell扩展。 该指南假设你理解COM和ATL的基本原理及应用。 第一节对Shell扩展进行了概括性的介绍, 并给出了一个上下文菜单的扩展以引起你对以后各章的兴趣. 但Shell扩展到底是什么玩意呢? "Shell扩展"从字面上分两个部分,Shell与Extension。 Shell指Windows Explorer, 而Extension则指由你编写的当某一预先约定好的事件(如在以.doc为后缀的文件图标上单击右键)发生时由Explorer调用执行的代码。 因此一个Shell扩展就是一个为Explorer添加功能的COM对象。 Shell扩展是个进程内服务器(运行在Explorer进程内),它实现了一些接口来处理与 Explorer 的通信。 ATL在我看来是设计Shell扩展最简单最快捷的方法, 如果没有它,你就不得不一遍又一遍地编写繁琐的 QueryInterface() 及AddRef()代码. 另外,在Windows NT 和 2000上调试Shell扩展相对比较容易一些,这我以后会讲到的。 Shell扩展有很多种类型,每种类型都在各自不同的事件发生时被调用运行,但也有一些扩展的类型和调用情形是非常相似的。
现在你可能想知道Shell扩展到底是什么样的. 如果你安装了 WinZip (有谁没装的吗?), 它就包含了多种的Shell扩展,其中也就有上下文菜单扩展. 下图是WinZip 8 为压缩文件对象添加的定制菜单项: WinZip 编写了添加菜单项的代码, 提供了浏览器状态栏上的菜单项帮助提示, 并在用户选择一个菜单命令时执行相应的操作。 WinZip 还包括一个拖放目标扩展处理器. 该类型与上下文菜单十分类似, 但它是在用户用右键拖放文件时被触发的. 下图是 WinZip 定制的拖放菜单: Shell扩展的类型很多,而且微软也正不断地在每一新版本的Windows中加入更多的扩展类型. 现在让我们把注意力放在上下文菜单上, 因为它们易于编写,效果也很明显(这能马上满足你). 在我们编写代码之前, 先说一下一些简化编码及调试工作的技巧. 当shell扩展被 Explorer调用后, 它会在内存中呆上一段时间, 这会使你无法重新编译并生成Shell扩展DLL文件. 要让 Explorer 更迅速地卸载Shell扩展执行文件,需要创建如下注册表项: HKLM\Software\Microsoft\Windows\CurrentVersion\Explorer\AlwaysUnloadDLL 并将其值设为 "1". 对于Win9x, 这是你能做的最好的方法。 而在Win NT/2000上, 你可以到如下键: HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer 并创建一个名为DesktopProcess的DWORD值 1. 这会使桌面和任务栏运行在同一个进程中, 而其他每一个 Explorer 窗口都运行在它自己的每一个进程内. 也就是说,你可以在单个的Explorer 窗口内进行调试, 而后只要你关闭该窗口,你的DLL就会被马上卸载, 这就避免了因为DLL正被Windows使用而无法替换更新. 而如果不幸出现这种情况,你就不得不注销登录后再重新登录进Windows从而强制卸载使用中的Shell扩展DLL. 我将在稍后解释如何在Win9x中进行调试的细节. 开始编写上下文菜单 – 它该做些什么? 开头先让我们做简单一些, 只弹出一个对话框以表明当前的扩展能够正常地工作. 我们把扩展关联到 .TXT 文件, 因此当用户右键单击文本文件对象时扩展就会被调用. 使用 AppWizard 开始 好吧, 让我们开始吧! 什么? 我还没告诉你怎样使用那些神秘的 shell 扩展接口? 别着急, 我会边进行边解释的。 我觉得先解释一下一个概念再紧接着说明示例代码,对理解例子程序会更简单一些. 当然我也可以把所有的东西都先解释完,然后再解释代码, 但我觉得这样做不能吸引人的注意力。不管怎么样, 向 VC开火,开始! 运行AppWizard,生成一个名为SimpleExt 的 ATL COM 工程. 保留所有默认的设置选项,点击”完成”. 现在我们已经有了一个空的 ATL工程,它可以编译并生成一个 DLL, 但我们还需要添加Shell扩展的 COM 对象. 在 ClassView 中, 右击 SimpleExt classes 条目, 选择 New ATL Object. 在ATL Object Wizard里, 第一页默认已经选择了 Simple Object , 所以单击 Next 即可. 在第二页中, 在Short Name 文本框里输入 SimpleShlExt ,点击 OK. (其余的文本框会自动填充完.) 这样就创建了一个名为 CSimpleShlExt 的类,其包含了实现COM对象最基本的代码. 我们将在这个类中加入我们自己的代码. 初始化接口 当我们的shell扩展被加载时, Explorer 将调用我们所实现的COM对象的 QueryInterface() 函数以取得一个 IShellExtInit 接口指针. 该接口仅有一个方法 Initialize(), 其函数原型为:
Explorer 使用该方法传递给我们各种各样的信息. PidlFolder是用户所选择操作的文件所在的文件夹的 PIDL 变量. (一个 PIDL [指向ID 列表的指针] 是一个数据结构,它唯一地标识了在Shell命名空间的任何对象, 一个Shell命名空间中的对象可以是也可以不是真实的文件系统中的对象.) pDataObj 是一个 IDataObject 接口指针,通过它我们可以获取用户所选择操作的文件名。 hProgID 是一个HKEY 注册表键变量,可以用它获取我们的DLL的注册数据. 在这个简单的扩展例子中, 我们将只使用到 pDataObj 参数. 要添加这个接口进 COM 对象, 先打开SimpleShlExt.h 文件, 然后加入下列标红的代码:
COM_MAP是ATL实现 QueryInterface()机制的宏,它包含的列表告诉ATL其它外部程序用QueryInterface()能从我们的 COM对象获取哪些接口. 接着,在类声明里, 加入Initialize()的函数原型. 另外我们需要一个变量来保存文件名:
然后, 在 SimpleShlExt.cpp 文件中, 加入该函数方法的实现定义:
我们要做的是取得被右击选择的文件名,再把该文件名显示在弹出消息框中。 可能会有多个文件同时被选择右击, 你可以用pDataObj 接口指针获取所有的文件名, 但现在为简单起见, 我们只获取第一个文件名. 文件名的存放格式与你拖放文件到带WS_EX_ACCEPTFILES风格的窗口时使用的文件名格式是一样的。 这就是说我们可以使用同样的API来获取文件名: DragQueryFile(). 首先我们先获取包含在IdataObject中的数据句柄:
请注意错误检查,特别是指针的检查。 由于我们的扩展运行在 Explorer 进程内, 要是我们的代码崩溃了, Explorer也会随之崩溃. 在Win 9x上, 这样的一个崩溃可能导致需要重启系统. 所以, 现在我们有了一个 HDROP 句柄, 我们就可以获取我们需要的文件名了:
要是我们返回 E_INVALIDARG, Explorer 将不会继续调用以后的扩展代码. 要是返回 S_OK, Explorer 将再一次调用QueryInterface() 获取另一个我们下面就要添加的接口指针: IContextMenu. 与上下文菜单交互的接口 一旦 Explorer 初始化了扩展,它就会接着调用 IContextMenu 的方法让我们添加菜单项, 提供状态栏上的提示, 并响应执行用户的选择. 添加IContextMenu 接口到Shell扩展类似于上面IshellExtInit接口的添加 .打开 SimpleShlExt.h,添加下列标红的代码:
添加 IContextMenu 方法的函数原型:
修改上下文菜单 IContextMenu 有三个方法. 第一个是 QueryContextMenu(), 它让我们可以修改上下文菜单. 其原型为:
hmenu 上下文菜单句柄. uMenuIndex 是我们应该添加菜单项的起始位置. uidFirstCmd 和 uidLastCmd 是我们可以使用的菜单命令ID值的范围. uFlags 标识了Explorer 调用QueryContextMenu()的原因, 这我以后会说到的. 而返回值根据你所查阅的文档的不同而不同. Dino Esposito 的书中说返回值是你所添加的菜单项的个数. 而 VC6.0所带的MSDN 又说它是我们添加的最后一个菜单项的命令ID加上 1. 而最新的 MSDN 又说: 将返回值设为你为各菜单项分配的命令ID的最大差值,加上1. 例如, 假设 idCmdFirst 设为5,而你添加了三个菜单项 ,命令ID分别为 5, 7, 和 8. 这时返回值就应该是: MAKE_HRESULT(SEVERITY_SUCCESS, 0, 8 - 5 + 1). 我是一直按 Dino 的解释来做的, 而且工作得很好. 实际上, 他的方法与最新的 MSDN 是一致的, 只要你严格地使用 uidFirstCmd作为第一个菜单项的ID,再对接续的菜单项ID每次加1. 我们暂时的扩展仅加入一个菜单项,所以 QueryContextMenu() 非常简单:
首先我们检查 uFlags. 你可以在 MSDN中到所有标志的解释, 但对于上下文菜单扩展而言, 只有一个值是重要的: CMF_DEFAULTONLY. 该标志告诉Shell命名空间扩展保留默认的菜单项,这时我们的Shell扩展就不应该加入任何定制的菜单项,这也是为什么此时我们要返回 0 的原因. 如果该标志没有被设置, 我们就可以修改菜单了 (使用 hmenu 句柄), 并返回 1 告诉Shell我们添加了一个菜单项. 在状态栏上显示提示帮助 下一个要被调用的IContextMenu 方法是 GetCommandString(). 如果用户是在浏览器窗口中右击文本文件,或选中一个文本文件后单击文件菜单时,状态栏会显示提示帮助. 我们的 GetCommandString() 函数将返回一个帮助字符串供浏览器显示. GetCommandString() 的原型是:
idCmd 是一个以0为基数的计数器,标识了哪个菜单项被选择. 因为我们只有一个菜单项, 所以idCmd 总是0. 但如果我们添加了3个菜单项, idCmd 可能是 0, 1, 或 2. uFlags 是另一组标志(我以后会讨论到的). PwReserved 可以被忽略. pszName 指向一个由Shell拥有的缓冲区,我们将把帮助字符串拷贝进该缓冲区. cchMax 是该缓冲区的大小. 返回值是S_OK 或 E_FAIL. GetCommandString() 也可以被调用以获取菜单项的动作( "verb") . verb 是个语言无关性字符串,它标识一个可以加于文件对象的操作。 ShellExecute()的文档中有详细的解释, 而有关verb的内容足以再写一篇文章, 简单的解释是:verb 可以直接列在注册表中(如 "open" 和 "print"等字符串shell程序的编写流程), 也可以由上下文菜单扩展创建. 这样就可以通过调用ShellExecute()执行实现在Shell扩展中的代码. 不管怎样, 我说了这多只是为了解释清楚GetCommandString() 的作用. 如果 Explorer 要求一个帮助字符串,我们就提供给它. 如果 Explorer 要求一个verb, 我们就忽略它. 这就是 uFlags 参数的作用. 如果 uFlags 设置了GCS_HELPTEXT 位, 则 Explorer 是在要求帮助字符串. 而且如果 GCS_UNICODE 被设置, 我们就必须返回一个Unicode字符串. 我们的 GetCommandString() 如下:
这里没有什么特别的代码; 我用了硬编码的字符串并把它转换为相应的字符集. 如果你从未使用过ATL字符串转化宏,你一定要学一下,因为当你传递Unicode字符串到COM和OLE函数时,使用转化宏会很有帮助的. 我在上面的代码中使用了T2CW 和 T2CA 将TCHAR 字符串分别转化为Unicode 和 ANSI字符串. 函数开头处的USES_CONVERSION 宏其实声明了一个将被转化宏使用的局部变量. 要注意的一个问题是: lstrcpyn() 保证了目标字符串将以null为结束符. 这与C运行时(CRT)函 数strncpy()不同. 当要拷贝的源字符串的长度大于或等于cchMax 时 strncpy()不会添加一个 null 结束符. 我建议总使用lstrcpyn(), 这样你就不必在每一个strncpy()后加入检查保证字符 串以 null为结束符的代码. 执行用户的选择 IContextMenu 接口的最后一个方法是 InvokeCommand(). 当用户点击我们添加的菜单项时该方法将被调用. 其函数原型是:
CMINVOKECOMMANDINFO 结构带有大量的信息, 但我们只关心 lpVerb 和 hwnd 这两个成员. lpVerb参数有两个作用 – 它或是可被激发的verb(动作)名, 或是被点击的菜单项的索引值. hwnd 是用户激活我们的菜单扩展时所在的浏览器窗口的句柄. 因为我们只有一个扩展的菜单项, 我们只要检查lpVerb 参数, 如果其值为0, 我们可以认定我们的菜单项被点击了. 我能想到的最简单的代码就是弹出一个信息框, 这里的代码也就做了这么多. 信息框显示所选的文件的文件名以证实代码正确地工作.
注册Shell扩展 现在我们已经实现了所有需要的COM接口. 可是我们怎样才能让浏览器使用我们的扩展呢? ATL 自动生成注册COM DLL服务器的代码, 但这只是让其它程序可以使用我们的DLL. 为了告诉浏览器使用我们的扩展, 我们需要在文本文件类型的注册表键下注册扩展: HKEY_CLASSES_ROOT\txtfile 在这个键下, 有个名为 ShellEx 的键保存了一个与文本文件关联的Shell扩展的列表. 在 ShellEx 键下, ContextMenuHandlers 键保存了上下文菜单扩展的列表. 每个扩展都在ContextMenuHandlers下创建了一个子键并将其默认值设为扩展COM的GUID. 所以, 对于我们这个简单的扩展, 我们将创建下键: HKEY_CLASSES_ROOT\txtfile\ShellEx\ContextMenuHandlers\SimpleShlExt 并将其默认值设为我们的 GUID: "{5E2121EE-0300-11D4-8D3B-444553540000}". 你不必写任何代码就可以完成注册操作. 如果你看一下Fileview页的文件列表, 你会看到s. 该文本文件将被ATL分析, 并指导ATL在该COM服务器注册时添加附加的注册键, 而注销时又该删除哪些键. 以下是所指定添加的注册表项:
每一行代表一个注册表键, "HKCR"是 HKEY_CLASSES_ROOT 的缩写. NoRemove 关键字表示当该COM服务器注销时该键 不用被删除. 最后一行有些复杂. ForceRemove 关键字表示如果该键已存在, 那么在新键添加之前该键先应被删除. 这行脚本的余下部分指定一个字符串,它将被存为 SimpleShlExt 键的默认值. 在这我插几句话. 我们是在 HKCR\txtfile下注册的. 但是 "txtfile" 名并不是一个永久的或预定好的名称. 如果你看一下 HKCR\.txt, 该键的默认值正是txtfile. 这样就会有一些副作用: 1.我们不能可靠地使用 RGS 教本,因为 "txtfile" 可能不是正确的键名. 2. 一些文本编辑软件可能安装到系统并直接关联到 .TXT 文件. 如果它改变了HKCR\.txt 键的默认值, 所有现存的Shell扩展都将停止工作. 在我看来,这确是个设计上的错误. 我认为微软也是这么想的, 因为最新的扩展类型, 如QueryInfo扩展注册在 .txt 键下. 好了,到此为止. 最后还有一个注册细节. 在NT/2000上, 我们还得将我们的扩展放到 "approved" 扩展列表中. 如果我们不这样做, 我们的扩展不会被没有管理员权限的用户调用. 该列表保存在: HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved 在该键下, 我们要创建一个以我们的GUID 为名的字符串键,键的内容任意. 与之有关的代码在DllRegisterServer() 和 DllUnregisterServer() 函数中. 只是些简单的注册表获取, 我也就不在这写出了. 你可以在例子工程代码中到它. 调试Shell 扩展 最终你会写一个不会这么简单的扩展, 那时你就不得不进行调试. 打开你的工程设置, 在 Debug 页” Executable for debug session”编辑框中输入浏览器程序的全路径, 如:"C:\". 如果你使用的是 NT 或 2000, 并且你已经设置了上述的 DesktopProcess 注册键, 那么当你按F5进行调试时就会打开一个新的浏览器窗口. 只要你在这个窗口内完成你所有的工作,当你关闭该窗口时扩展同时会被卸出内存,这样就不会防碍我们重建 DLL了. 在Windows 9x上, 在重新调试之前你不得不关闭Shell. 你可以: 点击 “开始”, 再点击”关闭”. 按住 Ctrl+Alt+Shift 并点击”取消”. 这样就会关闭Shell, 你会看到桌面消失了. 接着,你可以切换到 MSVC 再按 F5进行调试. 要中止调试, 按 Shift+F5 关闭. 完成调试后, 你可以从”开始 运行” 以正常重起. 扩展的样子 下面就是我们添加自定义菜单项后的样子: 看,我们的菜单项在那! 下图是浏览器状态栏的帮助提示字符串的显示: 弹出信息框如下图, 显示了所选的文件名: 第一节完 | |||||||||||||||||||||||||||||||||||||||||||||||||||
本资料由同济大学戴维制作 资料从网上各处收集,版权归原作者所有,由戴维进行了整理与加工,部分不全的地方进行了翻译补全。 如需转载,请联系作者E-mail E-mail:davidmails@126 davidsoft.6to23 | |||||||||||||||||||||||||||||||||||||||||||||||||||
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论