第十五章 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扩展开发者手册,其中有更详细的讨论
除了这些小困难之外,我们现在假设正在运行你的扩展。当你感觉到一个错误,并且需要排除代码找到错误发生点时复杂的事情发生了。排除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 <AtlCom.h>
#include <ShlObj.h>
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<HDROP>(medium.hGlobal);
// 取得选中文件数
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<LPARAM>(psz);
psp.pcRefParent = reinterpret_cast<UINT*>(&_Module.m_nLockCnt);
HPROPSHEETPAGE hPage = ::CreatePropertySheetPage(&psp);
// 添加页面到属性页上
if(hPage != NULL)
if(!lpfnAddPage(hPage, lParam))
:: DestroyPropertySheetPage(hPage);
}
return NOERROR;
}
关于这个版本的AddPages()函数,有几点需要注意,首先,我们设置属性页的标题为没有路径和扩展名的文件名,这使用了一些来自shlwapi.dll的函数,因此#include <shlwapi.h> 和连接shlwapi.lib是必须的。第二,在清单中注释了引用要在对话框过程中删除的指针,这个指针是在循环中分配的,所以现在的PropPage_DlgProc()应该是:
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));
delete [] reinterpret_cast<LPTSTR>(lppsp->lParam);
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 <comdef.h> // 接口 IDs
//////////////////////////////////////////////////////////////////////////
// CCExeMenu
class ATL_NO_VTABLE CExeMenu :public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CExeMenu, &CLSID_CExeMenu>,
public IShellExtInitImpl,
public IContextMenuImpl,
public IDispatchImpl<IExeMenu, &IID_IExeMenu, &LIBID_DEPENDSLib>
{
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 <AtlCom.h>
#include <ShlObj.h>
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<HDROP>(medium.hGlobal), 0, m_szFile, MAX_PATH);
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<TCHAR*>(280),
__TEXT("Version"), reinterpret_cast<TCHAR*>(103)};
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<LPTSTR>(pM->Alloc(iNumOfBytes));
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 <windows.h>
#include <imagehlp.h>
// 返回指定名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<LPTSTR>(pszFileName), NULL, NULL, SizeOfDLLs);
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<LPTSTR>(pszFileName), NULL, NULL, GetDLLs);
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-00104B4C822A}
}
}
}
Dllfile
{
Shellex
{
ContextMenuHandlers
{
{20349851-699F-11D2-9DAF-00104B4C822A}
}
}
}
}
改变之后,在下一次启动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-AC6805C10E27}'
{
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-00104B4C822A}'
}
}
}
}
头一件要注意的是,你的注册表条目是在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<HDROP>(medium.hGlobal);
// 取得拖拽的文件数
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 <AtlCom.h>
#include <ShlObj.h>
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 <AtlCom.h>
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 <comdef.h>
//////////////////////////////////////////////////////////////////////////
// CIcon
class ATL_NO_VTABLE CIcon :public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CIcon, &CLSID_Icon>,
public IExtractIconImpl,
public IPersistFileImpl,
public IDispatchImpl<IIcon, &IID_IIcon, &LIBID_BMPICONSLib>
{
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-AC6805C10E27}'
}
}
}
注意,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<CComSingleThreadModel>,
public CComCoClass<CMonitor, &CLSID_Monitor>,
public IShellCopyHookImpl,
public IDispatchImpl<IMonitor, &IID_IMonitor, &LIBID_COPYLib>
{
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 <AtlCom.h>
#include <ShlObj.h>
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服务器对象的映射问题。在添加新的Monitor对象到ATL工程后,头文件的代码有如下形式:
BEGIN_COM_MAP(CMonitor)
COM_INTERFACE_ENTRY(IMonitor)
COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
COM映射对应于对象的QueryInterface()的实现,所以为了输出ICopyHook接口,以及参考其它例子在这一点所做的,我们像这样添加了以行代码:
BEGIN_COM_MAP(CMonitor)
COM_INTERFACE_ENTRY(IMonitor)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(ICopyHook)
END_COM_MAP()
我们说过这能引起编译错,为此我们必须通知ATL这个接口输出ICopyHook,但是它的IID不是IID_ICopyHook。幸运地是,ATL设计者已经预先清除了这个问题,有一个COM_INTERFACE_ENTRY宏确切地处理这种情况:
BEGIN_COM_MAP(CMonitor)
COM_INTERFACE_ENTRY(IMonitor)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY_IID(IID_IShellCopyHook, CMonitor)
END_COM_MAP()
这个宏告诉ATL使用第二个参数命名的类的虚表(vtable)作为第一个参数所表示的接口的实现。这正好是我们所需要的。
记录操作
我们打算在这里建立的扩展简单地组成和输出串到Log文件。这些串包含了源目文件名,操作类型,和发生的时间。下面是CopyCallback()的实现:
UINT CMonitor::CopyCallback(HWND hwnd, UINT wFunc, UINT wFlags,
LPCSTR pszSrcFile, DWORD dwSrcAttribs,
LPCSTR pszDestFile, DWORD dwDestAttribs)
{
TCHAR szTime[50] = {0};
GetTimeFormat(LOCALE_SYSTEM_DEFAULT, 0, NULL, NULL, szTime, 50);
FILE* f = NULL;
f = fopen(__TEXT("c://monitor.log"), __TEXT("a + t"));
fseek(f, 0, SEEK_END);
switch(wFunc)
{
case FO_MOVE:
fprintf(f, __TEXT("/n/n/rMoving:/n/r[%s] to [%s]/n/rat %s"),
pszSrcFile, pszDestFile, szTime);
break;
case FO_COPY:
fprintf(f, __TEXT("/n/n/rCopying:/n/r[%s] to [%s]/n/rat %s"),
pszSrcFile, pszDestFile, szTime);
break;
case FO_DELETE:
fprintf(f, __TEXT("/n/n/rDeleting:/n/r[%s] /n/rat %s"),
pszSrcFile, szTime);
break;
}
fclose(f);
// 不妨碍正常的控制流
return IDYES;
}
注册CopyHook扩展
要注册CopyHook扩展,我们需要在想要钩住的文件类型键的ShellEx键下建立CopyHookHandlers键。在CopyHookHandlers下建立一个新键,名字可以是任何喜欢的名字——Shell简单地枚举所有它找到的子键。其默认值应该指向这个扩展的CLSID。
下面是ATL注册脚本代码的补充(我们取Monitor作为键名):
HKCR
{
// 对象注册
Directory
{
ShellEx
{
CopyHookHandlers
{
Monitor = s '{7842554E-6BED-11D2-8CDB-B05550C10000}'
}
}
}
}
此时我们注册了一个在目录上工作的扩展。你可以试着把它注册为应用于文件类型(如exe文件),此时Shell将不能唤醒这个扩展。这是设计行为。
下面显示了典型的Log文件:
可监控对象
尽管我们有关于文件的报警,目录也不是CopyHook扩展可以监视的唯一对象——打印机和驱动器也可以监视。要钩住打印机,你需要注册服务器到HKEY_CLASSES_ROOT/Printers键下,这就是允许打印机管理器在每次打印时弹出它们自己的用户界面的窍门。
在Internet 客户端SDK的资料中说明,你可以注册CopyHook扩展在*.键下,这使你相信能监视文件操作,但是不幸的是这不是真的。就我们的经验,没有能够查询单个文件是被拷贝还是被移动的办法。
关于拷贝钩子的进一步说明
我们前面说过,Shell并不通知你的扩展钩子操作的结果(成功,失败,中断)。然而因为你知道操作唤醒的目录,你可以使用通知对象(见第7章)来试着感觉它。通过在原路径上安装这个对象,你就可以知道什么时候某些事情发生了变化。然后通过检查,你还能发现事情怎么发生的变化。例如,对于拷贝,你可以验证目标目录是否包含了与源同名的文件。
实际,这并不太容易,因为 SHFileOperation()(钩子后面的函数)允许冲突文件重命名。所以系统指派的目标是不同于源名的。这是十分合理的。
在我们开发其基本文档由文件集组成的产品时就开始研究CopyHook扩展了。如果我们的客户想要通过Shell(而不是程序)管理文档,他们就必须记住文档的内部结构,以确保拷贝或删除它的所有部件。我们的想法就是钩住拷贝,移动,重命名和删除操作,这样才能保证在其中的任何一个变化时所有相关的文件都被影响。然而,正像我们说过的那样,这似乎是不可能的,所以我们所开发的程序最终使用了复合文件和OLE结构的存储。
拖拽数据到文件
Win32程序的通用特征是允许从探测器窗口选择文件,拖拽它们到程序的客户区域,以及使它感觉和处理所接收的数据。在前面章中我们已经给出了这方面的示例,特别是第6章。
我们将在这里给出另一种有点不同的方法。对于探测器窗口或Windows桌面上一定类型的单个文件,我们想要使它能够处理同样的拖拽事件。头一个例子就是WinZip:如果拖拽一个或多个文件到存在的.zip文件上,鼠标将变成‘+’形式(十字线),一旦落下,拖拽文件就被压缩并加到这个存档文件中。这个行为是通过另一种类型的Shell扩展获得的:DropHandler。
拖拽处理器扩展
‘DropHandler’扩展由IDropTarget 和 IPersistFile导出,必须注册在下面键下:
HKEY_CLASSES_ROOT
/<FileClass>
/ShellEx
/DropHandler
这里的<FileClass>显然是想要扩展处理的文档类型标识符名。
通常Default值应该是这个服务器的CLSID,注意,DropHandler不允许多重处理器同时操作同一个文件类型。也就是说,注册键的名字是不能重复的。
IDropTarget接口
在给出示例之前,需要查看一下IDropTarget接口的方法。它们在拖拽发生以后,和鼠标环绕可能的目标移动时都可能被涉及到:
方法 |
描述 |
DragEnter() |
鼠标进入一个可能的目标,这个目标决定数据是否可以被接受 |
DragOver() |
鼠标在一个可能的目标上移动 |
DragLeave() |
鼠标离开了拖拽区域 |
Drop() |
拖拽已经完成 |
一般情况下,对于OLE拖拽操作的可能目标是一个窗口(或窗口的一部分),它是通过RegisterDragDrop()函数自注册的。当组织和管理拖拽(源)的模块感觉到鼠标下有一个窗口时,它将核实是否有拖拽支持存在,如果存在,源则获得一个指向这个窗口输出的IDropTarget接口指针,而后开始调用上述方法。
在这个钩子下,就是简单地检查是否这个HWND有包含指向IDropTarget接口的指针属性。这里的属性是一个32位的Handle,它是通过SetProp() API函数附着在这个窗口上的。
当鼠标进入潜在的目标区域后,DragEnter()获得调用。IDropTarget接口总是与窗口关联,但是,通过适当地编码DragEnter(),你可以把任何区域作为可能的拖拽目标。这个方法的原型是:
HRESULT IDropTarget::DragEnter(LPDATAOBJECT pDO,
DWORD dwKeyState,
POINTL pt,
DWORD* pdwEffect);
它接收指向IDataObject接口的指针,这个接口包含了被拖拽的数据。另外的参数是32位值,它们表示键盘状态,鼠标在屏幕上的位置坐标,和用操作所允许的结果充填的缓冲。换句话说,这个方法期望解析接收的数据,鼠标位置和键盘状态,以便确定它是否能接受这次拖拽。使用这种方式,你就能够仅接受一定窗口区域上的拖拽操作。(你需要转换客户坐标位置),在鼠标移动到目标区域上时调用DragOver()方法。这个方法提供了拖拽操作的实时信息——随着鼠标的移动,最终结果可能改变,其原型考虑到了鼠标位置和键盘状态:
HRESULT IDropTarget::DragOver(DWORD dwKeyState,
POINTL pt,
DWORD* pdwEffect);
再次注意,你可以用32位输出缓冲通知鼠标所期望的结果。当然,DragOver()被更频繁地调用,并且总是在DragEnter()之后。因此由DragOver()设置的结果可以覆盖由DragEnter()所设置的。DragLeave()是一个非常简单的方法,它使目标知道鼠标已经退出它的领域,其原型为:
HRESULT IDropTarget::DragLeave();
最后一个方法,Drop(),当数据在目标上被释放时,获得调用。这显然是所有方法中最重要的方法。因此也有更多需要说明的。为了建立ATL部件,我们需要在下面的例子中使用IPersistFileImpl.h头文件。它也提供了IDropTarget接口的基类:
// IDropTargetImpl.h
#include <AtlCom.h>
class ATL_NO_VTABLE IDropTargetImpl : public IDropTarget
{
public:
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0;
_ATL_DEBUG_ADDREF_RELEASE_IMPL( IDropTargetImpl )
// IDropTarget (优化的Shell拖拽对象)
STDMETHOD(DragEnter)(LPDATAOBJECT pDO, DWORD dwKeyState,POINTL pt,
DWORD *pdwEffect)
{
STGMEDIUM sm;
FORMATETC fe;
// 我们是否接受这个类型的数据
ZeroMemory(&sm, sizeof(STGMEDIUM));
ZeroMemory(&fe, sizeof(FORMATETC));
fe.tymed = TYMED_HGLOBAL;
fe.lindex = -1;
fe.dwAspect = DVASPECT_CONTENT;
fe.cfFormat = CF_HDROP;
if(FAILED(pDO->GetData(&fe, &sm)))
{
fe.cfFormat = CF_TEXT;
if(FAILED(pDO->GetData(&fe, &sm)))
{
// 拒绝拖拽
*pdwEffect = DROPEFFECT_NONE;
return E_ABORT;
}
}
// 默认活动是拷贝数据
*pdwEffect = DROPEFFECT_COPY;
return S_OK;
}
STDMETHOD(DragOver)(DWORD dwKeyState, POINTL pt, DWORD* pdwEffect)
{
// 不接受键盘修改器
*pdwEffect = DROPEFFECT_COPY;
return S_OK;
}
STDMETHOD(DragLeave)()
{
return S_OK;
}
STDMETHOD(Drop)(LPDATAOBJECT pDO, DWORD dwKeyState,POINTL pt, DWORD* pdwEffect);
};
诚然,这个基本实现是为我们自己的目的所优化的,事实上,在这个头文件中的代码除了Drop()之外,是接口的基本行为。而这个类的另外两个特征是:
目标仅接受一般文本格式数据,格式名为CF_TEXT,这是Windows剪裁板标准格式。
目标仅支持‘拷贝’操作,不支持其它操作如‘链接’或‘移动’。
处理TXT文件的拖拽事件
我们给出的第一个例子由处理TXT文件上的拖拽文本组成。想法是拖拽数据(文件或简单文字)将添加到目标文件的底部。从建立DropText DLL工程(project)开始。使用ATL COM应用大师和对象大师建立StrAdd对象。这个ATL对象声明如下:
#include "resource.h" // 主符号表
#include "IPersistFileImpl.h"
#include "IDropTargetImpl.h"
#include <ComDef.h>
////////////////////////////////////////////////////////////////////////////
// CStrAdd
class ATL_NO_VTABLE CStrAdd :public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CStrAdd, &CLSID_StrAdd>,
public IDropTargetImpl,
public IPersistFileImpl,
public IDispatchImpl<IStrAdd, &IID_IStrAdd, &LIBID_DROPTEXTLib>
{
public:
CStrAdd()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_STRADD)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CStrAdd)
COM_INTERFACE_ENTRY(IStrAdd)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IDropTarget)
COM_INTERFACE_ENTRY(IPersistFile)
END_COM_MAP()
// IDropTarget
public:
STDMETHOD(Drop)(LPDATAOBJECT, DWORD, POINTL, LPDWORD);
// IStrAdd
public:
private:
HDROP GetHDrop(LPDATAOBJECT);
BOOL GetCFText(LPDATAOBJECT, LPTSTR, UINT);
};
最值得注意的代码部分是在落下发生时处理过程,这在Drop()中定义的,其代码显示如下:
#include "stdafx.h"
#include "DropText.h"
#include "StrAdd.h"
#include <shlwapi.h>
// 常量
const int MAXBUFSIZE = 2048; // 要接收的文本尺寸
const int MINBUFSIZE = 50; // 被显示的文本尺寸
////////////////////////////////////////////////////////////////////////////
// CStrAdd
HRESULT CStrAdd::Drop(LPDATAOBJECT pDO, DWORD dwKeyState, POINTL pt,
LPDWORD pdwEffect)
{
// 获取CF_HDROP 数据对象
HDROP hdrop = GetHDrop(pDO);
if(hdrop)
{
// 在多选情况下仅考虑头一个文件
TCHAR szSrcFile[MAX_PATH] = {0};
DragQueryFile(hdrop, 0, szSrcFile, MAX_PATH);
DragFinish(hdrop);
// 检查是否为TXT文件
LPTSTR pszExt = PathFindExtension(szSrcFile);
if(lstrcmpi(pszExt, __TEXT(".txt")))
{
MessageBox(GetFocus(),__TEXT("抱歉, 你仅能拖拽TXT文件!"),
__TEXT("拖拽文件..."),MB_ICONSTOP);
return E_INVALIDARG;
}
// 在连接之前确认
TCHAR s[2 * MAX_PATH] = {0};
wsprintf(s, __TEXT("Would you add /n%s/nat the bottom of/n%s?"),
szSrcFile, m_szFile);
UINT rc = MessageBox(GetFocus(), s,__TEXT("Drop Files..."),
MB_ICONQUESTION | MB_YESNO);
if(rc == IDNO)
return E_ABORT;
}
else
{
TCHAR szBuf[MAXBUFSIZE] = {0};
GetCFText(pDO, szBuf, MAXBUFSIZE);
TCHAR s[MAX_PATH + MINBUFSIZE] = {0};
TCHAR sClipb[MINBUFSIZE] = {0};
lstrcpyn(sClipb, szBuf, MINBUFSIZE);
wsprintf(s, __TEXT("Would you add/n[%s...]/nat the bottom of/n%s?"),
sClipb, m_szFile);
UINT rc = MessageBox(GetFocus(), s,__TEXT("Drop Files..."),
MB_ICONQUESTION | MB_YESNO);
if(rc == IDNO)
return E_ABORT;
}
// TO DO: 链接文字操作
return S_OK;
}
这个函数支持文字和文件名,因此你可以拖拽或者是探测器内的TXT文件或者是文本编辑器或文字处理器的一块文本,包括Word,记事本,字处理,甚至VC++ 编辑器。
Drop()首先执行的检查是落下的数据类型,如果GetHDrop()方法返回可用的Handle,则数据是CF_HDROP格式的,必须通过DragQueryFile()访问。此时,这个函数仅仅处理第一个文件,而放弃所有多选情况下的其它文件,然而这仅仅是为了简化处理——不妨碍你做更复杂的处理。而后这段代码使用PathFindExtension()函数,检查文件的扩展名是否为TXT,这个函数在shlwapi.dll中。
// 从LPDATAOBJECT中抽取 HDROP
HDROP CStrAdd::GetHDrop(LPDATAOBJECT pDO)
{
STGMEDIUM sm;
FORMATETC fe;
// 检查CF_HDROP 数据
ZeroMemory(&sm, sizeof(STGMEDIUM));
ZeroMemory(&fe, sizeof(FORMATETC));
fe.tymed = TYMED_HGLOBAL;
fe.lindex = -1;
fe.dwAspect = DVASPECT_CONTENT;
fe.cfFormat = CF_HDROP;
if(FAILED(pDO->GetData(&fe, &sm)))
return NULL;
else
return static_cast<HDROP>(sm.hGlobal);
}
如果落下的数据不是CF_HDROP格式的,则Drop()方法使用辅助函数GetCFText()努力从其中抽取简单文字。如果成功,它使用最大字节数填写缓冲。
// 从LPDATAOBJECT中抽取 CF_TEXT
BOOL CStrAdd::GetCFText(LPDATAOBJECT pDO, LPTSTR szBuf, UINT nMax)
{
STGMEDIUM sm;
FORMATETC fe;
// 检查 CF_TEXT 数据
ZeroMemory(&sm, sizeof(STGMEDIUM));
ZeroMemory(&fe, sizeof(FORMATETC));
fe.tymed = TYMED_HGLOBAL;
fe.lindex = -1;
fe.dwAspect = DVASPECT_CONTENT;
fe.cfFormat = CF_TEXT;
if(FAILED(pDO->GetData(&fe, &sm)))
return FALSE;
else
{
LPTSTR p = static_cast<LPTSTR>(GlobalLock(sm.hGlobal));
lstrcpyn(szBuf, p, nMax);
GlobalUnlock(sm.hGlobal);
return TRUE;
}
}
这段C++代码完成了这个扩展技术,下面要考虑的是注册问题,下面是适当的注册条目:
HKCR
{
// 对象注册
txtfile
{
Shellex
{
DropHandler = s '{AE62DAAC-509C-11D2-BC00-AC6805C10E27}'
}
}
}
下图显示在拖拽TXT文件或简单文字到一个Windows Shell中的TXT文件时显示的确认消息框:
你会发现我们并没有给出实际链接文字的代码,然而,这段代码的重要部分是感觉,而不是实际执行。
增加Shell对脚本的支持
在第13章中,我们讨论了Windows脚本环境对象模型,在结尾处讨论对WSH环境的改进方法时,我们提到过写一个Shell扩展在VBScript或Jscript上拖拽参数的可能性。现在我们就揭开这个秘密,看一下怎样来实现它。
这是扩展是一个DropHandler,它与VBS和JS文件关联。代码框架与前一个例子绝对一致:ATL COM对象实现IPersistFile和 IDropTarget,重用基本的接口实现。需要改变的是Drop()方法和注册脚本。
工程与注册脚本
建立简单的工程(project)VBSDrop,添加对象WSHUIDrop,改变它的RGS脚本:
HKCR
{
// 对象注册
vbsfile
{
Shellex
{
DropHandler = s '{E671DB13-4D41-11D2-BC00-AC6805C10E27}'
}
}
jsfile
{
Shellex
{
DropHandler = s '{E671DB13-4D41-11D2-BC00-AC6805C10E27}'
}
}
}
脚本文件上的拖拽参数
我们将要介绍的这个例子显示怎样通过Shell传递参数到VBS或JS脚本文件。这个想法是,选择数据——例如,在探测器中的文件名——并拖拽到通过命令行接收它们的脚本文件上。这个示例使用文件名和CF_HDROP格式,但是不仅限于此,你还可以处理串。
我们在这里介绍的Drop()函数抽取拖拽到VBS或JS文件上的各个文件名,并建立登记条目串,其中,每一个都由空格分隔,是全路径文件名。如果路径名包含空格,则把它们括在引号中。对最后的这个操作,我们开发另一个函数以排除shlwapi.dll对路径名的限制:PathQuoteSpaces()把路径名封装在引号中,如果它是含有空格的长文件名。
当串准备好后,它必须作为变量传递到脚本文件并执行之,这正是ShellExecute()要做的。在随后的调用中,脚本文件名在Shell扩展初始化时被存储在CWSHUIDrop类的m_szFile成员中:
ShellExecute(GetFocus(), __TEXT("open"), m_szFile, pszBuf, NULL, SW_SHOW);
为了运行VBS或JS文件,你必须执行打开动词。pszBuf变量组成了这个文件的命令行参数。
///////////////////////////////////////////////////////////////////////////
// A portion of this code also appeared in the December 1998 issue of MIND
HRESULT CWSHUIDrop::Drop(LPDATAOBJECT pDO, DWORD dwKeyState, POINTL pt,
LPDWORD pdwEffect)
{
// 获取 CF_HDROP 数据对象
HDROP hdrop = GetHDrop(pDO);
if(hdrop == NULL)
return E_INVALIDARG;
// 取得Shell存储处理器
LPMALLOC pMalloc = NULL;
SHGetMalloc(&pMalloc);
// 为最终的组合串分配足够的内存
int iNumOfFiles = DragQueryFile(hdrop, -1, NULL, 0);
LPTSTR pszBuf = static_cast<LPTSTR>(pMalloc->Alloc((1 +
MAX_PATH) * iNumOfFiles));
LPTSTR psz = pszBuf;
if(!pszBuf)
{
pMalloc->Release();
return E_OUTOFMEMORY;
}
ZeroMemory(pszBuf, (1 + MAX_PATH) * iNumOfFiles);
// 取得被拖拽文件名,并组合串
for(int i = 0 ; i < iNumOfFiles ; i++)
{
TCHAR s[MAX_PATH] = {0};
DragQueryFile(hdrop, i, s, MAX_PATH);
PathQuoteSpaces(s);
lstrcat(pszBuf, s);
lstrcat(pszBuf, __TEXT(" "));
}
DragFinish(hdrop);
// 运行脚本,传递拖拽文件作为命令行变量
ShellExecute(GetFocus(), __TEXT("open"), m_szFile, pszBuf, NULL, SW_SHOW);
pMalloc->Release();
return S_OK;
}
为了查看这段代码的整体效果,考虑下面的Jscript代码:
/////////////////////////////////////////////////////////////////////////
// 对Shell DropHandler 的 JScript 示例
// 它简单地显示命令行所接收的东西
var shell = WScript.CreateObject("WScript.Shell");
var sDrop = "Arguments received:/n/n";
if(WScript.Arguments.Length > 0)
{
for(i = 1 ; i <= WScript.Arguments.Length ; i++)
sDrop += i + ") " + WScript.Arguments.Item(i - 1) + "/n";
shell.Popup(sDrop);
}
else
shell.Popup("No argument specified.");
WScript.Quit();
把这段代码放在jsdrop.js文件中,检测命令行并显示各种参数到不同行上列表的消息框。然后试着拖拽几个文件到其上:
我们新的DropHandler Shell扩展将结果列出在消息框中:
DataHandler Shell扩展
我们就要完成Windows Shell扩展领域的旅程了,但是在结束之前,还有另一个扩展类型需要说几句,它涉及到另一个通用用户接口:剪裁板。如果想要获得对一定类型文件的‘拷贝/粘贴’操作的控制,你就应该编写DataHandler扩展。例如,你希望在按下Ctrl-C 时改变BMP文件拷贝到剪裁板的方式,默认情况下,Shell以CF_HDROP格式拷贝文件名。如果你希望图像以CF_BITMAP格式进入剪裁板,则需要写一个DataHandler扩展。
涉及的COM接口
写DataHandler Shell扩展需要实现IPersistFile 和 IDataObject接口。这种类型的扩展注册要求新的默认值:
HKEY_CLASSES_ROOT
/<FileClass>
/ShellEx
/DataHandler
通常你应该设置服务器的CLSID,而 <FileClass> 是这个扩展应用的文档文件类标识符。
IDataObject负责传递数据到剪裁板,并且包含几个方法。就我们在这里的目的,你仅需要实现星号* 标注的方法即可:
方法 |
描述 |
*GetData() |
恢复与给定对象关联的数据 |
GetDataHere() |
类似于GetData(),但是这个函数也接收存储数据的介质 |
*QueryGetData() |
确定请求的数据是否可用 |
GetCanonicalFormatEtc() |
对象列出所支持的格式 |
*SetData() |
连接指定数据与给定对象 |
EnumFormatEtc() |
枚举用于存储数据于对象的格式 |
DAdvise() |
连接接收器对象以便知道数据变化 |
DUnadvise() |
断开接器收对象 |
EnumDAdvise() |
枚举当前接收器对象 |
DataHandler与IconHandler 和 DropHandler一样不允许多重处理器同时操作同一种文件类型,也就是说,例如,你对BMP文件拷贝图像的点到剪裁板,也就失去了拷贝文件名到剪裁板的能力——除非你的扩展实现这两个目标。
Shell扩展开发者手册
在这一章中,我们使用ATL建立了一定数量的Shell扩展——事实上这已经成为一种习惯了。下面我们逐步说明使用ATL建立、编译和测试Shell扩展所需要做的工作。
使用ATL COM 应用大师建立新的ATL工程(project)
添加一个简单对象
如果没有可用的头,为每一个需要实现的接口写一个IxxxImpl.h文件。你需要层次地从接口 定义新类并为每种方法提供基本行为。如果需要,也可以添加属性或私有成员。
修改新对象类的头文件,尤其是使它从前一步定义的所有IxxxImpl类继承,添加接口到对象的COM映射,并添加所有需要重载的接口方法的声明。
修改ATL注册表脚本实现Shell扩展注册。正常情况,大师仅生成注册服务器必须的代码。
添加重载方法的代码
编译这个工程,并确保注册是所期望的。如果不能确定,使用regsvr32.exe重复注册
在测试功能之前一定要保证代码被适当地装入Shell。这个操作将依赖于Shell扩展的类型,对于关联菜单和属性页,刷新探测器窗口就足够了,而图标处理器和拷贝钩子,要求注销用户甚至重启机器。
所有要重新编译服务器的活动都必须首先注销服务器。然后或者LogOff或者重新引导机器。
文件观察器
作为这一章的结束,我们看一个不是Shell扩展的模块,但是它有相同的作用。文件观察器(也称为快速观察器)是一个进程内COM服务器,它通过系统Shell添加了文档类型功能:插入到探测器中提供快速观察一定类型文件内容的能力。例如,Word观察器,可以查看Word文档,但是远没有完全的Word程序大和有力。
在一个用快速观察器打开的文件上用户既不能修改也不能执行特殊的功能,这个目标只简单地提供只读的文档预览不必导出正常的文件关联应用。为了给出完全与Shell集成的文档,文件观察器必须与Shell扩展一样。
文件观察器依赖于Shell4.0以后才可用的通用控件,但这并不总是默认安装的——在有些PC上这个控件就可能没有安装,甚至这个控件都没有在选项中出现。此时应该手动安装和注册。完成之后‘Quick View’项将出现在文件的关联菜单中:
Windows有一定数量的文件观察器,其中有一个对于观察Win32 可执行文件(DLL/EXE)的输出和输入是有用的这里就是winword.exe的活动:
开始快速观察
在点击‘Quick View’时,Windows导出quikview.exe程序,这是一种文件观察管理器。它自己并不作任何工作,相反,它加载和连接相应实际显示文件内容的COM模块。
在我们的观点上看,文件观察器和Shell扩展之间最大的差别在于主程序不是explorer.exe 而是 quikview.exe——文件观察器不是运行在探测器地址空间中。此外,有一个新的COM接口与之(IFileViewer)对应,并且遵循不同的注册逻辑。因此可以说,当它加载和卸载文件观察器时quikview.exe就象探测器一样工作。
对于文件观察器重要的是不仅对不同类型的文件有不同的应用,而且只有一个主模块管理所有COM扩展。这些外部插入者提供了实际的观察功能。并且它们都注册在下面键下:
HKEY_CLASSES_ROOT
/QuickView
一看你就会发现,对每一个支持的文件类型都有一个键。下图是典型的Windows9.x注册表的情形:
每一个特定的文件扩展名键都包含一个子键,其中含有提供显示的COM服务器的CLSID。默认,所有支持的文件类型都在sccview.dll中实现,其CLSID是{F0F08735-0C36-101B-B086-0020AF07D0F4}。
快速观察器怎样获得调用
每次点击关联菜单或命令行‘quikview 文件名’都将引起快速观察管理器启动。它检查文件扩展名,扫视注册表的‘QuickView’注册区域,搜索CLSID。如果成功,就建立一个COM服务器实例,并开始处理这个对象必须实现的接口。在用户请求新的快速观察窗口时,管理器查看‘Replace Window’工具条按钮的状态:
如果设置,使用相同的窗口和实例。否则,显示观察器的新实例。快速观察器还需要支持拖拽,过一会我们就会看到。
写一个快速观察器
快速观察器是一个进程内COM模块,它实现三个接口:
! IPersistFile
! IFileViewer
! IFileViewerSite
IPersistFile用于装入指定文件。管理器只是查询IPersistFile模块,和调用Load()方法。典型的文件观察器则打开文件,和转换内容到可显示格式。例如,如果文件是元文件,则IPersistFile::Load()可能想要建立HENHMETAFILE Handle。因为文件观察器唯一的活动就是‘读’,因此不需要实现整个IPersistFile接口,只编写Load()方法就可以很好地工作。
显示文件
IFileViewer接口有三个函数组成:
! PrintTo()
! Show()
! ShowInitialize()
需要绘制内容的所有操作都应该写在ShowInitialize()中。它必须建立一个不可视窗口,并且用要显示的文件冲填其客户域。事实上这个函数应该做显示文件所需要的所有操作,简化开启建立窗口的WS_VISIBLE风格。换言之,ShowInitialize()函数需要工作在一种屏外缓冲区上。ShowInitialize()还应该注意与文件观察器用户界面协同操作。即:
! 建立主窗口(如果需要)
! 建立和初始化工具条和状态条
! 设置菜单和加速器
! 建立(初始不可视)窗口来显示内容
! 适当地缩放窗口尺寸
在任务成功地完成之后,它就转到Show()。在其它的操作中,这个方法使窗口可视,并进入消息循环。
从这个主要描述中,我们可以看出,快速观察器比插入模块作的要多。事实上,它实际是一个编译进DLL完整的文档/观察应用。你所看到的命令菜单,字体变换,甚至为打开文件而启动的默认应用都必须在这个DLL中处理。
快速观察应该支持拖拽,因此窗口必须有WS_EX_ACCEPTFILES标志。这可能引起一些情况,例如,如果当前观察器正在显示一个BMP,用户拖拽一个TXT文件到其窗口上。对于位图观察器怎样设计来管理文本文件呢?
为了管理这种情况,背后需要做大量的工作。在解释了这个操作之后,你就可以理解为什么IFileViewer接口需要这两个方法了(ShowInitialize()和Show())。头一个方法的调用只是保证显示文件的所有必要的事情都是可用的——如果失败,当前显示的文档仍然不变。就象你从未试图打开其它文档一样。这个特征有助于使整个快速观察应用看上去像一个整体,而不是不同部件的集合。
在Show()方法被调用的时候,快速观察器接收FVSHOWINFO结构作为单个的变量:
typedef struct
{
DWORD cbSize;
HWND hwndOwner;
int iShow;
DWORD dwFlags;
RECT rect;
LPUNKNOWN punkRel;
OLECHAR strNewFile[MAX_PATH];
} FVSHOWINFO, *LPFVSHOWINFO;
使用这个结构不仅是要传递信息进入,而且要返回数据到quikview.exe程序。当文件被落下时,快速观察器接收到通常的WM_DROPFILES消息,如果文件不能被处理,模块应该做下面的工作:
设置strNewFile到实际文件名
打开dwFlags字段的FVSIF_NEWFILE位
保存IUnknown指针到punkRel
设置rect到当前窗口尺寸
退出消息循环
重要的是你不需要毁坏窗口,避免闪烁和生硬地改变用户界面。你所返回的FVSHOWINFO结构被不变地传递给新文件观察器。quikview.exe唤醒这个新观察器(在此例中是处理TXT文件的),并调用它的ShowInitialize()方法来准备显示。注意,此时我们仍然有同一个位图在屏幕上,甚至是完全不同的模块在这个封装下工作。在TXT快速观察器完成装入和文字变换之后,quikview.exe调用Show()方法传递FVSHOWINFO结构,这是由BMP快速观察器Show()方法返回的结构。这个结构包含了窗口应该占有的精确区域,打开文件的名字,和前一个(仍然可视)快速观察器的IUnknown指针。Show()可以完全覆盖地显示它的窗口。在这一点上,老窗口仍然在新窗口背后,事实上Show()方法还有更多的任务要执行。如果它发现FVSIF_NEWFILE标志被设置,它就必须获得FVSHOWINFO的punkRel,并且调用Release()来释放老的观察器。
钉住连接
我们所涉及到的第三个接口是IFileViewerSite,它有两个相当容易的方法:
! GetPinnedWindow()
! SetPinnedWindow()
快速观察器窗口在‘Replace Window’按钮选中时被钉住。这个状态使管理器直接定向所有请求到新快速观察器窗口。如果一个窗口被钉住,点击关联菜单就等价于拖拽文件到那个窗口。GetPinnedWindow()就返回当前钉住的窗口Handle(记住,可以同时打开很多快速观察器),而SetPinnedWindow()则移动这个属性到新窗口。它们的原型是:
HRESULT GetPinnedWindow(HWND*);
HRESULT SetPinnedWindow(HWND);
钉住以后的操作逻辑可以概括如下:
SetPinnedWindow()总是失败,如果另一个窗口被钉住
你总是需要自己拔除当前钉住的窗口——这可以通过调用SetPinnedWindow()并设置NULL变量来完成
为了使你知道是否你的窗口应该开始钉住,FVSHOWINFO包含了FVSIF_PINNED标志在dwFlags成员中。如此,钉住窗口最明智的方法是下面这两行代码:
SetPinnedWindow(NULL);
SetPinnedWindow(hwnd);
写并注册文件观察器
写文件观察器不是一个简单的任务,在MSDN资料库中可以找到参考答案。写好了观察器后,注册它就是直接的了。假设你已经准备了一个.ext类型的文件观察器,下面是注册键的改变:
[HKEY_CLASSES_ROOT/QuickView/.EXT/<CLSID>]
@="EXT File Viewer"
这里的<CLSID>应该改为实际的CLSID,同时不要忘了注册服务器,就象任何其它COM服务器注册那样。如果使用ATL建立对象,则添加下面行到RGS脚本文件:
{
QuickView
{
.ext
{
<CLSID> = s 'description'
}
}
}
小结
这一章极详细地讨论了Shell扩展技术。我们检测了它与探测器的集成,它们背后的逻辑,以及它们的实现。我们还开发了几个示例来说明各种类型的Shell扩展行为。特别是,我们察看了:
怎样添加定制属性页到属性对话框
怎样添加定制菜单项到文档关联菜单
怎样添加定制菜单项到系统‘查找’菜单
怎样为一定类型的每一个文档绘制客户图标
怎样监视系统中任何文件夹的变化
怎样处理Shell中文件的拖拽
我们还给出了关于拷贝数据到剪裁板的实用技术,以及拖拽处理技术。最后介绍了文件观察器,并集中讨论了编程方面应注意的问题。