第十五章 SHELL扩展程序员
谈到Windows Shell编程,Shell扩展是最重要的科目之一,绝大多数商业应用的最酷特征的都是经过Shell扩展实现的,并且有许多显著的系统特征实际都是插入了扩展代码。Shell扩展尤为使人激动的是它容许你把你的应用做为Shell的一部分来处理。shell
Shell扩展的另外一个好处是微软正在使它变得更聪明,例如,‘查找’菜单,从Windows95 到Windows98 一直是经过Shell扩展加强的,并且增长了新条目。还有,出如今文档关联菜单上的位图项也是使用Shell扩展增长的。编程
Shell扩展不只是构建增长Shell功能模块的重要手段,并且也是使应用得到有力的Shell特征的重要方法。在前面各章中,咱们讨论了系统集成方面Win32应用程序应该作的工做。咱们探讨了关联菜单,图标,和几个其它方面技术。然而,这些都是静态和肯定的。你能够设置或删除它们,然而,这些就是你所能作的所有:在这之间你不能作任何事情。所以,通向彻底融入Windows的应用最后一步是要考虑编写一个或多个Shell扩展的可能性。注意,我说的“可能性”,事实上尽管Shell扩展是与Shell通信的有力而且是灵活的方法,可是它并非你和你的程序必须作的。小程序
在这一章中,咱们将探讨全部Shell扩展的编程技术,而且提供某些有意义的示例,主要方向是:windows
Shell扩展是什么,怎样与它们一同工做api
用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中文件的拖拽
咱们还给出了关于拷贝数据到剪裁板的实用技术,以及拖拽处理技术。最后介绍了文件观察器,并集中讨论了编程方面应注意的问题。