Windows中存在各种Shell扩展,但是介绍编写Shell扩展的文档却较少见。前段时间看了一个介绍Shell扩展编程的文档,简洁明了,可操作性强,于是就边看边照做里面的例子程序。文档一共9节,我已经看了6节了。写这篇文章小结一下。
Shell扩展的英文是“Shell Extension”,Shell就是Windows的资源管理器了,就是桌面,就是“我的电脑”,它是相对于内核Kernel而言的外部的用户接口;Extension的意思是对资源管理器进行功能上的增强。Shell扩展是为资源管理器增加功能的COM组件。Shell扩展都是进程内COM服务器,即运行在Shell进程(资源管理器进程)中,实现一些接口来与Shell进行通信,完成扩展功能。所谓的进程内COM服务器,说简单一点,就是外在表现为dll的COM服务器。
Shell扩展在Windows中随处可见。例如,安装WinRAR后,在“我的电脑”中右键击某文件,弹出的上下文菜单中会有“添加到压缩文件……”菜单项,这是上下文菜单扩展;Windows XP进行一定更新后,显示属性对话框(通过在桌面空白处右键单击,选择“属性”打开)中会有“壁纸自动换”标签页,这是属性页扩展。Shell扩展在Shell进程中发生一定事件时被Shell调用,例如,上下文菜单扩展在要弹出上下文菜单时被调用。Shell扩展的类型小结如下:
类型 | 调用时机 | 要做的动作 |
上下文菜单扩展 | 用户右键单击文件对象时,或者在文件窗口背景单击右键时 | 添加菜单项到上下文菜单中,用户选择扩展添加的菜单项时,执行扩展需要的操作 |
属性页扩展 | 显示文件属性对话框时 | 添加定制属性页到属性对话框,在定义的属性页中显示各种控件,完成所需的功能 |
拖放扩展 | 用户右键拖放文件对象时 | 添加菜单项到上下文菜单中,用户选择扩展添加的菜单项时,执行扩展需要的操作 |
放置(Drop)扩展 | 用户拖动Shell对象并放置到文件对象上时 | 执行需要的操作 |
信息查询(QueryInfo)扩展 | 资源管理器中用户将鼠标停留在Shell对象图标上时 | 返回资源管理器用于显示在提示框中的文本字符串 |
Shell扩展编程就是实现一些具有特定接口的COM对象的过程。这些接口一般在平台SDK的shlobj.h文件中定义。当然,shlobj.h文件也定义了与这些接口的方法相关的标志和结构体等。下面介绍一个简单的信息查询扩展。它的运行效果如下所示:
![](http://hi.csdn.net/attachment/201006/15/0_1276568745Gg11.gif)
在资源管理器中,当鼠标在文本文件上停留时,弹出的提示框中显示文件前512字节的内容,而扩展前的提示框中只是显示文件的类型、修改日期和大小的。下面简要介绍这个扩展的编写过程。
在Visual C++ 6.0中建立一个名为TxtInfo的ATL工程,在工程中加入一个名为TxtInfoShlExt的简单对象。现在编译链接工程就可以生成一个没有任何功能的COM对象了。下面要做的就是给这个COM对象添加并实现与信息查询Shell扩展相关的接口。
与信息查询Shell扩展相关的接口有两个:IPersistFile和IQueryInfo。前者用于取得鼠标停留于其上的文件的名称,含有6个方法,但这里只需要实现其中一个方法;后者用于为资源管理器显示的提示框提供提示文本,含有2个方法,但也只需要实现其中一个方法。IPersistFile在objidl.h中定义;IQueryInfo在shlobj.h中定义。
在Visual C++ 6.0中建立一个名为TxtInfo的ATL工程,在工程中加入一个名为TxtInfoShlExt的简单对象。现在编译链接工程就可以生成一个没有任何功能的COM对象了。下面要做的就是给这个COM对象添加并实现与信息查询Shell扩展相关的接口。
与信息查询Shell扩展相关的接口有两个:IPersistFile和IQueryInfo。前者用于取得鼠标停留于其上的文件的名称,含有6个方法,但这里只需要实现其中一个方法;后者用于为资源管理器显示的提示框提供提示文本,含有2个方法,但也只需要实现其中一个方法。IPersistFile在objidl.h中定义;IQueryInfo在shlobj.h中定义。
下面来给COM对象添加这两个接口。打开TxtInfoShlExt.h文件,在文件开头加入下面两行:
#include #include |
声明CTxtInfoShlExt实现上述两个接口:
class ATL_NO_VTABLE CTxtInfoShlExt : public CComObjectRootEx, public CComCoClass, public ITxtInfoShlExt, public IPersistFile, public IQueryInfo { public: CTxtInfoShlExt() { } |
添加接口映射:
BEGIN_COM_MAP(CTxtInfoShlExt) COM_INTERFACE_ENTRY(ITxtInfoShlExt) COM_INTERFACE_ENTRY(IPersistFile) COM_INTERFACE_ENTRY(IQueryInfo) END_COM_MAP() |
添加方法声明和方法实现需要的一个成员变量:
// ITxtInfoShlExt public: // IPersistFile STDMETHOD(GetClassID)(LPCLSID) { return E_NOTIMPL; } STDMETHOD(IsDirty)() { return E_NOTIMPL; } STDMETHOD(Save)(LPCOLESTR, BOOL) { return E_NOTIMPL; } STDMETHOD(SaveCompleted)(LPCOLESTR) { return E_NOTIMPL; } STDMETHOD(GetCurFile)(LPOLESTR*) { return E_NOTIMPL; } STDMETHOD(Load)(LPCOLESTR, DWORD); // IQueryInfo STDMETHOD(GetInfoFlags)(DWORD*) { return E_NOTIMPL; } STDMETHOD(GetInfoTip)(DWORD,LPWSTR*); protected: WCHAR m_szFileName[MAX_PATH+2]; |
上面说过,两个接口中都只有一个方法需要实现,其他方法与本扩展无关,所以把其他方法定义成内联函数,简单地返回E_NOTIMPL表示方法未实现。
下面是IPersistFile的Load()方法的实现:
HRESULT CTxtInfoShlExt::Load(LPCOLESTR wszFilename, DWORD dwMode) { lstrcpyW(m_szFileName,wszFilename); return S_OK; } |
该方法只是简单地保存资源管理器传给扩展的鼠标停留其上的文件名(Shell扩展都是与Shell,即资源管理器进行交互的)。说点似乎与本文主题无关的话:COM的基本字符类型是OLECHAR,在Win32平台上,它是wchar_t的typedef。也就是说,在Win32平台上,COM的基本字符类型是宽字符。那么,COM中的字符串当然也就是宽字符字符串的。所以复制它的时候使用的是宽字符版本的lstrcpy函数,因为这个工程没有定义使用Unicode编译(这些内容是昨天晚上看书才看到的^_^)。下面是wtypes.h中的相关定义:
#if defined(_WIN32) && !defined(OLE2ANSI) typedef WCHAR OLECHAR; typedef /* [string] */ OLECHAR *LPOLESTR; typedef /* [string] */ const OLECHAR *LPCOLESTR; #define OLESTR(str) L##str #else typedef char OLECHAR; typedef LPSTR LPOLESTR; typedef LPCSTR LPCOLESTR; #define OLESTR(str) str #endif |
当鼠标在文件对象上停留时,资源管理器会显示提示文本框。在显示文本框之前,资源管理器需要确定要显示的文本,它会首先查询相关的Shell扩展,调用其IQueryInfo接口的GetInfoTip()方法取得提示文本,如果没有相关的Shell扩展,或者调用GetInfoTip()方法失败,Shell就显示默认提示文本(文件类型、修改日期、大小等)。下面是IQueryInfo接口的GetInfoTip()方法的实现:
HRESULT CTxtInfoShlExt::GetInfoTip(DWORD dwFlags, LPWSTR* ppwszTip) { USES_CONVERSION; const int MAX_LEN = 512; WCHAR* pTip; *ppwszTip = NULL; pTip = (WCHAR*)CoTaskMemAlloc(sizeof(WCHAR) * (MAX_LEN+2)); if (NULL == pTip) return E_FAIL; ZeroMemory(pTip,sizeof(WCHAR) * (MAX_LEN+2)); HANDLE hFile = INVALID_HANDLE_VALUE; HANDLE hMap = NULL; LPVOID pData = NULL; __try{__try { hFile = CreateFileW(m_szFileName,GENERIC_READ,FILE_SHARE_READ, NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL); if (INVALID_HANDLE_VALUE == hFile) return E_FAIL; hMap = CreateFileMapping(hFile,NULL,PAGE_READONLY,0,0,NULL); pData = MapViewOfFile(hMap,FILE_MAP_READ,0,0,0); //将文件开始的一部分显示到提示文本中 MultiByteToWideChar(CP_ACP,0,(const char*)pData,-1,pTip,MAX_LEN); *ppwszTip = pTip; return S_OK; } __except(EXCEPTION_EXECUTE_HANDLER){}} __finally { if (NULL != pData) UnmapViewOfFile(pData); if (NULL != hMap) CloseHandle(hMap); if (INVALID_HANDLE_VALUE != hFile) CloseHandle(hFile); } return S_OK; } |
方法首先分配用于容纳提示文本的内存,这里使用的是CoTaskMemAlloc()方法,这是COM所要求的。分配内存后,写入提示文本,然后通过ppwszTip返回提示文本。方法中并没有释放分配的内存,因为ppwszTip是out参数,它使用的内存由客户端(资源管理器进程)负责释放。随后方法使用内存映射文件进行文件内容的读取。我看的那文档中介绍的方法是用MFC提供的CStdioFile进行文件的读取,用CString进行数据转换,比较烦琐,还要求工程有MFC支持,所以我没有采用。关于内存映射文件的使用方法,可以参考《Windows核心编程》一书。今年年初我基本看完了这书,学习的好多内容没地方使用,在这里用到了,也是一种实践了。
注意方法对MultiByteToWideChar()的调用。COM接口方法使用的一般是宽字符,所以这里要把多字节字符转换成宽字符。当然,这里假定了文件内容采用的是多字节字符集。如果文件内容不是多字节字符集编码的,比如说,是Unicode编码的,则这样处理会出错的。下图就是本扩展处理Unicode编码文件的效果:
![](http://hi.csdn.net/attachment/201006/15/0_1276568746klVe.gif)
至此,这个扩展编写完成了。下面需要在系统中注册这个扩展,让系统在弹出上下文菜单时调用它。Shell扩展都是通过注册表来注册的,也就是要在注册表中添加一些项目。手动进行添加当然是比较麻烦的。ATL项目都有一个相关的.rgs文件,其内容是COM服务器被注册时要向注册表添加的内容,当ATL项目被编译时,相关条目被添加到注册表。这其实是一个Custom Building步骤,在项目属性对话框的Custom Build标签页可以看到相关定义。为注册Shell扩展,只需要把与Shell扩展相关的注册表内容添加到这个文件就可以了。下面是要添加的内容:
HKCR { NoRemove .txt { NoRemove shellex { ForceRemove {00021500-0000-0000-C000-000000000046} = s '{1C426AB5-B5F6-44CC-A9D8-0A26EE45A760}' } } } |
我参考的那个文档的作者说,微软把这个键值定义为{00021500-0000-0000-C000-000000000046},而不是我们所预想的 “TooltipHandlers”之类有意义的字符串,这是微软在故意隐瞒一些Shell扩展。^_^
![菊子曰](https://i-blog.csdnimg.cn/blog_migrate/bcfc021cd9686f287c7a9998494cbb28.png)