第十五章 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<HDROP>(medium.hGlobal);
上面代码段说明怎样从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 <AtlCom.h>
#include <ShlObj.h>
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 <AtlCom.h>
#include <ShlObj.h>
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 <comdef.h> // 标准接口 GUIDs
#include "IShellExtInitImpl.h" // IShellExtInit
#include "IShellPropSheetExtImpl.h" // IShellPropSheetExt
BOOL CALLBACK PropPage_DlgProc(HWND, UINT, WPARAM, LPARAM);
////////////////////////////////////////////////////////////////////////////
// CPropPage
class ATL_NO_VTABLE CPropPage :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CPropPage, &CLSID_PropPage>,
public IShellExtInitImpl,
public IShellPropSheetExtImpl,
public IDispatchImpl<IPropPage, &IID_IPropPage, &LIBID_WMFPROPLib>
{
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<HDROP>(medium.hGlobal);
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<LPARAM>(g_szFile); // 为dlgproc定制数据
psp.pcRefParent = reinterpret_cast<UINT*>(&_Module.m_nLockCnt);
// 建立新页面
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<LPPROPSHEETPAGE>(lParam);
DisplayMetaFile(hwndMeta, reinterpret_cast<LPTSTR>(lppsp->lParam));
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-505850C10000}'
}
WMFProp.PropPage = s 'PropPage Class'
{
CLSID = s '{0D0E3558-8011-11D2-8CDB-505850C10000}'
CurVer = s 'WMFProp.PropPage.1'
}
NoRemove CLSID
{
ForceRemove {0D0E3558-8011-11D2-8CDB-505850C10000} = 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-505850C10000}'
}
}
WinMetafile
{
Shellex
{
PropertySheetHandlers
{
{0D0E3558-8011-11D2-8CDB-505850C10000}
}
}
}
EnhMetafile
{
Shellex
{
PropertySheetHandlers
{
{0D0E3558-8011-11D2-8CDB-505850C10000}
}
}
}
}
下图说明了注册增强元文件后的注册表状态。注意,其中有三个属性页的Shell扩展。如果你还有另一个增强元文件的Shell扩展——例如管理关联菜单——它们应该以同样的方法注册,但是是在另一个子键下。定位在与PropertySheetHandlers同层。
现在Shell扩展正确地注册了以后,你就能右击EMF或WMF文件,并且有下面的行为出现:
测试Shell扩展
到目前为止我们已经编写并注册了一个Shell扩展,现在我们来看一下它是否做了它应该做的工作。运行Shell扩展的唯一方法是启动探测器并执行引起Shell扩展动作的活动,但是要使探测器确信你的扩展存在可能是比较困难的。在一定场合下,你可能需要注销登录,甚至重启机器来使Shell加载更新版的扩展,相反,对比重启机器,简单地关闭探测器可能更好一点,而且可以使用任务条实用程序,我们在第9章中就是这么做的。还有就是按F5键,但这种方法不能总奏效。
参见这一章后面的Shell扩展开发者手册,其中有更详细的讨论
除了这些小困难之外,我们现在假设正在运行你的扩展。当你感觉到一个错误,并且需要排除代码找到错误发生点时复