第十五章 SHELL扩展
谈到Windows Shell编程,Shell扩展是最重要的科目之一,绝大多数商业应用的最酷特征的都是通过Shell扩展实现的,而且有许多显著的系统特征实际都是插入了扩展代码。Shell扩展尤其令人激动的是它允许你把你的应用作为Shell的一部分来处理。
Shell扩展的另一个好处是微软正在使它变得更聪明,例如,‘查找’菜单,从Windows95 到Windows98 一直是通过Shell扩展增强的,而且增加了新条目。还有,出现在文档关联菜单上的位图项也是使用Shell扩展增加的。
Shell扩展不仅是构建增加Shell功能模块的重要手段,而且也是使应用获得有力的Shell特征的重要方法。在前面各章中,我们讨论了系统集成方面Win32应用程序应该做的工作。我们探讨了关联菜单,图标,和几个其它方面技术。然而,这些都是静态和确定的。你可以设置或删除它们,然而,这些就是你所能做的全部:在这之间你不能做任何事情。因此,通向完全融入Windows的应用最后一步是要考虑编写一个或多个Shell扩展的可能性。注意,我说的“可能性”,事实上尽管Shell扩展是与Shell通讯的有力并且是灵活的方法,但是它并不是你和你的程序必须做的。
在这一章中,我们将探讨所有Shell扩展的编程技术,并且提供某些有意义的示例,主要方向是:
Shell扩展是什么,怎样与它们一同工作
用C++ 和ATL怎样写Shell扩展
Shell扩展的排错方法
使用Shell扩展定制关联菜单,图标,和属性
这章的最后部分将专注于文件观察器,严格地说,它们并不是Shell扩展,但是它们有类似的内部结构。文件观察器是一个程序模块,它可以使你能快速预览给定类型的文档而不需要借助建立和管理那种类型文件的应用。文件观察器通常与关联菜单的‘快速观察’项关联。
Shell扩展:类型和提示
Shell扩展是一个进程内COM服务器,它在探测器需要时被加载。Shell扩展不是一个全新的概念,它只比Wondows3.1的文件管理器外挂多了一点点东西。然而,Shell扩展使用了COM体系结构而不是DLL函数集,并且给出更广泛的功能范围。
什么是Shell扩展
正象上面提到的,Shell扩展是实现COM接口的进程内COM服务器。你需要编写模块,注册它到注册表,并运行探测器窗口实例来测试它。不必着急知道什么时候,怎样或由谁来调用它——倘若你正确地注册了它,这些是自动发生的。Shell扩展是DLL,可以放在PC的任何地方。就象任何其它COM服务器一样,它输出四个全程函数,通过这些函数,客户端模块可以识别和连接到这个服务器:
DllGetClassObject()
DllCanUnloadNow()
DllRegisterServer()
DllUnregisterServer()
除此之外,Shell扩展还需要提供通常COM的一些接口,如类工厂和IUnknown接口的实现。最后它还必须实现需要与Shell交互的接口。
调用Shell扩展
有一定数量的探测器可识别事件是可经由客户模块定制的,例子是探测器显示关联菜单或属性页,绘制图标或拖拽文件操作,也就是说,在执行一种文档的特殊任务时,探测器查找注册的用户模块,如果找到,则连接这个模块并调用要求的接口方法。这个关系看上去有点象Windows初级编程所描述的回调机理。回调是预定义原型的函数(通常有推荐的行为),服务器模块将调用这个回调函数以使客户可以插入响应给定的事件。Windows API的枚举函数EnumWindows()就是一个极好的例子。对于Shell扩展所发生的情形概念上与此完全类似。
文件管理器的外挂
文件管理器的外挂正好依赖于回调函数,在加载时,文件管理器扫描它的winfile.ini文件查找‘外挂’节的DLL名:
[AddOns]
MyExtension=C:/WINDOWS/SYSTEM/FMEXT.DLL
在这个DLL中文件管理器希望找到FMExtensionProc()函数,其原型为:
LRESULT CALLBACK FMExtensionProc(HWND hwnd, WORD wMsg, LPARAM lParam);
此时,管理器开始发送消息到这个函数。通过编写这样一个函数,你就能够添加新工具条按钮,被通知选中状态,修改菜单,和作其它操作。如果你愿意,可以参考Internet客户端SDK资料。
从文件管理器的外挂到Shell扩展
我们已经有了文件管理器外挂导出操作的概念,现在可以把这个概念转换到Shell扩展。这里主要的结构差异是:
代替单一回调函数的是COM接口
代替INI文件的是一批注册键和值,它们关联到扩展的文件类型
代替简单DLL的是COM服务器
所以,尽管有一些无可否认的类似性,文件管理器的外挂与Shell扩展是两个根本不同的概念。技术范围已经改变:文件管理器外挂是应用为中心的,信息交换很少考虑单个文件,并且不识别文件类型。Shell扩展分别施加于每一种文件类型——它们是为这种活动方法而专门设计。
探测器怎样导入Shell扩展
为了理解探测器与Shell扩展之间的交互作用,让我们调查一个实际情况。在这个工作完成后你就能清楚地理解这些操作怎样互相作用,以及为什么Shell扩展要这样设计。
我们前面提到过,在进一步处理特定任务集之前,探测器在注册表的某个地方寻找注册模块。它装入找到的所有扩展,并且调用它们的方法。为了获得一定的行为,只需适当地注册模块。要禁止它就要注销这个模块。
要探查的注册表确切路径和扩展的编程接口可以各不相同,这依赖于探测器触发调用所引起的事件。
显示关联菜单
看一个典型的例子:显示特定文件类型——位图(bitmap)的关联菜单。用户在Shell观察下右击BMP类型文件时这个过程启动。关联菜单由不同的项目组构成,首先是系统标准项如‘拷贝’,‘剪切’,‘建立快捷方式’和‘属性’。然后是文档特有的动词,这是静态附加的。再有就是所有文件附加的通用动词,不管是什么类型的文件都有这些项。第四组是来自关联菜单Shell扩展的项,这是为特定类型文件而注册的扩展,此时是位图文件。
当探测器建立弹出菜单时,它启动所有附加的标准项,和每一个注册表中的项,然后它在相关文件类型的ShellEx键下查看(如果存在),搜索ContextMenuHandlers子键。对于BMP,其形式为:
HKEY_CLASSES_ROOT
/Paint.Picture
/ShellEx
/ContextMenuHandlers
位图的主键是Paint.Picture,微软的Paint是一个管理位图的程序。这是默认的,除非你安装了不同的图像软件。
在ContextMenuHandlers键下,默认值包含实现扩展的COM 服务器的CLSID。知道了这个CLSID后。探测器模块装入它到自己的内存空间。这就完成了服务器实例的建立,并且查询扩展所要求的接口。对于关联菜单,接口是IContextMenu,这个接口包含了添加新菜单项的方法,恢复在状态条上显示的描述串,和执行响应用户点击的一些代码。
其工作过程是:探测器首先唤醒IContextMenu::QueryContextMenu(),来请求模块添加新菜单项。每当新菜单项被选中,探测器都调用GetCommandString()来获取显示在状态条上的描述。最后,当有点击发生在客户菜单项上时,运行InvokeCommand()来提供运行时的行为。这些由探测器唤醒的函数可以提供在Shell中定制菜单项的手段,当然还需要严格地按规定注册。后面我们将深入的研究这些方法。
Shell扩展的类型
我们反复提到Shell扩展是在Shell响应特定事件集时被装入的。因此,有固定数量的Shell扩展,即有输出不同函数的COM接口集来影响特殊的情况。显示关联菜单不同于绘制图标,或显示属性对话框,所以不同的COM接口做不同的工作也就不奇怪了。
Shell扩展的类型是:
Shell扩展 |
接口 |
描述 |
关联菜单 |
IContextMenu |
允许添加新项到Shell对象的关联菜单 |
右键拖拽 |
IContextMenu |
允许添加新项显示在右键拖拽文件后的关联菜单上 |
Shell图标 |
IExtractIcon |
可以在运行时决定在一个文件类中给定文件应该显示的图标 |
属性页 |
IShellPropSheetExt |
可以附加属性页到文件类的属性对话框,对控制板小程序也能工作 |
文件钩子 |
ICopyHook |
可以控制任何通过Shell的文件操作。在允许或拒绝时不需告知成功或失败。 |
左键拖拽 |
IDropTarget |
可以决定在Shell中当对象被拖动(使用鼠标左键)到另一个之上时需要做什么 |
剪裁板 |
IDataObject |
可以定义对象怎样拷贝到剪裁板或怎样从剪裁板抽取对象 |
编写Shell扩展
编写Shell扩展就如同编写进程内COM服务器一样,这没有什么可奇怪的。你必须提供基本的COM素材,实现接口,适当地注册服务器,以及随后的测试和排错。与任何开发过的其它COM模块一样,其中含有大量的重复且很少改动的代码,这些代码本身已经封装在某些C++ 类中。因此我们可以预知下一步将要干什么。
使用ATL
我们建议使用ATL作为开发Shell扩展的工具,毕竟,现在的ATL是C++ 开发COM服务器最好的工具,而且Shell扩展本身就是ATL结构的。微软活动模版库是特别设计用于简化开发COM模块的,而且远比MFC先进。
第一个Shell扩展
现在是我们编写Shell扩展的时候了。Shell扩展实际是相当简单的对象,就象开发玩具一样,即使是头一个要开发的,也是如此。我们将从完成前一章的Windows元文件和增强元文件的例子开始。目标是展示怎样添加客户页面到WMF和EMF文件的属性对话框。
添加属性页
直接在属性页预览元文件是不是更好一点。确实,你可以从文件夹的‘观察 | 作为Web页面’的选项打开所选择的文件进行预览,但是,如果你不知道或不想要这个观察时会怎么样。此外,如果你还运行在Windows95或NT上,Shell没有更新,会怎么样。当然,答案是属性页的Shell扩展。它与其它任何Shell扩展一样,都能在IE4.0上工作。
要实现哪些接口
通过ATL COM AppWizard生成ATL代码之后,所需要解决的问题是:添加属性页到‘属性’对话框需要实现哪些接口。事实上有两个接口:IShellPropSheetExt和IShellExtInit。头一个提供添加页的方法,而后一个仔细的初始化和建立Shell与扩展之间的连接。两者都在shlobj.h中定义。
IShellPropSheetExt请求使用API函数建立新的属性页,这涉及到通用控件,而后这个页通过回调函数传递给Shell。也就是说,当调用IShellPropSheetExt方法时,Shell传递了一个指向函数的指针,这个函数由扩展回调,将页面作为变量。这个接口有两个方法,其中一个在绝大多数场合都不需要实现。
单一方法的IShellExtInit接收在Shell中选中的文件(或文件组)的名字,并使它成为可用的模块。可以使用任何技术来存储这些名字,而典型的是使用成员变量。Shell扩展的初始化是一个过程,可能对不同类型的扩展有相当的变化,所以使这个机理通用是关键所在。
Shell扩展的初始化
我们需要花费一点时间来讨论Shell扩展怎样初始化的问题。在这里‘初始化’意指探测器调用扩展,传递正确的变量所遵循的过程。基本上,初始化可以取三种形式之一:不必初始化,经由IShellExtInit初始化,和经由IPersistFile初始化。初始化使用的方法依赖于Shell扩展本身的本质。
下表给出各种类型扩展获得初始化的方法(参考前面的Shell扩展类型表)。
初始化 |
应用于 |
描述 |
无须初始化 |
文件钩子,剪裁板 |
Shell扩展不要求任何初始化过程 |
经IShellExtInit初始化 |
关联菜单,属性页, 右键拖拽 |
Shell扩展操作所有选中的文件。它们的名字以相同于拷贝到剪裁板的格式传递 |
经IPersistFile初始化 |
左键拖拽,图标 |
Shell扩展在文件上操作,无论其是否被选中,名字以Unicode串形式传递 |
启动Shell扩展的过程由调用一个或多个初始化接口的方法组成。当探测器感觉到它可能要触发Shell扩展的事件时,它知道注册了哪一种扩展,以及怎样初始化它。它所要做的全部工作就是附加对适当接口的查询操作。
我们的目的是要详细描述当Shell扩展需要时IShellExtInit和IPersistFile接口的工作过程,因此,现在让我们看一下唤醒属性页Shell扩展时IShellExtInit接口的工作过程(我们也将在IconHandler扩展中讨论IPersistFile的初始化过程)。
IShellExtInit接口
我们这里所涉及到的属性页扩展是通过IShellExtInit接口的方式装入的,它只有一个方法称为Initialize(),探测器唤醒并传递三个参数:
类型 |
参数 |
描述 |
LPCITEMIDLIST |
pidlFolder |
对于属性页扩展总是NULL |
LPDATAOBJECT |
Lpdobj |
指向IDataObject对象的指针,可以用这个对象获得当前选中的文件 |
HKEY |
hkeyProgID |
所涉及文件的注册表键 |
因为同一个接口服务于几种类型的扩展,头一个和第三个参数可以有不同的意义,这依赖于被初始化的类型。对于属性页,不涉及到文件夹,所以pidlFolder变量没有使用。hkeyProdID参数是HKEY Handle,指向注册表键,包含对象要唤醒的文件信息。例如,如果Shell扩展操作WMF文件,考虑上一章的例子,则hkeyProdID将握有:
HKEY_CLASSES_ROOT
/WinMetafile
对于属性页的扩展最重要的变量是lpdobj,它包含了指向实现IDataObject接口对象的指针。这是一个已知的接口,有许多用户接口都使用这个接口。基本上,IDataObject定义了运行模块之间要交换的数据块的行为,因此剪裁板和拖拽操作是它的主要应用领域。
拷贝数据到剪裁板和从剪裁板取得数据这种OLE方法说明了存储和恢复指向实现IDataObject对象指针的情况。同样,当你使用COM接口拖拽数据时,源和目的数据交换也是通过IDataObject完成的。另一个观察IDataObject对象的方法是:把IDataObject对象作为Windows Handle的演化——即,表示包含数据的内存块的通用对象。这种增强提供了对数据的存储能力:
具有精确格式的数据,不只是通用的‘某些东西的指针’
在存储介质中而不是在内存中的数据
同时容纳更多的数据块
IDataObject接口输出方法来取得和枚举数据。特别,它使用象FORMATETC和STGMEDIUM这样的结构来定义格式和数据存储介质。在获得IDataObject指针后,你可以询问它以便发现它是否在一定介质上包含特定格式的数据。过一会,在我们揭示了它怎样应用于属性页扩展之后,这一点就更清楚了。
回到属性页的Shell扩展。此时,传递给Initialize()的IDataObject对象包含一个HDROP Handle。在第6章我们看到,这个Handle包含了一个文件名列表,我们可以使用象DragQueryFile()这样的函数遍历这个列表。对于属性页扩展,这个列表包含在Shell中所有当前选中文件的名字。
属性页对话框仅在从Shell右击一个或多个选中文件并且从导出的关联菜单中选择属性项后弹出。选中的文件列表经由实现IDataObject的对象传递给Shell扩展,而且包含了CF_HDROP格式的数据。CF_HDROP是标准剪裁板格式之一,这种形式的数据存储在称之为HDROP的全程内存Handle上。
STGMEDIUM medium;
HDROP hDrop;
FORMATETC fe = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
HRESULT hr = lpdobj->GetData(&fe, &medium);
if(SUCCEEDED(hr))
hDrop = static_cast
上面代码段说明怎样从IDataObject指针恢复HDROP Handle。GetData()通过FORMATETC变量接收要恢复的数据描述,如果成功,则经由STGMEDIUM变量返回。FORMATETC结构定义如下:
typedef struct tagFORMATETC
{ CLIPFORMAT cfFormat;
DVTARGETDEVICE* ptd;
DWORD dwAspect;
LONG lindex;
DWORD tymed;
} FORMATETC, *LPFORMATETC;
就我们的观点,值得注意的成员是cfFormat和tymed,它们分别说明数据格式和存储介质类型。因而代码中CF_HDROP是数据格式,而TYMED_HGLOBAL表示全程内存Handle作为数据返回的存储介质。其它可能的存储介质是磁盘文件,原文件和指向IStorage或IStream对象的指针。
下面我们给出实现‘Do_nothing’的ATL类,其函数在建立示例工程(project)时将重载,下面清单是IShellExtInitImpl.h 头文件,它包含大多数IShellExtInit接口的基本实现。
// IShellExtInitImpl.h
#include
#include
class ATL_NO_VTABLE IShellExtInitImpl : public IShellExtInit
{
public:
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0;
_ATL_DEBUG_ADDREF_RELEASE_IMPL(IShellExtInitImpl)
// IShellExtInit
STDMETHOD(Initialize)(LPCITEMIDLIST, LPDATAOBJECT, HKEY)
{
return S_FALSE;
}
};
IShellPropSheetExt接口
提供添加新属性页方法的接口是IShellPropSheetExt,它输出两个函数(在IUnknown之上的函数):AddPages()和ReplacePage()。第一个函数有下面形式的参数:
类型 |
参数 |
描述 |
LPFNADDPROPSHEETPAGE |
lpfnAddPage |
指向实际添加页面函数的指针 |
LPARAM |
lParam |
必须传递给由lpfnAddPage指定的函数的变量 |
AddPages()建立新的属性页,并调用从lpfnAddPage参数接收的函数。这是一个由Shell定义的回调函数,它有下面的原型:
BOOL CALLBACK AddPropSheetPageProc(HPROPSHEETPAGE hpage, LPARAM lParam);
第二个变量总是由Shell传递来,使第一个参数获得AddPages()的任务。对每一个注册属性页的Shell扩展,这个回调函数都被调用一次,特别是Shell正在显示属性对话框时。AddPages()函数可以添加一个或多个页面,然而,在加多个页面时,它必须建立页面并重复调用由lpfnAddPage指向的函数。
另一个由IShellPropSheetExt输出的方法,ReplacePage(),仅仅用于置换控制面板小程序的属性页在我们的示例中没有实现这个函数,但它的原型是:
HRESULT ReplacePage(UINT uPageID, // 要置换的页索引
LPFNADDPROPSHEETPAGE lpfnReplacePage, // 指向置换页函数的指针
LPARAM lParam); // 附加到函数的变量
遵守我们早期的承诺,下面的清单是IShellPropSheetExtImpl.h,包含了IShellPropSheetExt接口的基本实现:
// IShellPropSheetExtImpl.h
#include
#include
class ATL_NO_VTABLE IShellPropSheetExtImpl : public IShellPropSheetExt
{
public:
TCHAR m_szFile[MAX_PATH];
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0;
_ATL_DEBUG_ADDREF_RELEASE_IMPL(IShellPropSheetExtImpl)
// IShellPropSheetExt
STDMETHOD(AddPages)(LPFNADDPROPSHEETPAGE, LPARAM)
{
return S_FALSE;
}
STDMETHOD(ReplacePage)(UINT, LPFNADDPROPSHEETPAGE, LPARAM)
{
return E_NOTIMPL;
}
};
添加新的属性页
为了适当地开始一个工程(project),我们建立一个新的ATL DLL工程(project)WMFProp,并添加一个简单的对象PropPage。在 ATL 部件框架生成以后,我们需要对新对象的头文件做一些改变,PropPage.h:
// PropPage.h : 声明 CPropPage 对象类
#ifndef __PROPPAGE_H_
#define __PROPPAGE_H_
#include "resource.h" // 主程序符号
#include
#include "IShellExtInitImpl.h" // IShellExtInit
#include "IShellPropSheetExtImpl.h" // IShellPropSheetExt
BOOL CALLBACK PropPage_DlgProc(HWND, UINT, WPARAM, LPARAM);
// CPropPage
class ATL_NO_VTABLE CPropPage :
public CComObjectRootEx
public CComCoClass
public IShellExtInitImpl,
public IShellPropSheetExtImpl,
public IDispatchImpl
{
public:
CPropPage()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_PROPPAGE)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CPropPage)
COM_INTERFACE_ENTRY(IPropPage)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IShellExtInit)
COM_INTERFACE_ENTRY(IShellPropSheetExt)
END_COM_MAP()
// IPropPage
public:
STDMETHOD(Initialize)(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
STDMETHOD(AddPages)(LPFNADDPROPSHEETPAGE, LPARAM);
};
#endif //__PROPPAGE_H_
需要实现的接口方法是Initialize()和AddPages()。我们还声明了静态成员函数PropPage_DlgProc(),它用于定义被添加页面的行为——这是新页面的窗口过程。
Initialize()函数的代码
Initialize()方法代码如下:
HRESULT CPropPage::Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT
lpdobj, HKEY hKeyProgID)
{
if(lpdobj == NULL)
return E_INVALIDARG;
// 初始化通用控件(属性页是通用控件)
InitCommonControls();
// 从IDataObject获得选中文件名,数据以CF_HDROP格式存储
STGMEDIUM medium;
FORMATETC fe = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
HRESULT hr = lpdobj->GetData(&fe, &medium);
if(FAILED(hr))
return E_INVALIDARG;
HDROP hDrop = static_cast
if(DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0) == 1)
{
DragQueryFile(hDrop, 0, m_szFile, sizeof(m_szFile));
hr = NOERROR;
}else
hr = E_INVALIDARG;
ReleaseStgMedium(&medium);
return hr;
}
由于属性页是通用控件,我们需要初始化适当的库。这也说明必须#include commctrl.h,和引入comctl32.lib库。在使用前面描述的技术获得选中文件后,检查有多少选中文件。为简单起见,如果有多个选中文件,我们退出这个函数,这就是下面代码所做的操作:
if(DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0) == 1)
{
...
}
如上调用DragQueryFile()之后,返回选中文件数量。下一行则抽取第一个也是唯一一个文件(它的索引为0),并把它的名字存入m_szFile缓冲:
DragQueryFile(hDrop, 0, m_szFile, sizeof(m_szFile));
最后,所有活动完成后,通过调用ReleaseStgMedium()释放存储介质结构。
AddPages()函数的代码
AddPages()函数的代码如下:
HRESULT CPropPage::AddPages(LPFNADDPROPSHEETPAGE lpfnAddPage, LPARAM lParam)
{
lstrcpy(g_szFile, m_szFile);
// 建立新页面需要填充PROPSHEETPAGE 结构
PROPSHEETPAGE psp;
ZeroMemory(&psp, sizeof(PROPSHEETPAGE));
psp.dwSize = sizeof(PROPSHEETPAGE);
psp.dwFlags = PSP_USEREFPARENT | PSP_USETITLE | PSP_DEFAULT;
psp.hInstance = _Module.GetModuleInstance();
psp.pszTemplate = MAKEINTRESOURCE(IDD_WMFPROP);
psp.pszTitle = __TEXT("预览");
psp.pfnDlgProc = PropPage_DlgProc;
psp.lParam = reinterpret_cast
psp.pcRefParent = reinterpret_cast
// 建立新页面
HPROPSHEETPAGE hPage = ::CreatePropertySheetPage(&psp);
// 添加页面到属性页
if(hPage != NULL)
{
if(!lpfnAddPage(hPage, lParam))
::DestroyPropertySheetPage(hPage);
return NOERROR;
}
return E_INVALIDARG;
}
新页面包含一个对话框,既没有标题也没有边框,而且在上面代码中,PROPSHEETPAGE结构的pszTemplate成员被设置为它的ID。我们设计的对话框包含单个图像控件,具有SS_ENHMETAFILE风格,取名为IDC_METAFILE,附加一个对话框模板到工程的资源中对属性页面的Shell扩展总是必要的。然而,对话框要求对话框过程处理所有它包含的控件。在上例中是PropPage_DlgProc()简单地响应WM_INITDIALOG和绘制原文件,为此,我们使用在前一章中定义的函数。由于对话框过程不能访问类成员,我们通过PROPSHEETPAGE结构的lParam字段传递要显示的文件名,并且对话框过程接收指向这个结构的指针作为WM_INITDIALOG消息的lParam变量。
BOOL CALLBACK PropPage_DlgProc(HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
switch(uiMsg)
{
case WM_INITDIALOG:
HWND hwndMeta = GetDlgItem(hwnd, IDC_METAFILE);
LPPROPSHEETPAGE lppsp = reinterpret_cast
DisplayMetaFile(hwndMeta, reinterpret_cast
return FALSE;
}
return FALSE;
}
注册Shell扩展
我们前面说过,如果没有正确地注册Shell扩展,它们将不能工作:探测器不能找到要加载的模块。每一个Shell扩展每次关联到指定的文件对象是通过文件类型(比如说EMF),或通用的对象(如文件夹)。因而,在注册Shell扩展时,你必须考虑是否增加安装文件类型的信息。如果你写的Shell扩展是对系统文件类型的比如BMP,TXT,文件夹或 *,就不必注册新文件类型了。然而对于客户的文件类型(比如说XYZ),或没有默认定义的文件类型(就象EMF和WMF),你应该保证注册信息的输入。假定文件类型的注册信息正确地注册了,我们仍然需要添加几行到由ATL应用大师产生的标准注册脚本中。这些行应该与Shell扩展操作的文件类型或一同工作的文件类型相关。此时Shell扩展不仅必须注册连接WMF和EMF,还要在下面这些键下注册:
HKEY_CLASSES_ROOT
/WinMetafile
对应WMFs, 和
HKEY_CLASSES_ROOT
/EnhMetafile
对应EMFs。
Shell扩展必须在指定文件类键的shellex子键下注册,在shellex下,你需要建立附加的键分组各种类型的扩展,而且这些都有特定的名字。注册属性页Shell扩展的键为PropertySheetHandlers,在其下可以列出对这个文件类所有属性页Shell扩展的CLSID。
有点陌生的是Shell扩展类型允许定义同一个文件类的多个服务器,它们被顺序调用。例如,很可能是有三个COM服务器实现位图文件类型的三个关联菜单的不同扩展。对于所有Shell扩展,除了那些处理剪裁板和左键拖拽的扩展,都允许有多重扩展存在。后面我们还要讨论这个问题。
下面清单说明怎样将默认的注册脚本改变为正确注册属性页Shell扩展的脚本。
HKCR
{
WMFProp.PropPage.1 = s 'PropPage Class'
{
CLSID = s '{0D0E3558-8011-11D2-8CDB -505850C 10000}'
}
WMFProp.PropPage = s 'PropPage Class'
{
CLSID = s '{0D0E3558-8011-11D2-8CDB -505850C 10000}'
CurVer = s 'WMFProp.PropPage.1'
}
NoRemove CLSID
{
ForceRemove {0D0E3558-8011-11D2-8CDB -505850C 10000} = s 'PropPage Class'
{
ProgID = s 'WMFProp.PropPage.1'
VersionIndependentProgID = s 'WMFProp.PropPage'
ForceRemove 'Programmable'
InprocServer32 = s '%MODULE%'
{
val ThreadingModel = s 'Apartment'
}
'TypeLib' = s '{0D0E354B-8011-11D2-8CDB -505850C 10000}'
}
}
WinMetafile
{
Shellex
{
PropertySheetHandlers
{
{0D0E3558-8011-11D2-8CDB -505850C 10000}
}
}
}
EnhMetafile
{
Shellex
{
PropertySheetHandlers
{
{0D0E3558-8011-11D2-8CDB -505850C 10000}
}
}
}
}
下图说明了注册增强元文件后的注册表状态。注意,其中有三个属性页的Shell扩展。如果你还有另一个增强元文件的Shell扩展——例如管理关联菜单——它们应该以同样的方法注册,但是是在另一个子键下。定位在与PropertySheetHandlers同层。
现在Shell扩展正确地注册了以后,你就能右击EMF或WMF文件,并且有下面的行为出现:
测试Shell扩展
到目前为止我们已经编写并注册了一个Shell扩展,现在我们来看一下它是否做了它应该做的工作。运行Shell扩展的唯一方法是启动探测器并执行引起Shell扩展动作的活动,但是要使探测器确信你的扩展存在可能是比较困难的。在一定场合下,你可能需要注销登录,甚至重启机器来使Shell加载更新版的扩展,相反,对比重启机器,简单地关闭探测器可能更好一点,而且可以使用任务条实用程序,我们在第9章中就是这么做的。还有就是按F5键,但这种方法不能总奏效。
参见这一章后面的Shell扩展开发者手册,其中有更详细的讨论
除了这些小困难之外,我们现在假设正在运行你的扩展。当你感觉到一个错误,并且需要排除代码找到错误发生点时复杂的事情发生了。排除Shell扩展的错误不是直觉的任务,我们需要仔细地检查扩展操作的过程。第一步是设置explorer.exe为排错会话的可执行程序。因为Shell扩展是DLL,并且不是独立可执行程序,因此这一步是必要的。注意,你需要指定探测器的全路径:
第二步是要保证你的Shell扩展工程在VC++IDE中打开。这个技巧是停止Shell,然后在排错器下导出它的新实例运行,这比想象的要困难一点。如果你简单地运行排错器,可以引起探测器窗口的出现,但是这并不是说新的Shell进程已经启动,对于要发生的排错,你首先需要终止Shell进程,而不终止机器上的其它进程,然后再次运行排错器,它将实际地建立一个可排错的Shell进程。
要停止Shell,你可以编程发送WM_QUIT消息到唯一的窗口类‘program’(我们在第9章中已经讨论了这个技术),要手动做这个工作,执行下面的操作:
从开始菜单中选择‘关闭’,并且在按下Ctrl-Alt-Shift时点击‘取消’。这并不容易做到,但是它能工作。当你这样做了之后,任务条消失,你将感觉到系统重启了,但是并没有导致机器的重启。没有任何错误发生,所有都在控制之中。
使用Alt-Tab键导出VC++窗口到顶部,然后运行排错器,现在任务条将再次出现,它标志着新的Shell进程在VC++的排错器下运行。
现在所要做的是与任何其它程序排错一样:点击‘Build | 启动排错 | Go’菜单项。当探测器窗口显示出来时,执行导出Shell扩展的活动。在这个例子中你应该选择WMF文件,右击,并打开属性对话框。
你放置在代码中的断点现在能象通常一样被感觉到,并且在遇到时引起过程停止。在完成排错之后双击桌面将导出任务管理器窗口来到前面:
选择‘文件 | 运行’,导出探测器,所有事情都恢复到以前的状态。我们给出的并不是你每天都要操作的过程,但是它却是能够解决Shell扩展排错的问题。
值得注意的是控制台小程序——它们总是包含一系列帐单页面——不是运行在探测器地址空间中的。也就是说你不能使用上面描述的技术对它们排错。相反,应该指定运行rundll32.exe作为排错会话的可执行程序。
在Windows NT下排错
如果需要在NT下测试,我们建议在下面的注册键上添加你自己的值:
HKEY_CURRENT_USER
/Software
/Microsoft
/Windows
/CurrentVersion
/Explorer
添加的值称为DesktopProcess,其类型为REG_DWORD,值为1。设置了这个值之后,重新登录,你将发现WindowsNT的Shell被划分成两个部分——桌面,任务条和托盘域运行在文件夹和文件的不同进程中。现在在VC++ 环境下运行探测器,你实际正在启动可以排错的新进程,而且任何冲突都不影响稳定的系统桌面。
卸载Shell扩展
另一个关于Shell扩展测试的科目是确定什么时候卸载Shell扩展。与其它COM对象一样,Shell扩展是持续流目标,要求通过DllCanUnloadNow()导出卸载过程。模块是否可以被卸载依赖于它内部的引用计数。没有自动机理来从内存删除引用计数已经变为0的模块,因此探测器调用DllCanUnloadNow()越快,无用的Shell扩展卸载的就越快。注意,卸载后的Shell扩展模块是可以安全再编译的,这对于Shell扩展在开发期间是十分重要的。
默认情况下,探测器每十秒钟尝试一次卸载Shell扩展。资料说明可以通过设置下面注册键的默认值为1来改变这个卸载尝试的频率:
HKEY_LOCAL_MACHINE
/Software
/Microsoft
/Windows
/CurrentVersion
/Explorer
/AlwaysUnloadDll
设置这个键在较老的系统上并没有多大改变——Shell扩展的卸载没有更快。
再说属性页的Shell扩展
上面的例子仅在选种单个文件时才能工作,而且没有阻止我们为每个被选中文件添加属性页,例如:
这种改变要求的代码不是主要的,甚至可以用同时运行多个扩展来实现这个目的——探测器将顺序管理它们。唯一的缺点是你可能需要附加某些属性页的拷贝。下面就看一下我们需要做哪些改变。
修改代码来支持多重选择
要做的头一件也是最显然的一件事就是Shell扩展的类声明,以使其反映出我们不再使用单文件保持轨迹,而是使用列表文件名。这个列表有一个上限,因为prsht.h(属性页头文件)限制其任何一个页表上的页数最大到100,助记常量为MAXPROPPAGES。
这说明在一个页表控件上不可能管理超过100的页面数——我们已经注意到这个控件不能有超过六行的页面,因此合理的最大数是30—35页。下面是我们的新版本IShellPropSheetExt.h:
// IShellPropSheetExtImpl.h (多选版本)
//
//
#include
#include
class ATL_NO_VTABLE IShellPropSheetExtImpl : public IShellPropSheetExt
{
public:
TCHAR m_aFiles[MAXPROPPAGES][MAX_PATH];
int m_iNumOfFiles;
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0;
_ATL_DEBUG_ADDREF_RELEASE_IMPL(IShellPropSheetExtImpl)
// IShellPropSheetExt
STDMETHOD(AddPages)(LPFNADDPROPSHEETPAGE, LPARAM)
{
return S_FALSE;
}
STDMETHOD(ReplacePage)(UINT, LPFNADDPROPSHEETPAGE, LPARAM)
{
return E_NOTIMPL;
}
};
在Initialize()和AddPages()的实现中代码也要做稍微的改变。下面是新的Initialize():
HRESULT CPropPage::Initialize(LPCITEMIDLIST pidlFolder,
LPDATAOBJECT lpdobj, HKEY hKeyProgID)
{
if(lpdobj == NULL)
return E_INVALIDARG;
// 初始化通用控件
InitCommonControls();
// 获取CF_HDROP格式数据
STGMEDIUM medium;
FORMATETC fe = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
HRESULT hr = lpdobj->GetData(&fe, &medium);
if(FAILED(hr))
return E_INVALIDARG;
HDROP hDrop = static_cast
// 取得选中文件数
m_iNumOfFiles = DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0);
// 规格化到允许的最大数
m_iNumOfFiles = (m_iNumOfFiles >= MAXPROPPAGES ? MAXPROPPAGES : m_iNumOfFiles);
// 抽取和管理所有选中的文件
for(int i = 0 ; i < m_iNumOfFiles ; i++)
DragQueryFile(hDrop, i, m_aFiles[i], MAX_PATH);
Rele〉 0aseStgMedium(&medium);
return hr;
}
现在所有文件都存储在文件名数组中了。它们将在AddPages()中一次处理:
HRESULT CPropPage::AddPages(LPFNADDPROPSHEETPAGE lpfnAddPage, LPARAM lParam)
{
for(int i = 0 ; i < m_iNumOfFiles ; i++)
{
// 检查选中的文件是否为元文件
LPTSTR p = PathFindExtension(m_aFiles[i]);
if(lstrcmpi(p, __TEXT(".WMF")) && lstrcmpi(p, __TEXT(".EMF")))
continue;
// 分配要传递的串。它将在 dlgproc 中被释放。
LPTSTR psz = new TCHAR[MAX_PATH];
lstrcpy(psz, m_aFiles[i]);
// 剥离路径和扩展名,以显示在标题上
LPTSTR pszTitle = PathFindFileName(m_aFiles[i]);
PathRemoveExtension(pszTitle);
// 填写PROPSHEETPAGE结构
PROPSHEETPAGE psp;
ZeroMemory(&psp, sizeof(PROPSHEETPAGE));
psp.dwSize = sizeof(PROPSHEETPAGE);
psp.dwFlags = PSP_USEREFPARENT | PSP_USETITLE | PSP_DEFAULT;
psp.hInstance = _Module.GetModuleInstance();
psp.pszTemplate = MAKEINTRESOURCE(IDD_WMFPROP);
psp.pszTitle = pszTitle;
psp.pfnDlgProc = PropPage_DlgProc;
psp.lParam = reinterpret_cast
psp.pcRefParent = reinterpret_cast
HPROPSHEETPAGE hPage = ::CreatePropertySheetPage(&psp);
// 添加页面到属性页上
if(hPage != NULL)
if(!lpfnAddPage(hPage, lParam))
:: DestroyPropertySheetPage(hPage);
}
return NOERROR;
}
关于这个版本的AddPages()函数,有几点需要注意,首先,我们设置属性页的标题为没有路径和扩展名的文件名,这使用了一些来自shlwapi.dll的函数,因此#include
BOOL CALLBACK PropPage_DlgProc(HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
switch(uiMsg)
{
case WM_INITDIALOG:
HWND hwndMeta = GetDlgItem(hwnd, IDC_METAFILE);
LPPROPSHEETPAGE lppsp = reinterpret_cast
DisplayMetaFile(hwndMeta, reinterpret_cast
delete [] reinterpret_cast
return FALSE;
}
return FALSE;
}
最后,函数现在能够识别WMF/EMF与其它文件类型——它接收前者而拒绝后者。当选中了一定数量的文件时,你不用保证它们都是同样类型的。这就是说在右击给定的属性对话框时,你并不需要选择期望类型的文件,因此也不能保证你的扩展被使用。例如,选择EMF和BMP文件,在选中的BMP上右击请求属性对话框时,你将获得BMP的对话框,相反,如果你的文件是元文件或在元文件上右击,你所获得的是下面的情形:
关联菜单
关于添加新项到关联菜单,Shell扩展是最灵活的技术,因为它们给出了事件的全部控制。在前一章中,我们探讨了使用注册表操作来达到同样目的的方法,但是那种技术引起外部代码段执行。用Shell扩展,你可以运行直接与Shell通讯的代码段,接收和返回信息。如果你编写和注册了关联菜单的Shell扩展,你可以有机会选择指定菜单项串,状态条描述和每次菜单被显示的行为。只要你喜欢,总是能编程改变它们,而不需要修改任何注册表。
实现IContextMenu接口
处理关联菜单的Shell扩展就是编写一个实现IContextMenu接口的COM服务器。除了这个变化外,在我们前面描述的示例中不需要做任何改动。从IUnknown导出的IContextMenu有三个函数:
GetCommandString()
InvokeCommand()
QueryContextMenu()
它们分别恢复菜单项的描述,响应点击操作和添加新命令到菜单。
新项的帮助文字
GetCommandString()有下面的原型:
HRESULT GetCommandString(UINT idCmd, // 需要描述的菜单命令ID
UINT uFlags, // 指定要做什么的标志
UINT* pwReserved, // 保留,总是NULL
LPSTR pszName, // 接收要恢复串的缓冲(最大 40)
UINT cchMax); //接收串的实际长度
GetCommandString()函数的uFlags可用的取值是:
标志 |
描述 |
GCS_HELPTEXT |
Shell要求项的描述串 |
GCS_VALIDATE |
Shell简单地想要知道是否具有这个ID的项存在和有效 |
GCS_VERB |
Shell要求这个菜单项动词的语言无关的名 |
动词是实施命令的名字(我们在前面章节中已经解释过了,特别在第8章)。动词可通过ShellExecute()和ShellExecuteEx()函数执行。在通过注册表静态添加新的菜单项时,建立的键名就是语言无关的动词,其后的命令则隐藏在‘Command’子键下。在动态添加菜单项时,你应该实现InvokeCommand()来提供类似‘Command’键的行为,并且适当地响应GCS_VERB标志令Shell知道新命令的动词。
注意,你传递的任何帮助文字都将在40字符之后返回,尽管传递了较长的串,也不要截断除了串本身之外的任何东西。
新项的行为
InvokeCommand()是在用户点击关联菜单项时被调用的方法。其原型为:
HRESULT InvokeCommand(LPCMINVOKECOMMANDINFO lpici);
CMINVOKECOMMANDINFO结构声明如下:
typedef struct _CMINVOKECOMMANDINFO
{
DWORD cbSize;
DWORD fMask;
HWND hwnd;
LPCSTR lpVerb;
LPCSTR lpParameters;
LPCSTR lpDirectory;
INT nShow;
DWORD dwHotKey;
HANDLE hIcon;
} CMINVOKECOMMANDINFO, *LPCMINVOKECOMMANDINFO;
让我们更详细地讨论这个结构:
成员 |
描述 |
cbSize |
这个结构的尺寸 |
fMask |
允许dwHotkey和hIcon成员,和防止任何UI活动的屏蔽位,就象消息框的标志一样。 |
hwnd |
菜单的父窗口 |
lpVerb |
一个命令ID给出的DWORD类型值(高字为0),或表示要执行动词的串 |
lpParameters |
如果接口从Shell调用,总是NULL |
lpDirectory |
如果接口从Shell调用,总是NULL |
nShow |
如果启动新应用,这是一个传递给ShowWindow()的 SW_ 型常量。 |
dwHotKey |
由命令分配给应用启动的热键。如果fMask关闭了它的特定位,这个热键不必考虑。 |
hIcon. |
由命令分配给启动应用的图标,如果fMask关闭了它的特定位,这个图标不必考虑。 |
fMask的合法值如下:
值 |
描述 |
CMIC_MASK_HOTKEY |
dwHotKey成员是可用的 |
CMIC_MASK_ICON |
hIcon成员是可用的 |
CMIC_MASK_FLAG_NO_UI |
没有可以影响用户界面的活动发生(例如,建立窗口或消息框) |
lpVerb成员是一个32位值,有两种方法确定其内容,它可以是调用
lpVerb = MAKEINTRESOURCE(idCmd, 0);
的结果。这里idCmd是菜单项的ID,而lpVerb也可以表示要执行动词的名字。此时,高字不为0,这个值实际指向一个串。
与其它Shell相关的接口类似,IContextMenu也可以从Shell之外调用,不用响应在Shell元素上的UI活动。例如,当你获得了IShellFolder指针后,就可以请求绑定在这个文件夹或文件对象上的IContextMenu接口。然后就可以使用IContextMenu编程唤醒动词,而不需要通过Shell。此时的lpParameters和lpDirectory可能不是NULL。
此外,你还可以使用ShellExecuteEx()来调用Shell扩展动态添加的动词。此时可以通过这个接口函数指定附加的参数和工作目录,这就是最终所填写的lpParameters和lpDirectory变量。(参见第8章)
添加新项
在建立给定文件对象的关联菜单时,Shell通过调用QueryContextMenu()查询所有注册的关联菜单Shell扩展来添加扩展所拥有的项。这个函数的原型是:
HRESULT QueryContextMenu(HMENU hmenu, // 要添加项的菜单Handle
UINT indexMenu, // 被添加的第一项的索引(从0开始)
UINT idCmdFirst, // 新项的最低可用命令ID
UINT idCmdLast, // 新项的最高可用命令ID
UINT uFlags); // 影响关联菜单的属性
在添加新菜单项时,Shell指示头一个添加项的位置,以及命令ID的取值范围。下面一小段代码显示了典型的通过QueryContextMenu()插入新项的方法:
idCmd = idCmdFirst;
lstrcpy(szItem, ...);
InsertMenu(hMenu, indexMenu++, MF_STRING | MF_BYPOSITION, idCmd++, szItem);
在所有uFlags变量可用的标志中,我们所困扰的是CMF_NORMAL和CMF_DEFAULTONLY。其它的对于‘简单’的Shell扩展是没有意义的,而主要是应用于命名空间扩展。下面是这些值的完整列表:
标志 |
描述 |
CMF_CANRENAME |
如果设置,命名空间扩展应该添加一个‘重命名’项 |
CMF_DEFAULTONLY |
用户双击,命名空间扩展可以添加它为默认项。Shell扩展不应该做任何事情,事实上如果这个标志设置,应该避免添加项。 |
CMF_EXPLORE |
当探测器打开树窗口时设置此标志 |
CMF_INCLUDESTATIC |
Shell扩展不顾此标志 |
CMF_NODEFAULT |
菜单不应该有默认项,Shell扩展忽略这个标志,但命名空间扩展应该避免定义默认项 |
CMF_NORMAL |
非特殊情况,Shell扩展可以添加它们的项。 |
CMF_NOVERBS |
Shell扩展忽略这个标志。它用于‘发送到’菜单。 |
CMF_VERBSONLY |
Shell扩展忽略这个标志。它用于快捷方式对象的菜单 |
你肯定很奇怪Shell扩展为什么忽略在命名空间扩展中有用的标志,或忽略应用于特定菜单如‘发送到’和快捷方式菜单的标志。IContextMenu不是一个Shell扩展接口吗?
实际上,答案是否定的,IContextMenu是提供关联菜单功能的通用COM接口。几乎所有的系统菜单都可以通过在注册表的适当位置注册关联菜单处理器来扩展——Shell加载它,因而提供添加和管理客户菜单项的可能性。IContextMenu可用于在探测器窗口以外工作,我们在后面将给出这方面的例子。命名空间扩展是一个定制的Shell观察,可以直接调用提供的关联菜单到用户,因此IContextMenu也影响命名空间扩展。
QueryContextMenu()的返回值
与其它COM 函数一样,QueryContextMenu()返回HRESULT值。在很多情况下,你可以使用预定义常量,偶尔,需要格式化特定的返回值。QueryContextMenu()就是需要这样做的函数之一。我们都知道HRESULT是32位值,其位被分成三部分:严格(severity),简易(facility)和代码(code)。QueryContextMenu()要求你返回代码到特定值,和0。特别是,你应该返回添加的菜单项数。要格式化HRESULT,MAKE_HRESULT()宏是极为有用的:
return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, idCmd - idCmdList);
可执行程序的相关列表
现在我们把学过的关于关联菜单的所有技术都串联在一起做一个练习。在操作探测器时你可能会遇到成百上千的可执行程序,是否有人能告诉你这些程序引用了什么库呢?程序有一个静态引用的模块列表,它称之为相关列表。
通过扫描Win32 可执行程序的二进制格式(假设对Win32简携可执行格式有很好的理解),就有可能抽取出一个应用所需要的所有DLL名。在这个例子中,我们打算实现一个工具,作为关联菜单对EXE和DLL文件查看它们的相关列表。
在开始之前,我们要说明几件事,首先,这个工具不需要运行应用——这将限制对其检查字节。其次,它仅能恢复那些在代码中显式引入的DLL。这是因为仅静态连接到工程中的DLL在代码中留有标记,如果程序通过LoadLibrary()动态装入DLL,这个DLL不在引入表中引用,我们就不能跟踪它。
建立关联菜单的扩展
我们并不打算就获取Win32可执行程序相关列表给出方方面面的细节说明,因为这是一个十分复杂的科目并且超出了本书的范围。如果你感兴趣,请参考相关的MSDN资料。在这个例子中,我们使用相对新的DLL,其名字为ImageHlp。这个库并不输出特殊的函数来获得文件名,而是通过使用其中的一个例程,来完成这些操作。
开始,使用ATL COM应用大师建立DLL工程(project),取名为Depends,加入一个新的简单对象ExeMenu,接受所有默认的选项。这是一个实现关联菜单Shell扩展所要求接口的对象:IContextMenu和IShellExtInit。下面是我们需要对ExeMenu.h主头文件所作的改变:
#include "resource.h" // 主符号
#include "IContextMenuImpl.h" // IContextMenu
#include "IShellExtInitImpl.h" // IShellExtInit
#include "DepListView.h" // 对话框
#include
//
// CCExeMenu
class ATL_NO_VTABLE CExeMenu :public CComObjectRootEx
public CComCoClass
public IShellExtInitImpl,
public IContextMenuImpl,
public IDispatchImpl
{
public:
CExeMenu()
{
}
TCHAR m_szFile[MAX_PATH]; // 可执行文件名
CDepListView m_Dlg; // 显示结果的对话框
// IContextMenu
STDMETHOD(GetCommandString)(UINT, UINT, UINT*, LPSTR, UINT);
STDMETHOD(InvokeCommand)(LPCMINVOKECOMMANDINFO);
STDMETHOD(QueryContextMenu)(HMENU, UINT, UINT , UINT, UINT);
// IShellExtInit
STDMETHOD(Initialize)(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
DECLARE_REGISTRY_RESOURCEID(IDR_EXEMENU)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CExeMenu)
COM_INTERFACE_ENTRY(IExeMenu)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IShellExtInit)
COM_INTERFACE_ENTRY(IContextMenu)
END_COM_MAP()
// IExeMenu
public:
};
CExeMenu类从IShellExtInitImpl和IContextMenuImpl两个ATL类中导出,它提供IShellExtInit 和 IContextMenu接口的基本实现。IShellExtInitImpl.h头文件与我们在前一个例子中使用的一样,而IContextMenuImpl.h头文件有如下形式:
// IContextMenuImpl.h
#include
#include
class ATL_NO_VTABLE IContextMenuImpl : public IContextMenu
{
public:
// 数据
TCHAR m_szFile[MAX_PATH];
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0;
_ATL_DEBUG_ADDREF_RELEASE_IMPL(IContextMenuImpl)
// IContextMenu
STDMETHOD(GetCommandString)(UINT, UINT, UINT*, LPSTR, UINT)
{
return S_FALSE;
}
STDMETHOD(InvokeCommand)(LPCMINVOKECOMMANDINFO)
{
return S_FALSE;
}
STDMETHOD(QueryContextMenu)(HMENU, UINT, UINT , UINT, UINT)
{
return S_FALSE;
}
};
退一步说,这里的是最小实现。在其它情况下,你可能需要准备更有效的类,并增强代码可重用的质量,然而,对于我们的例子,这段代码足够了。剩下的就是要提供两个接口全部函数的代码,它们都包含在ExeMenu.cpp中:
// QueryContextMenu
HRESULT CExeMenu::QueryContextMenu(HMENU hmenu, UINT indexMenu, UINT idCmdFirst,
UINT idCmdLast, UINT uFlags)
{
// 这个Shell扩展打算在EXE文件的关联菜单上提供相关列表
UINT idCmd = idCmdFirst;
// 添加新菜单项
InsertMenu(hmenu, indexMenu++, MF_STRING | MF_BYPOSITION,idCmd++,
__TEXT("Dependency &List"));
return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, idCmd - idCmdFirst);
}
// InvokeCommand
HRESULT CExeMenu::InvokeCommand(LPCMINVOKECOMMANDINFO lpcmi)
{
// 建立模式对话框显示信息
lstrcpy(m_Dlg.m_szFile, m_szFile);
m_Dlg.DoModal();
return S_OK;
}
// 取得命令串
HRESULT CExeMenu::GetCommandString(UINT idCmd, UINT uFlags, UINT* pwReserved,
LPSTR pszText, UINT cchMax)
{
// 我们不关心命令ID,因为我们只有单个项
if(uFlags & GCS_HELPTEXT)
lstrcpyn(pszText, __TEXT("显示模块需要的所有DLL"), cchMax);
return S_OK;
}
// Initialize
HRESULT CExeMenu::Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT lpdobj,
HKEY hKeyProgID)
{
if(lpdobj == NULL)
return E_INVALIDARG;
// 取得 CF_HDROP
STGMEDIUM medium;
FORMATETC fe = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
HRESULT hr = lpdobj->GetData(&fe, &medium);
if(FAILED(hr))
return E_INVALIDARG;
// 取得选中文件名
DragQueryFile(reinterpret_cast
ReleaseStgMedium(&medium);
return hr;
};
应该看到,Initialize()代码与前面属性页例子中的初始化代码基本一致。
初始化关联菜单扩展
前面我们说过,Initialize()的参数对不同类型的Shell扩展是不同的。对于关联菜单扩展,pidlFolder变量是文件夹的PIDL,它包含选中的文件对象。这些文件对象由lpdobj通过IDataObject接口指向,IDataObject接口我们在上一个例子中遇到过。hKeyProgID参数指定了选中文件对象的文件类,而且,如果选中了多个对象,它指向有焦点的一个。
获取可执行的关联链表
这个扩展的目的是当用户点击‘相关列表’菜单项时:
Shell将调用InvokeCommand()方法导出对话框。在这个截图中注意状态条中的显示文字,这是我们通过GetCommandString()函数提供的串。我们使用ATL对象大师添加一个对话框命名为DepListView,并且加入一个公共数据成员m_szFile来保存文件名:
enum {IDD = IDD_DEPLISTVIEW};
TCHAR m_szFile[MAX_PATH];
对话框的初始化在其OnInitDialog()方法中发生,这要求包含shlobj.h 和 windowsx.h到DepListView.h的顶部:
LRESULT CDepListView::OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam,
BOOL& bHandled)
{
// 准备列表观察,使用前面章中定义的函数
HWND hwndList = GetDlgItem(IDC_LIST);
LPTSTR pszCols[] = {__TEXT("Library"), reinterpret_cast
__TEXT("Version"), reinterpret_cast
MakeReportView(hwndList, pszCols, 2);
// 使用省略号设置文件名,如果它太长的话
TCHAR szTemp[60] = {0};
PathCompactPathEx(szTemp, m_szFile, 60, '//');
SetDlgItemText(IDC_FILENAME, szTemp);
// 获得引入表的尺寸
int iNumOfBytes = GetImportTableSize(m_szFile);
if(iNumOfBytes <= 0)
return 0;
// 取得COM分配器 并保留一些内存
LPMALLOC pM = NULL;
SHGetMalloc(&pM);
LPTSTR psz = static_cast
if(psz == NULL)
{
::MessageBox(0, __TEXT("没有足够的内存!"), 0, MB_ICONSTOP);
pM->Release();
return 0;
}
ZeroMemory(psz, iNumOfBytes);
// 访问引入表
int iNumOfLibs = GetImportTable(m_szFile, psz);
if(iNumOfLibs <= 0)
{
pM->Release();
return 0;
}
int i = 0;
while(i < iNumOfLibs)
{
// p 为列表观察格式化NULL分割的串
TCHAR buf[2048] = {0};
LPTSTR p = buf;
lstrcpy(p, psz);
lstrcat(p, __TEXT("/0"));
p += lstrlen(p) + 1;
// 取得版本信息
TCHAR szInfo[30] = {0};
SHGetVersionOfFile(psz, szInfo, NULL, 0);
lstrcpy(p, szInfo);
lstrcat(p, __TEXT("/0"));
p += lstrlen(p) + 1;
// 添加传到列表观察
AddStringToReportView(hwndList, buf, 2);
// 下一个库
psz += lstrlen(psz) + 1;
i++;
}
pM->Release();
return 1;
}
首先我们通过添加两个列来格式化报告列表观察,一为文件名,一为版本号。第二,我们读出执行模块的引入表,并格式化一个NULL分隔的串。为了处理这个对话框,我们重用了一些函数——MakeReportView()和AddStringToReportView(),以及SHGetVersionOfFile()函数。下图显示了最后的对话框:
这个对话框由表示为IDC_LIST的报告列表和命名为IDC_FILENAME的文字标签组成。还要注意,我们使用了shlwapi.dll中的PathCompactPathEx()函数来强迫文件名到固定的字符数——当文件名太长时自动插入省略号来截断它。
我们前面说过不打算深入讨论获取相关列表的技术,但是这个过程有几件事是需要提到的。ImageHlp API是在Windows9x和NT下可用的,它提供在可执行模块产生的内存映像上操作的函数。还有些函数遍历符号表把它映射进内存。(参见MSDN资料库)。
特别值得注意的函数是BindImageEx(),它允许你获取可执行模块从外部库引入的任何函数的虚地址。从我们的观点看,这个函数接受一个回调例程,并且传递每一个它遇到的DLL名到这个例程。通过钩住这些调用,我们能够很容易地计算出需要多少字节来存储整个名字列表(GetImportTableSize()),并且把所有名字都变成NULL分隔的串(GetImportTable())。
我们打算用一个简单的DLL来提供这些函数,头文件为DepList.h,应该在顶部包含#include DepListView.h:
#include
#include
// 返回指定名DLL的字节数
int APIENTRY GetImportTableSize(LPCTSTR pszFileName);
// 用DLL名充填指定的缓冲
int APIENTRY GetImportTable(LPCTSTR pszFileName, LPTSTR pszBuf);
较大的源代码是DepList.cpp:
#pragma comment(lib, "imagehlp.lib")
#include "DepList.h"
/*----------------------------------------------------------------*/
// GLOBAL 节
/*----------------------------------------------------------------*/
// 数据
LPTSTR* g_ppszBuf = NULL;
int g_iNumOfBytes = 0;
int g_iNumOfDLLs = 0;
// 回调
BOOL CALLBACK SizeOfDLLs(IMAGEHLP_STATUS_REASON, LPSTR, LPSTR, ULONG, ULONG);
BOOL CALLBACK GetDLLs(IMAGEHLP_STATUS_REASON, LPSTR, LPSTR, ULONG, ULONG);
/*----------------------------------------------------------------*/
// 过程:GetImportTableSize()
/*----------------------------------------------------------------*/
int APIENTRY GetImportTableSize(LPCTSTR pszFileName)
{
g_iNumOfBytes = 0;
// 绑定到可执行
BindImageEx(BIND_NO_BOUND_IMPORTS | BIND_NO_UPDATE,
const_cast
return g_iNumOfBytes;
}
BindImageEx()的原型是:
BOOL BindImageEx(DWORD dwFlags,
LPSTR pszFileName,
LPSTR pszFilePath,
LPSTR pszSymbolPath,
PIMAGEHLP_STATUS_ROUTINE pfnStatusProc);
你必须在pszFileName中指定要操作的文件名,并且可能包含路径。如果不包含路径,可以使用pszFilePath来指定搜索pszFileName的根路径。更重要的是,这个函数回调pfnStatusProc中的例程,这个例程在函数绑定到指定可执行模块期间被唤醒,下面是回调的原型:
BOOL CALLBACK BindStatusProc(IMAGEHLP_STATUS_REASON Reason,
LPSTR ImageName,
LPSTR DllName,
ULONG Va,
ULONG Parameter);
我们唯一感兴趣的参数是Reason 和 DllName。第二个参数的目的是显然的,而第一个参数令你过滤对这个函数的众多调用,使之专注于实际感兴趣的。我们仅想知道需要多少字节来存储所有模块的引用,以及它们是哪些模块。SizeOfDLLs()是返回文件引入表尺寸的回调函数,GetDLLs()是通过调用BindImageEx()连接到所有绑定模块名而获得返回NULL分隔串的函数。这个串与版本信息组合产生输出显示。
/*----------------------------------------------------------------*/
// 过程: GetImportTable
/*----------------------------------------------------------------*/
int APIENTRY GetImportTable(LPCTSTR pszFileName, LPTSTR pszBuf)
{
g_ppszBuf = &pszBuf;
g_iNumOfDLLs = 0;
// 绑定到可执行
BindImageEx(BIND_NO_BOUND_IMPORTS | BIND_NO_UPDATE,
const_cast
return g_iNumOfDLLs;
}
/*----------------------------------------------------------------*/
// 过程: SizeOfDLLs()
// Description.: 计算DLL尺寸的回调
/*----------------------------------------------------------------*/
BOOL CALLBACK SizeOfDLLs(IMAGEHLP_STATUS_REASON Reason,
LPSTR ImageName, LPSTR DllName, ULONG Va, ULONG Parameter)
{
if(Reason == BindImportModule || Reason == BindImportModuleFailed)
g_iNumOfBytes += lstrlen(DllName) + 1;
return TRUE;
}
/*----------------------------------------------------------------*/
// 过程: GetDLLs()
// Description.: 封装串的回调
/*----------------------------------------------------------------*/
BOOL CALLBACK GetDLLs(IMAGEHLP_STATUS_REASON Reason, LPSTR ImageName,
LPSTR DllName, ULONG Va, ULONG Parameter)
{
if(Reason == BindImportModule || Reason == BindImportModuleFailed)
{
lstrcpy(*g_ppszBuf, DllName);
*g_ppszBuf += lstrlen(*g_ppszBuf) + 1;
g_iNumOfDLLs++;
}
return TRUE;
}
最后,这些函数由 DepList.def 文件输出:
EXPORTS
GetImportTableSize @1
GetImportTable @2
现在,你可以编译我们给出的所有代码了。
注册扩展
这个清单显示了需要添加到ATL脚本ExeMenu.rgs末尾的修改代码,以便注册我们的Shell扩展。
Exefile
{
Shellex
{
ContextMenuHandlers
{
{20349851 -699F -11D2-9DAF-00104B 4C 822A }
}
}
}
Dllfile
{
Shellex
{
ContextMenuHandlers
{
{20349851 -699F -11D2-9DAF-00104B 4C 822A }
}
}
}
}
改变之后,在下一次启动Shell时,你将发现由右键在EXE和DLL文件上生成的关联菜单有一个新的‘相关列表’项。这是我们的Shell扩展给出的。
添加新查找菜单
产生关联菜单扩展的另一个值得注意的用途是定制显示‘查找’菜单的列表,例如,我们可以添加查找所有当前运行中进程的工具。
倘若我们已经有了一个添加了新的‘查找’实用程序的关联菜单,则要做的只是写几个注册表信息段:
在你所看到的静态键下,需要添加新键FindProcess,并使之成为根的新子键。这个键的默认值必须是一个关联菜单扩展的CLSID。在它的下面,键名为0 的默认值是显示在菜单上的串。最后通过添加0键的DefaultIcon子键,可以为这个菜单项分配图标。
稍微思考一下,我们将看到这是一个陌生的而且是最小的Shell扩展。不需要任何初始化,因为没有要操作的文件。不需要描述,因为没有状态条,甚至不需要显式添加新项,因为Shell在读注册表时作了这个工作。事实上我们需要Shell扩展来定制‘查找’菜单一点也不神秘。
因为‘查找’菜单也是通过探测器导出的,你可能以为描述是必须的,但是经过快速测试已经存在的菜单项后,我们知道,不是这样。建立关联菜单Shell扩展的复杂性减少到仅仅实现InvokeCommand()方法,这是一个导出实际运行查找实用程序的函数。
设置注册表
编写关联菜单的Shell扩展作为新的‘查找’实用程序工作只需非常小的努力,就象下面代码说明的一样。这里是在ExeMenu.h实现的四个接口方法需要作一点工作:
// QueryContextMenu
HRESULT CProcess::QueryContextMenu(HMENU hmenu, UINT indexMenu, UINT idCmdFirst,
UINT idCmdLast, UINT uFlags)
{
return S_OK;
}
// InvokeCommand
HRESULT CProcess::InvokeCommand(LPCMINVOKECOMMANDINFO lpcmi)
{
m_Dlg.DoModal();
return S_OK;
}
// GetCommandString
HRESULT CProcess::GetCommandString(UINT idCmd, UINT uFlags, UINT* pwReserved,
LPSTR pszText, UINT cchMax)
{
return S_OK;
}
// Initialize
HRESULT CProcess::Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT lpdobj,
HKEY hKeyProgID)
{
return S_OK;
};
有点复杂的是构造注册表的脚本。注意下面的扩展,不要置换出时的脚本。这段代码应该加到ATL给出的RGS末尾。
HKLM
{
Software
{
Microsoft
{
Windows
{
CurrentVersion
{
Explorer
{
FindExtensions
{
Static
{
FindProcess = s '{977DA8D2-41D5-11D2-BC00-AC 6805C 10E27}'
{
0 = s 'Find &Process...'
{
DefaultIcon = s '%MODULE%,0'
}
}
}
}
}
}
}
}
}
}
查找运行中的进程
枚举运行进程在Windows9x和NT下要求不同的技术——前者在ToolHelp.dll中提供了有价值的函数集,而后者没有。在NT下,你必须借助于另一个有相当差别的库PSAPI.dll。这个库与NT4.0一同发布,但并不总是拷贝到你的硬盘上,不过在VC++ 的CD上你将能找到两个文件psapi.h 和 psapi.lib。
我们不打算细说这个过程,因为它超出了本书的范围,你可以参考MSDN知识库的文章。
IContextMenu2 和 IContextMenu3接口
在IE4.0中增加了两个关联菜单的接口,二者都是在IContextMenu上进行的改进。更精确地讲,IContextMenu2是对IContextMenu的扩充,而IContextMenu3(要求IE4.0)是对IContextMenu2的增强。然而,这两个接口仅仅比IContextMenu多了一个函数。这个额外的函数在IContextMenu2中为HandleMenuMsg(),而在IContextMenu3中反而为HandleMenuMsg2(),这就使人更容易混淆了。其原型类似于:
HRESULT HandleMenuMsg(UINT uMsg,
WPARAM wParam,
LPARAM lParam);
HRESULT HandleMenuMsg2(UINT uMsg,
WPARAM wParam,
LPARAM lParam,
LRESULT* plResult);
这两个接口通过提供自绘制(位图)关联菜单,对IContextMenu进行了扩充。尤其,HandleMenuMsg()可以解释和处理三个系统消息:
WM_INITMENUPOPUP
WM_MEASUREITEM
WM_DRAWITEM
后两个消息仅在有自绘制菜单项时才起作用。对此,HandleMenuMsg2()增加了第四个消息:WM_MENUCHAR。这个科目的资料可以在Internet客户端SDK中找到。
右键拖拽
Windows Shell提供了从一个目录拖拽文件到另一目录的可能性,但是如果你使用鼠标右键执行这个操作时,这个行为就被修改了:有一个菜单来提示你。这并不是最有用的Windows特征,但是它允许你决定在拖拽文件对象集之后要做什么:
象图中显示的那样,Windows提供了一种典型的操作菜单。同时还考虑到作为活动结果,什么操作是正确的——例如,如果你在同一个源文件夹内拖拽,就没有‘Move Here’菜单项。因此右键拖拽不支持键盘修改器操作,如Ctrl 或 Shift按键,它们允许快速改变操作结果。所有可用的操作都在最终的菜单上列出。
你也可以在此添加客户项——一个普通的关联菜单扩展就够了。然而,即使拖拽处理器和关联菜单处理器在编程上看是同一个东西,但是在注册时它们还是有相当的差别。
注册拖拽处理器
右键拖拽处理器并不是在基本文件类型上工作,因此,你不能安装它来单独处理如ZIP这样文件。它们仅仅应用于目录,下面是一个典型的注册脚本,其中我们注册右键拖拽处理器在目录内容上工作。
HKCR
{
Directory
{
Shellex
{
DragDropHandlers
{
RightDropDemo = s '{20349851 -699F -11D2-9DAF-00104B 4C 822A }'
}
}
}
}
头一件要注意的是,你的注册表条目是在DragDropHandlers键下,不是ContextMenuHandlers。进一步,你需要建立特殊的子键并设置默认值为接口的CLSID。子键的名字并不重要。探测器将枚举全部DragDropHandlers树的内容。
通常这个扩展的头一个被调用的方法是IShellExtInit::Initialize(),在这里你可以检查选中文件的类型。输入的变量分别给出用户拖拽的目标文件夹的PIDL,数据对象(以此可以恢复被操作文件),和包含具有焦点文件的文件类型信息的注册键。
通过检查文件扩展名,你可以避免对不希望或不必要的文件进行操作。这完全不同于我们前面所作的。对于拖拽处理器,在同一棵树上注册所有Shell扩展,以及在初始化期间就可以决定是否对选中的文件感兴趣。要终止这个Shell扩展,只需要简单地从Initialize()返回E_FAIL即可。下面是一个例子,其中我们假设一个类CDropExt实现了IContextMenu 和 IShellExtInit接口:
STDMETHODIMP CDropExt::Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT lpdobj,
HKEY hkeyProgID)
{
FORMATETC fe = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
STGMEDIUM medium;
HRESULT hr = lpdobj->GetData(&fe, &medium);
if(FAILED(hr))
return E_FAIL;
TCHAR szFile[MAX_PATH] = {0};
HDROP hdrop = static_cast
// 取得拖拽的文件数
UINT cFiles = DragQueryFile(hdrop, 0xFFFFFFFF, NULL, 0);
// 依次处理文件
for(int i = 0 ; i < cFiles ; i++)
{
// 取得第i个文件名
DragQueryFile(hdrop, i, szFile, MAX_PATH);
// 检查扩展名和返回 E_FAIL 来终止
}
return S_OK;
}
在上面代码中,我们扫视了拖拽文件列表(通过IDataObject获得的),依次取得每一个文件的名字,并且检查其扩展名以决定它是否是所支持的类型。
倘若右鼠标键执行了操作,右键拖拽处理器是在拖拽操作的源文件上工作的。这不同于DropHandler示例,它应用于拖拽活动的目标。
如果你查看一下你的PC注册表内容,就会发现,没有象关联菜单处理那样对给定文件类型注册的拖拽扩展。WinZip这个少有的实用程序以这种方式工作:当你右键拖拽文件时,它的扩展总是在后台工作,仅在你拖拽了ZIP文件后它才弹出。
指派动态图标
我们到目前为止讨论的属性页和关联菜单是两个具有挑战性的通用Shell扩展应用,但它们并不是仅有的。这一节我们将介绍动态图标。即,讨论给定同文件类型的不同文件以不同的图标。
考虑EXE文件,每当在Shell观察中遇到它们时,所显示的图标都不是那种文件类型的一般图标,而是属于文件本身的图标(当然,除非这个EXE不包含图标)。甚至对ICO文件也是如此。
事实上这是自Windows95以来的Shell特征,所以很有可能你从未过多地考虑过它。然而动态指派图标到一定类型的文件是Shell通过Shell扩展提供的确切行为。我们下面就介绍一个例子,它向你展示怎样应用这个技术到BMP文件。这里展示的并不是对任何位图的16x16像素图片的预览——压缩800x600真彩图象到小图标是一项痛苦的活动。我们所要做的是在视野中使用图标来提供位图的信息,以及怎样使不同的图标来适应BMP文件的调色板。
不同颜色深度的图标
基本上,我们打算区别四种情况,并指派不同的图标:
单色位图
16色(4-位t)
256色(8-位)
真彩色位图(24-位或更大)
想法是定义IconHandlerShell扩展(并放置到注册表键),使它来检查每一个位图文件的色彩表,以便返回正确的图标到探测器显示。IconHandlerShell扩展要求实现下面的COM接口:
IExtractIcon
IPersistFile
头一个是在模块与探测器之间进行通讯的工具。换句话说,探测器将调用IExtractIcon的方法来请求通过IPersistFile接口装入的文件要显示的图标。
注意,由于这个扩展不仅应用于选中文件,而且是任何文件,因此初始化是由IPersistFile而不是IShellExtInit执行的。
初始化图标处理器扩展
IPersistFile接口在IUnknown之上由六个函数组成,其原形如下:
HRESULT GetClassID(LPCLSID lpClsID);
HRESULT IsDirty();
HRESULT Load(LPCOLESTR pszFileName, DWORD dwMode);
HRESULT Save(LPCOLESTR pszFileName, BOOL fRemember);
HRESULT SaveCompleted(LPCOLESTR pszFileName);
HRESULT GetCurFile(LPOLESTR* ppszFileName);
因为我们知道这个Shell扩展的目的,因此并不需要实现所有这些方法。事实上,只Load()方法就足够了,其它方法,我们将只返回E_NOTIMPL。Load()方法存储需要图标的位图文件名,所以我们所要做的是转换Unicode文件名到ANSI串,并把它存储到要进一步使用的数据成员中。
恢复图标
探测器取得显示图标有两种可能的方法,而每一种方法都通过IExtractIcon传递,它们是:
GetIconLocation()
Extract()
头一个返回要使用的图标路径和索引,使用一些标志来向Shell说明怎样处理它。相反,探测器调用第二种方法以给这个扩展一个机会来抽取图标本身。现在让我们从GetIconLocation()开始更详细地说明一下:
HRESULT GetIconLocation(UINT uFlags, // 需要图标的理由
LPSTR szIconFile, // 含有图标路径名的缓冲
INT cchMax, // 缓冲尺寸
LPINT piIndex, // 包含图标索引的整数指针
UINT* pwFlags); // 发送关于图标的信息到Shell
uFlags对我们来讲并不是特别有用,但是如果操作文件夹或一般的文件而不是位图,它可能是有用的——其中,它可以使你知道是否要求一个‘打开’状态的图标。
另一个标志参数pwFlags,允许我们告诉Shell下面几点:
标志 |
描述 |
GIL_DONTCACHE |
防止探测器将图标存入其内部缓存 |
GIL_NOTFILENAME |
通过szIconFile和piIndex传递的信息内有封装为<路径,索引>对。 |
GIL_PERCLASS |
这个图标应该被用于这个类的任何文档。在我们的例子中这个标志没有使用,因为我们想要获得要求的图标。如果想要指派文件类的图标,微软推荐使用注册表(参见第14章) |
GIL_PERINSTANCE |
这个图标被指派给特定的文档。这个类中的每一个文档都有自己的图标。这正是我们想要的。 |
GIL_SIMULATEDOC |
这是建立文档所需要的图标 |
当探测器需要显示文件图标时,它首先查找注册的IconHandler扩展,如果找到,就通过调用IPersistFile::Load()函数使用给定的文件初始化这个模块。然后,它通过调用IExtractIcon::GetIconLocation()请求扩展提供图标的路径名和索引。探测器现在希望接收所有需要恢复图标的信息,如果GetIconLocation()失败,Shell继续在找到的下一个扩展上操作。GetIconLocation()成功,则返回S_OK,如果返回S_FALSE,Shell将使用在DefaultIcon注册表键下指定的默认图标。GetIconLocation()返回后探测器检查pwFlags变量。如果GIL_NOTFILENAME位打开,这说明扩展想要自己抽取图标。它就调用Extract()方法,并传递从szIconFile和
piIndex中接收来的信息。探测器希望从Extract()中接收一对HICONs为小图标和大图标,其定义是:
HRESULT Extract(LPCSTR pszFile, // 由GetIconLocation通过szIconFile返回的值
UINT nIconIndex, // 由GetIconLocation通过piIndex返回的值
HICON* phiconLarge, // 指向接收大图标 Handle 的 HICON 指针
HICON* phiconSmall, // 指向接收小图标 Handle 的HICON 指针
UINT nIconSize); // 期望的图标像素尺寸低字为大图标,高字为小图标
这个函数必须确保探测器获得文件的大小图标的Handle。更重要的,这个函数应该返回S_FALSE来防止探测器自己抽取图标。在绝大多数情况下你不需要实现Extract(),但是你应该指派它返回S_FALSE而不是E_NOTIMPL。
详细示例
为了说明这项技术,我们打算建立一个命名为BmpIcons的ATL DLL工程(project)。下图显示我们用于表示各种位图的图标,你当然可以在自己的实现中自由调换它们:
这四个图标分别表示单色,16色,256色,和真彩色。把它们作为资源加入到我们的工程中,命名为BmpMono.ico,Bmp16.ico,Bmp256.ico 和 Bmp24.ico。
然后添加一个简单的对象Icon 到工程中。所生成的CIcon类需要从IExtractIconImpl和 IPersistFileImpl导出,这两个ATL类提供了IExtractIcon和IPersistFile接口的基本实现:
// IExtractIconImpl.h
#include
#include
class ATL_NO_VTABLE IExtractIconImpl : public IExtractIcon
{
public:
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0;
_ATL_DEBUG_ADDREF_RELEASE_IMPL(IExtractIconImpl)
// IExtractIcon
STDMETHOD(Extract)(LPCSTR, UINT, HICON*, HICON*, UINT)
{
return S_FALSE;
}
STDMETHOD(GetIconLocation)(UINT, LPSTR, UINT, LPINT, UINT*)
{
return S_FALSE;
}
};
对于我们而言,Extract()这里定义的是完美的——我们并不需要在CIcon源码中重载它。反回来考虑IPersistFile接口,我们可以把所有东西都放在‘Impl’类中,以提高它的可重用性:
// IPersistFileImpl.h
#include
class ATL_NO_VTABLE IPersistFileImpl : public IPersistFile
{
public:
TCHAR m_szFile[MAX_PATH];
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0;
_ATL_DEBUG_ADDREF_RELEASE_IMPL(IPersistFileImpl)
// IPersistFile
STDMETHOD(GetClassID)(LPCLSID)
{
return E_NOTIMPL;
}
STDMETHOD(IsDirty)()
{
return E_NOTIMPL;
}
STDMETHOD(Load)(LPCOLESTR wszFile, DWORD /*dwMode*/)
{
USES_CONVERSION;
lstrcpy(m_szFile, OLE2T(wszFile));
return S_OK;
}
STDMETHOD(Save)(LPCOLESTR, BOOL)
{
return E_NOTIMPL;
}
STDMETHOD(SaveCompleted)(LPCOLESTR)
{
return E_NOTIMPL;
}
STDMETHOD(GetCurFile)(LPOLESTR*)
{
return E_NOTIMPL;
}
};
我们的Shell扩展声明如下:
#include "resource.h"
#include "IPersistFileImpl.h"
#include "IExtractIconImpl.h"
#include
//
// CIcon
class ATL_NO_VTABLE CIcon :public CComObjectRootEx
public CComCoClass
public IExtractIconImpl,
public IPersistFileImpl,
public IDispatchImpl
{
public:
CIcon()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_ICON)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CIcon)
COM_INTERFACE_ENTRY(IIcon)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IPersistFile)
COM_INTERFACE_ENTRY(IExtractIcon)
END_COM_MAP()
// IExtractIcon
STDMETHOD(GetIconLocation)(UINT, LPSTR, UINT, LPINT, UINT*);
// IIcon
public:
private:
int GetBitmapColorDepth();
};
现在我们只需给出GetIconLocation()函数块即可,这是图标处理器的核心函数。我们还添加了私有的辅助函数GetBitmapColorDepth()。
HRESULT CIcon::GetIconLocation(UINT uFlags, LPSTR szIconFile, UINT cchMax,
LPINT piIndex, UINT* pwFlags)
{
// 存储我们自己的图标
::GetModuleFileName(_Module.GetModuleInstance(), szIconFile, cchMax);
// 解析位图色彩表
int iBitCount = GetBitmapColorDepth();
if(iBitCount < 0)
return S_FALSE;
switch(iBitCount)
{
case 1:
*piIndex = 0; // 单色
break;
case 4:
*piIndex = 1; // 16 色
break;
case 8:
*piIndex = 2; // 256 色
break;
default:
*piIndex = 3; // 真彩色
}
*pwFlags |= GIL_PERINSTANCE | GIL_DONTCACHE;
return S_OK;
};
int CIcon::GetBitmapColorDepth()
{
// 读文件头
HFILE fh = _lopen(m_szFile, OF_READ);
if(fh == HFILE_ERROR)
return -1;
BITMAPFILEHEADER bf;
_lread(fh, &bf, sizeof(BITMAPFILEHEADER));
BITMAPINFOHEADER bi;
_lread(fh, &bi, sizeof(BITMAPINFOHEADER));
_lclose(fh);
// 返回
return bi.biBitCount;
};
到此这个Shell扩展的源码就完成了,但是我们还要考虑它的注册问题。与任何其它Shell扩展一样,如果在注册期间遗漏了某些东西,将不能使这个扩展正常工作。
注册图标处理器
图标处理器的Shell扩展与其它Shell扩展遵循一样的模式,然而它使用不同的键。此时我们需要在被唤醒文档类的ShellEx键下建立IconHandler键。对于位图(如果使用微软的‘图画’打开它们),其键为:
HKEY_CLASSES_ROOT
/Paint.Picture
/ShellEx
/IconHandler
然后把它的默认值设置为对象的CLSID,并且还应该设置DefaultIcon键到%1,以使探测器知道图标应该逐个文件确定。正常情况下,DefaultIcon键包含文件名和索引组成的逗号分隔的串。下面是ATL生成脚本的非标准部分:
HKCR
{
// 对象注册
Paint.Picture
{
DefaultIcon = s '%%1'
ShellEx
{
IconHandler = s '{A2B00480 -425A -11D2-BC00-AC 6805C 10E27}'
}
}
}
注意,DefaultIcon键所取的值%1需要两个百分号(%%)。
为了确保正常工作,最安全的办法就是重启机器或注销登录。注意老的DefaultIcon键值被覆盖,所以你应该把它保存在一个安全的地方。下图显示了你所看到的Shell是怎样由扩展所改变的:
同一个文件类不能有多个IconHandler扩展。如果注册了多个,仅第一个被考虑。
通过ICopyHook监视文件夹
许多程序员的梦想是能够编写实用程序来监视文件系统发生的事件。肯定有许多理由要这么做,但是测试应用,排错和满足好奇心也一定是其中的原因。
在第7章中我们讨论了通知对象,它通知你的应用在文件系统中或指定文件夹内某些东西发生了变化。不幸的是在Windows95和98下,没有办法知道那一个文件引起通知发生。换句话说,你知道了在被监视的文件夹下某些东西发生了变化,而后则完全要你来确切地描绘发生了什么。在NT下这个事情就稍微好了一点,这要感谢平台专用的函数ReadDirectoryChangesW()。
即使有一定数量的NT函数随Windows98一起输出到了Windows9x平台,ReadDirectoryChangesW()函数还是没在其中。带之的是 MoveFileEx(),CreateFiber()和CreateRemoteThread()在Windows98下是可用的。
Windows Shell的帮助说明一个称之为ICopyHook的接口,可以用于执行类似的操作。基本上,它能监视发生在文件夹内的拷贝,移动,重命名和删除操作。看上去确实够刺激的,但是很不幸,有三个严重的缺陷限制了这个扩展的使用:
它仅应用于文件夹和打印机,不能对文件类型
它仅能使你允许或禁止操作,不能自己执行它
它仅是你知道操作什么时候开始,不能知道它什么时候结束
作为另一个例子我们打算建立一个ATL工程(project)来说明怎样实现这个接口和建立一个目录监视工具。
实现ICopyHook接口
对于这个例子,我们使用ATL COM应用大师建立一个‘Copy’工程(project),接受所有默认的选项,生成之后,使用对象大师添加一个简单的ATL对象‘Monitor’,并对其头文件Monitor.h做一些修改:
#include "resource.h"
#include "ICopyHookImpl.h"
/
// CMonitor
class ATL_NO_VTABLE CMonitor :public CComObjectRootEx
public CComCoClass
public IShellCopyHookImpl,
public IDispatchImpl
{
public:
CMonitor()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_MONITOR)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CMonitor)
COM_INTERFACE_ENTRY(IMonitor)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY_IID(IID_IShellCopyHook, CMonitor)
END_COM_MAP()
// ICopyHook
public:
STDMETHOD_(UINT, CopyCallback)(HWND hwnd, UINT wFunc, UINT wFlags,
LPCSTR pszSrcFile, DWORD dwSrcAttribs,
LPCSTR pszDestFile, DWORD dwDestAttribs);
// IMonitor
public:
};
你可能已经注意到COM映射与我们前面的例子有点不同,这是因为新的COM_INTERFACE_ENTRY_IID()宏,我们过一会再讨论它。CMonitor类从IShellCopyHookImpl类中导出,依次继承于ICopyHook:
#include
#include
class ATL_NO_VTABLE IShellCopyHookImpl : public ICopyHook
{
public:
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0;
_ATL_DEBUG_ADDREF_RELEASE_IMPL(IShellCopyHookImpl)
// ICopyHook
STDMETHOD_(UINT, CopyCallback)(HWND hwnd, UINT wFunc, UINT wFlags,
LPCSTR pszSrcFile, DWORD dwSrcAttribs,
LPCSTR pszDestFile, DWORD dwDestAttribs);
};
在前面的例子中已经看到,绝大多数过程看上去都是非常相似的。ICopyHook接口要求你实现单一个函数CopyCallback(),这基本上是建立在SHFileOperation()之上的滤波函数(参见第3章)。它捕捉所有通过那个函数的操作,而你的实现可以允许或拒绝它们发生。CopyCallback()函数与SHFileOperation()函数十分相像是不奇怪的,
UINT CopyCallback(HWND hwnd, // 处理器显示所有窗口的父窗口
UINT wFunc, // 要执行的操作
UINT wFlags, // 操作属性
LPCSTR pszSrcFile, // 操作源文件
DWORD dwSrcAttribs, // 源文件的DOS属性
LPCSTR pszDestFile, // 操作的目标文件
DWORD dwDestAttribs); // 目标文件的DOS属性
CopyCallback()返回UINT值,它封装了典型的MessageBox()返回内容:IDYES, IDNO, IDCANCEL。操作是继续还是拒绝,或被取消依赖于这个返回值。拒绝的意思是只是这个操作不被执行,相反,取消则是所有相关操作都将被取消。
ICopyHook接口的IID
在开发我们的第一个CopyHook扩展期间,我们规定ICopyHook的接口ID为IID_ICopyHook,因此编译器在编译时有一个未声明标识符错,奇怪ICopyHook的IID不是IID_ICopyHook,而是IID_IShellCopyHook。
这实际引起ATL代码的一个问题,声明COM