Thursday, March 10, 2005

 

Some notes on Windows Shell programming - 2

1. Shell扩展编程完全指南

The Complete Idiot''s Guide to Writing Shell Extensions - Index
作者:Michael Dunn
出处:http://www.codeproject.com/shell/shellextguideindex.asp
翻译:soarlove
出处:http://www.vccode.com/file_show.php?id=528

第一节 一步步教你如何编写Shell扩展

所谓的Shell扩展就是能够添加某种功能到Windows Shell的COM对象。Windows里有着各种各样的扩展,但关于Shell扩展的原理以及如何编写Shell扩展的文档却很少。如果你想深入地了解Shell各方面的细节,我特别推荐Dino Esposito的著作《Visual C++ Windows Shell Programming》。但对于那些没有这本书的,或只对Shell扩展本身感兴趣的朋友,我写了这个编程指南希望能够帮助你理解怎样编写Shell扩展。该指南假设你理解COM和ATL的基本原理及应用。

第一节对Shell扩展进行了概括性的介绍, 并给出了一个上下文菜单的扩展以引起你对以后各章的兴趣.

但Shell扩展到底是什么玩意呢?
"Shell扩展"从字面上分两个部分,Shell与Extension。 Shell指Windows Explorer, 而Extension则指由你编写的当某一预先约定好的事件(如在以.doc为后缀的文件图标上单击右键)发生时由Explorer调用执行的代码。因此一个Shell扩展就是一个为Explorer添加功能的COM对象。

Shell扩展是个进程内服务器(运行在Explorer进程内),它实现了一些接口来处理与 Explorer 的通信。 ATL在我看来是设计Shell扩展最简单最快捷的方法, 如果没有它,你就不得不一遍又一遍地编写繁琐的 QueryInterface() 及AddRef()代码. 另外,在Windows NT 和 2000上调试Shell扩展相对比较容易一些,这我以后会讲到的。

Shell扩展有很多种类型,每种类型都在各自不同的事件发生时被调用运行,但也有一些扩展的类型和调用情形是非常相似的。

类型
何时被调用
应该作些什么

上下文菜单扩展处理器
用户右键单击文件或文件夹对象时。

或在一个文件夹窗口中的背景处单击右键时(要求shell版本为4.71+)。
添加菜单项到上下文菜单中。

属性页扩展处理器
要显示一个文件对象的属性框时。
添加定制属性页到属性表中。

拖放目标扩展处理器
用户用右键拖放文件对象到文件夹窗口或桌面时。
添加菜单项到上下文菜单中。

放置目标扩展处理器
用户拖动Shell对象并将它放到一个文件对象上时。
任何想要的操作。

提示信息扩展处理器 (需要shell版本 4.71+)
用户将鼠标盘旋于文件或其他Shell对象的图标上时。
返回一个浏览器用于显示在提示框中的字符串。


现在你可能想知道Shell扩展到底是什么样的. 如果你安装了 WinZip (有谁没装的吗?), 它就包含了多种的Shell扩展,其中也就有上下文菜单扩展. 下图是WinZip 8 为压缩文件对象添加的定制菜单项:



WinZip 编写了添加菜单项的代码, 提供了浏览器状态栏上的菜单项帮助提示, 并在用户选择一个菜单命令时执行相应的操作。

WinZip 还包括一个拖放目标扩展处理器. 该类型与上下文菜单十分类似, 但它是在用户用右键拖放文件时被触发的. 下图是 WinZip 定制的拖放菜单:



Shell扩展的类型很多,而且微软也正不断地在每一新版本的Windows中加入更多的扩展类型. 现在让我们把注意力放在上下文菜单上, 因为它们易于编写,效果也很明显(这能马上满足你).

在我们编写代码之前, 先说一下一些简化编码及调试工作的技巧. 当shell扩展被 Explorer调用后, 它会在内存中呆上一段时间, 这会使你无法重新编译并生成Shell扩展DLL文件. 要让 Explorer 更迅速地卸载Shell扩展执行文件,需要创建如下注册表项:

HKLM\Software\Microsoft\Windows\CurrentVersion\Explorer\AlwaysUnloadDLL

并将其值设为 "1". 对于Win9x, 这是你能做的最好的方法。 而在Win NT/2000上, 你可以找到如下键:

HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer

并创建一个名为DesktopProcess的DWORD值 1. 这会使桌面和任务栏运行在同一个进程中, 而其他每一个 Explorer 窗口都运行在它自己的每一个进程内. 也就是说,你可以在单个的Explorer 窗口内进行调试, 而后只要你关闭该窗口,你的DLL就会被马上卸载, 这就避免了因为DLL正被Windows使用而无法替换更新. 而如果不幸出现这种情况,你就不得不注销登录后再重新登录进Windows从而强制卸载使用中的Shell扩展DLL.

我将在稍后解释如何在Win9x中进行调试的细节.

开始编写上下文菜单 – 它该做些什么?
开头先让我们做简单一些, 只弹出一个对话框以表明当前的扩展能够正常地工作. 我们把扩展关联到 .TXT 文件, 因此当用户右键单击文本文件对象时扩展就会被调用.

使用 AppWizard 开始
好吧, 让我们开始吧! 什么? 我还没告诉你怎样使用那些神秘的 shell 扩展接口? 别着急, 我会边进行边解释的。我觉得先解释一下一个概念再紧接着说明示例代码,对理解例子程序会更简单一些. 当然我也可以把所有的东西都先解释完,然后再解释代码, 但我觉得这样做不能吸引人的注意力。不管怎么样, 向 VC开火,开始!

运行AppWizard,生成一个名为SimpleExt的 ATL COM 工程. 保留所有默认的设置选项,点击”完成”. 现在我们已经有了一个空的 ATL工程,它可以编译并生成一个 DLL, 但我们还需要添加Shell扩展的 COM 对象. 在 ClassView 中, 右击 SimpleExt classes 条目, 选择 New ATL Object.

在ATL Object Wizard里, 第一页默认已经选择了 Simple Object , 所以单击 Next 即可. 在第二页中, 在Short Name 文本框里输入 SimpleShlExt ,点击 OK. (其余的文本框会自动填充完.) 这样就创建了一个名为 CSimpleShlExt 的类,其包含了实现COM对象最基本的代码. 我们将在这个类中加入我们自己的代码.

初始化接口
当我们的shell扩展被加载时, Explorer 将调用我们所实现的COM对象的 QueryInterface() 函数以取得一个 IShellExtInit 接口指针. 该接口仅有一个方法 Initialize(), 其函数原型为:

HRESULT IShellExtInit::Initialize (
LPCITEMIDLIST pidlFolder,
LPDATAOBJECT pDataObj,
HKEY hProgID );
Explorer 使用该方法传递给我们各种各样的信息. PidlFolder是用户所选择操作的文件所在的文件夹的 PIDL 变量. (一个 PIDL [指向ID 列表的指针] 是一个数据结构,它唯一地标识了在Shell命名空间的任何对象, 一个Shell命名空间中的对象可以是也可以不是真实的文件系统中的对象.) pDataObj 是一个 IDataObject 接口指针,通过它我们可以获取用户所选择操作的文件名。 hProgID 是一个HKEY 注册表键变量,可以用它获取我们的DLL的注册数据. 在这个简单的扩展例子中, 我们将只使用到 pDataObj 参数.

要添加这个接口进 COM 对象, 先打开SimpleShlExt.h 文件, 然后加入下列标红的代码:

#include
#include
class ATL_NO_VTABLE CSimpleShlExt :
public CComObjectRootEx,
public CComCoClass,
public IDispatchImpl,
public IShellExtInit
{
BEGIN_COM_MAP(CSimpleShlExt)
COM_INTERFACE_ENTRY(ISimpleShlExt)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IShellExtInit)
END_COM_MAP()
COM_MAP是ATL实现 QueryInterface()机制的宏,它包含的列表告诉ATL其它外部程序用QueryInterface()能从我们的
COM对象获取哪些接口.
接着,在类声明里,
加入Initialize()的函数原型.
另外我们需要一个变量来保存文件名:
protected:
TCHAR m_szFile [MAX_PATH];
public:
// IShellExtInit
STDMETHOD(Initialize)(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
然后, 在 SimpleShlExt.cpp 文件中, 加入该函数方法的实现定义:
HRESULT CSimpleShlExt::Initialize (
LPCITEMIDLIST pidlFolder,
LPDATAOBJECT pDataObj,
HKEY hProgID )

我们要做的是取得被右击选择的文件名,再把该文件名显示在弹出消息框中。可能会有多个文件同时被选择右击, 你可以用pDataObj 接口指针获取所有的文件名, 但现在为简单起见, 我们只获取第一个文件名.

文件名的存放格式与你拖放文件到带WS_EX_ACCEPTFILES风格的窗口时使用的文件名格式是一样的。 这就是说我们可以使用同样的API来获取文件名: DragQueryFile(). 首先我们先获取包含在IdataObject中的数据句柄:
{
FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM stg = { TYMED_HGLOBAL };
HDROP hDrop;
// 在数据对象内查找 CF_HDROP 型数据.
if ( FAILED( pDataObj->GetData ( &fmt, &stg )))
{
// Nope! Return an "invalid argument" error back to Explorer.
return E_INVALIDARG;
}
// 获得指向实际数据的指针
hDrop = (HDROP) GlobalLock ( stg.hGlobal );
// 检查非NULL.
if ( NULL == hDrop )
{
return E_INVALIDARG;
}

请注意错误检查,特别是指针的检查。 由于我们的扩展运行在 Explorer 进程内, 要是我们的代码崩溃了, Explorer也会随之崩溃. 在Win 9x上, 这样的一个崩溃可能导致需要重启系统.

所以, 现在我们有了一个 HDROP 句柄, 我们就可以获取我们需要的文件名了:
// 有效性检查 – 保证最少有一个文件名.
UINT uNumFiles = DragQueryFile ( hDrop, 0xFFFFFFFF, NULL, 0 );
if ( 0 == uNumFiles )
{
GlobalUnlock ( stg.hGlobal );
ReleaseStgMedium ( &stg );
return E_INVALIDARG;
}
HRESULT hr = S_OK;
// 取得第一个文件名并把它保存在类成员m_szFile 中.
if ( 0 == DragQueryFile ( hDrop, 0, m_szFile, MAX_PATH ))
{
hr = E_INVALIDARG;
}
GlobalUnlock ( stg.hGlobal );
ReleaseStgMedium ( &stg );
return hr;
}

要是我们返回 E_INVALIDARG, Explorer 将不会继续调用以后的扩展代码. 要是返回 S_OK, Explorer 将再一次调用QueryInterface() 获取另一个我们下面就要添加的接口指针: IContextMenu.

与上下文菜单交互的接口
一旦 Explorer 初始化了扩展, 它就会接着调用 IContextMenu 的方法让我们添加菜单项, 提供状态栏上的提示, 并响应执行用户的选择.

添加IContextMenu 接口到Shell扩展类似于上面IshellExtInit接口的添加 .打开 SimpleShlExt.h,添加下列标红的代码:

class ATL_NO_VTABLE CSimpleShlExt :
public CComObjectRootEx,
public CComCoClass,
public IDispatchImpl,
public IShellExtInit,
public IContextMenu
{
BEGIN_COM_MAP(CSimpleShlExt)
COM_INTERFACE_ENTRY(ISimpleShlExt)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IShellExtInit)
COM_INTERFACE_ENTRY(IContextMenu)
END_COM_MAP()
添加 IContextMenu 方法的函数原型:
public:
// IContextMenu
STDMETHOD(GetCommandString)(UINT, UINT, UINT*, LPSTR, UINT);
STDMETHOD(InvokeCommand)(LPCMINVOKECOMMANDINFO);
STDMETHOD(QueryContextMenu)(HMENU, UINT, UINT, UINT, UINT);
修改上下文菜单
IContextMenu 有三个方法. 第一个是 QueryContextMenu(), 它让我们可以修改上下文菜单. 其原型为:
HRESULT IContextMenu::QueryContextMenu (
HMENU hmenu,
UINT uMenuIndex,
UINT uidFirstCmd,
UINT uidLastCmd,
UINT uFlags );
hmenu 上下文菜单句柄. uMenuIndex 是我们应该添加菜单项的起始位置. uidFirstCmd 和 uidLastCmd 是我们可以使用的菜单命令ID值的范围. uFlags 标识了Explorer 调用QueryContextMenu()的原因, 这我以后会说到的.

而返回值根据你所查阅的文档的不同而不同. Dino Esposito 的书中说返回值是你所添加的菜单项的个数. 而 VC6.0所带的MSDN 又说它是我们添加的最后一个菜单项的命令ID加上 1. 而最新的 MSDN 又说:

将返回值设为你为各菜单项分配的命令ID的最大差值,加上1. 例如, 假设 idCmdFirst 设为5,而你添加了三个菜单项 ,命令ID分别为 5, 7, 和 8. 这时返回值就应该是:

MAKE_HRESULT(SEVERITY_SUCCESS, 0, 8 - 5 + 1).

我是一直按 Dino 的解释来做的, 而且工作得很好.实际上, 他的方法与最新的 MSDN 是一致的, 只要你严格地使用 uidFirstCmd作为第一个菜单项的ID,再对接续的菜单项ID每次加1.

我们暂时的扩展仅加入一个菜单项,所以 QueryContextMenu() 非常简单:

HRESULT CSimpleShlExt::QueryContextMenu (
HMENU hmenu,
UINT uMenuIndex,
UINT uidFirstCmd,
UINT uidLastCmd,
UINT uFlags )
{
// 如果标志包含 CMF_DEFAULTONLY 我们不作任何事情.
if ( uFlags & CMF_DEFAULTONLY )
{
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );
}
InsertMenu ( hmenu, uMenuIndex, MF_BYPOSITION, uidFirstCmd, _T("SimpleShlExt Test Item") );
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 1 );
}
首先我们检查 uFlags. 你可以在 MSDN中找到所有标志的解释, 但对于上下文菜单扩展而言, 只有一个值是重要的: CMF_DEFAULTONLY. 该标志告诉Shell命名空间扩展保留默认的菜单项,这时我们的Shell扩展就不应该加入任何定制的菜单项,这也是为什么此时我们要返回 0的原因. 如果该标志没有被设置, 我们就可以修改菜单了 (使用 hmenu 句柄), 并返回1 告诉Shell我们添加了一个菜单项.

在状态栏上显示提示帮助
下一个要被调用的IContextMenu 方法是 GetCommandString(). 如果用户是在浏览器窗口中右击文本文件,或选中一个文本文件后单击文件菜单时,状态栏会显示提示帮助.我们的 GetCommandString() 函数将返回一个帮助字符串供浏览器显示.

GetCommandString() 的原型是:

HRESULT IContextMenu::GetCommandString (
UINT idCmd,
UINT uFlags,
UINT *pwReserved,
LPSTR pszName,
UINT cchMax );
idCmd 是一个以0为基数的计数器,标识了哪个菜单项被选择. 因为我们只有一个菜单项, 所以idCmd 总是0. 但如果我们添加了3个菜单项, idCmd 可能是 0, 1, 或 2. uFlags 是另一组标志(我以后会讨论到的). PwReserved 可以被忽略. pszName 指向一个由Shell拥有的缓冲区,我们将把帮助字符串拷贝进该缓冲区. cchMax 是该缓冲区的大小. 返回值是S_OK 或 E_FAIL.

GetCommandString() 也可以被调用以获取菜单项的动作( "verb") . verb 是个语言无关性字符串,它标识一个可以加于文件对象的操作。ShellExecute()的文档中有详细的解释, 而有关verb的内容足以再写一篇文章, 简单的解释是:verb 可以直接列在注册表中(如 "open" 和 "print"等字符串), 也可以由上下文菜单扩展创建. 这样就可以通过调用ShellExecute()执行实现在Shell扩展中的代码.

不管怎样, 我说了这多只是为了解释清楚GetCommandString() 的作用. 如果 Explorer 要求一个帮助字符串,我们就提供给它. 如果 Explorer 要求一个verb, 我们就忽略它. 这就是 uFlags 参数的作用. 如果 uFlags 设置了GCS_HELPTEXT 位, 则 Explorer 是在要求帮助字符串. 而且如果 GCS_UNICODE 被设置, 我们就必须返回一个Unicode字符串.

我们的 GetCommandString() 如下:

#include // 为使用 ATL 字符串转换宏而包含的头文件
HRESULT CSimpleShlExt::GetCommandString (
UINT idCmd,
UINT uFlags,
UINT* pwReserved,
LPSTR pszName,
UINT cchMax )
{
USES_CONVERSION;
//检查 idCmd, 它必须是0,因为我们仅有一个添加的菜单项.
if ( 0 != idCmd )
return E_INVALIDARG;
// 如果 Explorer 要求帮助字符串, 就将它拷贝到提供的缓冲区中.
if ( uFlags & GCS_HELPTEXT )
{
LPCTSTR szText = _T("This is the simple shell extension''s help");
if ( uFlags & GCS_UNICODE )
{
// 我们需要将 pszName 转化为一个 Unicode 字符串, 接着使用Unicode字符串拷贝 API.
lstrcpynW ( (LPWSTR) pszName, T2CW(szText), cchMax );
}
else
{
// 使用 ANSI 字符串拷贝API 来返回帮助字符串.
lstrcpynA ( pszName, T2CA(szText), cchMax );
}
return S_OK;
}
return E_INVALIDARG;
}
这里没有什么特别的代码; 我用了硬编码的字符串并把它转换为相应的字符集. 如果你从未使用过ATL字符串转化宏,你一定要学一下,因为当你传递Unicode字符串到COM和OLE函数时,使用转化宏会很有帮助的. 我在上面的代码中使用了T2CW 和 T2CA 将TCHAR 字符串分别转化为Unicode 和 ANSI字符串. 函数开头处的USES_CONVERSION 宏其实声明了一个将被转化宏使用的局部变量.

要注意的一个问题是: lstrcpyn() 保证了目标字符串将以null为结束符. 这与C运行时(CRT)函
数strncpy()不同. 当要拷贝的源字符串的长度大于或等于cchMax 时 strncpy()不会添加一个
null 结束符. 我建议总使用lstrcpyn(), 这样你就不必在每一个strncpy()后加入检查保证字符
串以 null为结束符的代码.

执行用户的选择
IContextMenu 接口的最后一个方法是 InvokeCommand(). 当用户点击我们添加的菜单项时该方法将被调用. 其函数原型是:

HRESULT IContextMenu::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo );
CMINVOKECOMMANDINFO 结构带有大量的信息, 但我们只关心 lpVerb 和 hwnd 这两个成员. lpVerb参数有两个作用 – 它或是可被激发的verb(动作)名, 或是被点击的菜单项的索引值. hwnd 是用户激活我们的菜单扩展时所在的浏览器窗口的句柄.

因为我们只有一个扩展的菜单项, 我们只要检查lpVerb 参数, 如果其值为0, 我们可以认定我们的菜单项被点击了. 我能想到的最简单的代码就是弹出一个信息框, 这里的代码也就做了这么多. 信息框显示所选的文件的文件名以证实代码正确地工作.

HRESULT CSimpleShlExt::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo )
{
// 如果lpVerb 实际指向一个字符串, 忽略此次调用并退出.
if ( 0 != HIWORD( pCmdInfo->lpVerb ))
return E_INVALIDARG;
// 点击的命令索引 – 在这里,唯一合法的索引为0.
switch ( LOWORD( pCmdInfo->lpVerb ))
{
case 0:
{
TCHAR szMsg [MAX_PATH + 32];
wsprintf ( szMsg, _T("The selected file was:\n\n%s"), m_szFile );
MessageBox ( pCmdInfo->hwnd, szMsg, _T("SimpleShlExt"),
MB_ICONINFORMATION );
return S_OK;
}
break;
default:
return E_INVALIDARG;
break;
}
}
注册Shell扩展
现在我们已经实现了所有需要的COM接口. 可是我们怎样才能让浏览器使用我们的扩展呢? ATL 自动生成注册COM DLL服务器的代码, 但这只是让其它程序可以使用我们的DLL. 为了告诉浏览器使用我们的扩展, 我们需要在文本文件类型的注册表键下注册扩展:

HKEY_CLASSES_ROOT\txtfile

在这个键下, 有个名为 ShellEx 的键保存了一个与文本文件关联的Shell扩展的列表. 在 ShellEx 键下, ContextMenuHandlers 键保存了上下文菜单扩展的列表. 每个扩展都在ContextMenuHandlers下创建了一个子键并将其默认值设为扩展COM的GUID. 所以, 对于我们这个简单的扩展, 我们将创建下键:

HKEY_CLASSES_ROOT\txtfile\ShellEx\ContextMenuHandlers\SimpleShlExt

并将其默认值设为我们的 GUID: "{5E2121EE-0300-11D4-8D3B-444553540000}".

你不必写任何代码就可以完成注册操作. 如果你看一下Fileview页的文件列表, 你会看到SimpleShlExt.rgs. 该文本文件将被ATL分析, 并指导ATL在该COM服务器注册时添加附加的注册键, 而注销时又该删除哪些键. 以下是所指定添加的注册表项:

HKCR
{
NoRemove txtfile
{
NoRemove ShellEx
{
NoRemove ContextMenuHandlers
{
ForceRemove SimpleShlExt = s ''{5E2121EE-0300-11D4-8D3B-444553540000}''
}
}
}
}
每一行代表一个注册表键, "HKCR"是 HKEY_CLASSES_ROOT 的缩写. NoRemove 关键字表示当该COM服务器注销时该键 不用被删除. 最后一行有些复杂. ForceRemove 关键字表示如果该键已存在, 那么在新键添加之前该键先应被删除. 这行脚本的余下部分指定一个字符串,它将被存为 SimpleShlExt 键的默认值.

在这我插几句话. 我们是在 HKCR\txtfile下注册的. 但是 "txtfile" 名并不是一个永久的或预定好的名称. 如果你看一下 HKCR\.txt, 该键的默认值正是txtfile. 这样就会有一些副作用:

我们不能可靠地使用 RGS 教本,因为 "txtfile" 可能不是正确的键名.
一些文本编辑软件可能安装到系统并直接关联到 .TXT 文件. 如果它改变了HKCR\.txt 键的默认值, 所有现存的Shell扩展都将停止工作.
在我看来,这确是个设计上的错误. 我认为微软也是这么想的, 因为最新的扩展类型, 如QueryInfo扩展注册在 .txt 键下.

好了,到此为止. 最后还有一个注册细节. 在NT/2000上, 我们还得将我们的扩展放到 "approved" 扩展列表中. 如果我们不这样做, 我们的扩展不会被没有管理员权限的用户调用. 该列表保存在:

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved

在该键下, 我们要创建一个以我们的GUID 为名的字符串键,键的内容任意. 与之有关的代码在DllRegisterServer() 和 DllUnregisterServer() 函数中. 只是些简单的注册表获取, 我也就不在这写出了. 你可以在例子工程代码中找到它.

调试Shell 扩展
最终你会写一个不会这么简单的扩展, 那时你就不得不进行调试. 打开你的工程设置, 在 Debug 页” Executable for debug session”编辑框中输入浏览器程序的全路径, 如:"C:\windows\explorer.exe". 如果你使用的是 NT 或 2000, 并且你已经设置了上述的 DesktopProcess 注册键, 那么当你按F5进行调试时就会打开一个新的浏览器窗口. 只要你在这个窗口内完成你所有的工作,当你关闭该窗口时扩展同时会被卸出内存,这样就不会防碍我们重建 DLL了.

在Windows 9x上, 在重新调试之前你不得不关闭Shell. 你可以: 点击 “开始”, 再点击”关闭”. 按住 Ctrl+Alt+Shift 并点击”取消”. 这样就会关闭Shell, 你会看到桌面消失了. 接着,你可以切换到 MSVC 再按 F5进行调试. 要中止调试, 按 Shift+F5 关闭. 完成调试后, 你可以从”开始 运行” Explorer.exe以正常重起.

第二节 - 如何编写一次操作多个文件对象的Shell扩展
在 第一节中, 我介绍了如何编写简单的Shell扩展, 并给出了一个简单的一次仅处理单个选择文件的上下文菜单扩展例子. 在本节中, 我将示范如何在一次操作中处理多个被选文件. 本扩展是一个用于注册和注销COM服务器的工具. 该例子也示范了如何使用ATL对话框类 CDialogImpl. 在本节末尾我将讲述一些特殊的注册键,利用之你可以使你的扩展被所有的文件类型激活而不只是事先定好的文件类型.

第二节假设你已经读过 第一节 ,你应该了解上下文菜单扩展的基本内容. 你也必须理解基础的COM, ATL, 还有STL 集合类的知识.

开始编写上下文菜单扩展 – 它该作些什么?
这个 Shell扩展可以让你注册以EXE, DLL, 和 OCX文件形式提供的COM服务器. 不同于 第一节 中的扩展, 该扩展将一次处理右击选择的所有文件.

使用 AppWizard 开始
运行 AppWizard 并生成一个名为DllReg 的ATL工程. 保留所有的默认设置, 点击”完成”. 然后在ClassView树中右击DllReg classes 项,在弹出的菜单中选择New ATL Object ,添加一个COM 对象到 DLL中.

在 ATL Object Wizard 中, 第一页已经选中了 Simple Object , 因此单击 Next. 在第二页中, 在Short Name编辑框中输入DllRegShlExt点击 OK. (其余编辑框中的内容将自动填写.) 这样就创建了一个名为 CdllRegShlExt 的已实现基本COM接口的新类. 我们将在该类中添加我们的代码.

初始化接口
我们的 IShellExtInit::Initialize() 实现与上一节大不相同, 有两方面的原因. 第一, 我们将列举所有被选的文件. 第二, 我们将检查所选择的文件是否输出了注册和注销的函数功能. 我们将只处理那些输出 DllRegisterServer() 和 DllUnregisterServer()函数的文件对象. 其余的文件将被忽略.

我们将使用列表视控件、STL 字符串和列表类, 所以你必须添加以下几行代码到 stdafx.h 文件中:
#include
#include
#include
#include
typedef std::list > string_list;

我们的 CDllRegShlExt 类需要一些成员变量:
protected:
HBITMAP m_hRegBmp;
HBITMAP m_hUnregBmp;
string_list m_lsFiles;
TCHAR m_szDir [MAX_PATH];
CDllRegShlExt 构造器加载两个位图供上下文菜单使用:
CDLLRegShlExt::CDLLRegShlExt()
{
m_hRegBmp = LoadBitmap ( _Module.GetModuleInstance(),
MAKEINTRESOURCE(IDB_REGISTERBMP) );
m_hUnregBmp = LoadBitmap ( _Module.GetModuleInstance(),
MAKEINTRESOURCE(IDB_UNREGISTERBMP) );
}
在你添加 IShellExtInit 接口到 CDllRegShlExt 接口列表中后(参看 第一节 的说明), 我们开始编写 Initialize() 函数.Initialize() 将执行这些步骤:

改变当前工作目录为所查看的浏览器窗口目录。
列举所有被选择的文件.
对于每一个文件, 试着用LoadLibrary()加载.
如果 LoadLibrary() 成功了, 查看文件是否输出 DllRegisterServer()及 DllUnregisterServer().
如果输出了这两个函数, 添加该文件名到我们的文件列表m_lsFiles中去.
HRESULT CDllRegShlExt::Initialize ( LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hProgID ){TCHAR szFile [MAX_PATH];TCHAR szFolder [MAX_PATH];TCHAR szCurrDir [MAX_PATH];TCHAR* pszLastBackslash;UINT uNumFiles;HDROP hdrop;FORMATETC etc = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };STGMEDIUM stg = { TYMED_HGLOBAL };HINSTANCE hinst;bool bChangedDir = false;HRESULT (STDAPICALLTYPE* pfn)();一大堆繁琐的变量! 第一步是从传进来的 pDataObj 参数中获取 HDROP 句柄. 这些与 第一节 没什么区别. (实在太罗嗦了!)
// 从数据对象中读取文件列表. 他们存储在HDROP 格式中
// 因此,取得 HDROP 句柄,并使用拖放API
if ( FAILED( pDO->GetData ( &etc, &stg )))
return E_INVALIDARG;
//取得HDROP 句柄.
hdrop = (HDROP) GlobalLock ( stg.hGlobal );
if ( NULL == hdrop )
{
ReleaseStgMedium ( &stg );
return E_INVALIDARG;
}
// 检查在该操作中有几个文件被选择.
uNumFiles = DragQueryFile ( hdrop, 0xFFFFFFFF, NULL, 0 );
接着是获取文件名的循环代码 (使用 DragQueryFile()) 并试用LoadLibrary()加载. 实际附带的例子中的代码事先改变了当前目录, 在这里我忽略它,因为它实在太冗长了.
for ( UINT uFile = 0; uFile < uNumFiles; uFile++ )
{
//取得下一个文件名.
if ( 0 == DragQueryFile ( hdrop, uFile, szFile, MAX_PATH ))
continue;
//试着加载文件.
hinst = LoadLibrary ( szFile );
if ( NULL == hinst )
continue;

接着, 我们将查看该模块是否输出两个必须的函数.
// 获取 DllRegisterServer()函数的地址;
(FARPROC&) pfn = GetProcAddress ( hinst, "DllRegisterServer" );
// 如果没找着, 跳过该文件.
if ( NULL == pfn )
{
FreeLibrary ( hinst );
continue;
}
// 获取DllUnregisterServer()函数的地址;
(FARPROC&) pfn = GetProcAddress ( hinst, "DllUnregisterServer" );
// 如果有,我们就可以处理该文件, 因此添加其到我们的文件名列表中去(m_lsFiles).
if ( NULL != pfn )
{
m_lsFiles.push_back ( szFile );
}
FreeLibrary ( hinst );
} // end for

最后添加文件名到一个保存字符串的STL 列表集合变量m_lsFiles. 该列表在接下来将被使用, 我们将轮循所有的文件并进行注册或注销操作.

Initialize() 中要做的最后一件事是释放所使用的资源并返回正确的值给浏览器.
// 释放资源.
GlobalUnlock ( stg.hGlobal );
ReleaseStgMedium ( &stg );
// 如果我们找到可以操作的文件, 返回 S_OK.否则,返回E_INVALIDARG,我们的代码就不会再被右击事件调用。
return ( m_lsFiles.size() > 0 ) ? S_OK : E_INVALIDARG;
}
如果你看一下例子代码, 你会看到我不得不通过检查文件名来判断当前查看的浏览器目录. 你可能奇怪为什么我不简单地使用 pidlFolder 参数,文档上说它是 "包含所选择点击的文件对象的目录的标识符列表(Identifier List)" 嗯, 当我在Windows 98进行调试时, 该参数总是为空 NULL, 所以它毫无用处.

添加我们的菜单项
接下来是 IContextMenu 的方法. 与先前一样, 你得加入ContextMenu 接口到 CDllRegShlExt 实现的接口列表中. 而这些操作也正如 第一节 中的步骤.

我们的 QueryContextMenu() 实现的开始如第一节. 我们检查uFlags, 如果CMF_DEFAULTONLY 标志被设置就立即返回.

HRESULT CDLLRegShlExt::QueryContextMenu ( HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags ){UINT uCmdID = uidFirstCmd; // 如果 CMF_DEFAULTONLY 标志被设置我们不作任何操作. if ( uFlags & CMF_DEFAULTONLY ) { return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 ); }接着, 我们添加 "Register servers" 菜单项. 这儿有些新东西: 我们为每个菜单项设置一个位图. 这与 WinZip 所作的一样,即放置一个图标在菜单左方.
// 添加 register/unregister 项.
InsertMenu ( hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION, uCmdID++,
_T("Register server(s)") );
// 为register项设置位图.
if ( NULL != m_hRegBmp )
{
SetMenuItemBitmaps ( hmenu, uMenuIndex, MF_BYPOSITION, m_hRegBmp, NULL );
}
uMenuIndex++;

SetMenuItemBitmaps() API用来在菜单项旁边显示小齿轮图标 . 注意 uCmdID 被递增了, 所以下一次调用InsertMenu()添加的菜单项的命令ID比上一个大1. 最后, uMenuIndex 也递增了,这样第二个菜单项将显示在第一个后.

对于第二个菜单项, 添加的代码与以上大致相同.
InsertMenu ( hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION, uCmdID++,
_T("Unregister server(s)") );
// 设置 unregister 项的位图.
if ( NULL != m_hUnregBmp )
{
SetMenuItemBitmaps ( hmenu, uMenuIndex, MF_BYPOSITION, m_hUnregBmp, NULL
);
}
最后, 我们告诉浏览器我们添加了几个菜单项.
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 2 );

提供帮助提示和动作 verb
正如先前, GetCommandString() 方法在浏览器需要显示帮助信息或取得动作(verb)命令ID时被调用. 该扩展有两个菜单项, 所以我们需要检查uCmdID 参数以确定哪一个菜单项被点击调用.

#include HRESULT CDLLRegShlExt::GetCommandString ( UINT uCmdID, UINT uFlags, UINT* puReserved, LPSTR szName, UINT cchMax ){LPCTSTR szPrompt; USES_CONVERSION; if ( uFlags & GCS_HELPTEXT ) { switch ( uCmdID ) { case 0: szPrompt = _T("Register all selected COM servers"); break; case 1: szPrompt = _T("Unregister all selected COM servers"); break; default: return E_INVALIDARG; break; }如果 uCmdID 为 0, 表示是第一个菜单项 (register)被点击调用. 如果为 1,是第二项(unregister)被调用. 我们在决定了帮助字符串后, 将之拷贝到提供的缓冲区中, 必要时转化为 Unicode 格式.
// 拷贝帮助字符串到提供的缓冲区中. 如果Shell需要Unicode字符串,我们需要转化szName 到 LPCWSTR.
if ( uFlags & GCS_UNICODE )
{
lstrcpynW ( (LPWSTR) szName, T2CW(szPrompt), cchMax );
}
else
{
lstrcpynA ( szName, T2CA(szPrompt), cchMax );
}
}

在这个扩展中, 我也实现了一个动作(verb). 然而, 当在 Windows 98上测试时, 浏览器从不会调用 GetCommandString() 来获取动作(verb). 我也写了一个测试程序,对一个DLL文件调用 ShellExecute() 并试着使用动作(verb), 但也不能工作. 我不知道NT上的行为是否不同. 我在这忽略了与此有关的代码, 如果你有兴趣可以去看例子代码.

执行用户选择
当用户点击菜单项, 浏览器调用我们的 InvokeCommand() 方法. InvokeCommand() 函数首先检查 lpVerb 的高字. 如果非0, 其值就是要调用的动作(verb)名. 而我们知道动作不能正确地工作(至少在Win 98上), 所以不用处理. 否则, 如果 lpVerb 的低字为 0 或 1, 我们就知道有一个定制的菜单项被点击了.

HRESULT CDllRegShlExt::InvokeCommand ( LPCMINVOKECOMMANDINFO pCmdInfo ){ // 如果 lpVerb 指向一字符串, 忽略此次调用. if ( 0 != HIWORD( pInfo->lpVerb )) return E_INVALIDARG; // 检查lpVerb 是不是我们添加的命令(0 或 1) switch ( LOWORD( pInfo->lpVerb )) { case 0: case 1: { CProgressDlg dlg ( &m_lsFiles, pInfo ); dlg.DoModal(); return S_OK; } break; default: return E_INVALIDARG; break; }}如果 lpVerb 为 0 或 1, 我们创建一个进度对话框 (从ATL的CdialogImpl类派生), 并将文件名列表传给它.

所有实质的工作发生在 CProgressDlg 类中. 它的 OnInitDialog() 函数初始化列表控件, 并调用CProgressDlg::DoWork(). DoWork() 轮循在 CDllRegShlExt::Initialize()时创建的字符串列表, 并对每一个文件调用合适的函数. 基本的代码如下所示; 这里的代码并不完整,我忽略了错误检查代码, 以及填充列表控件的代码. 这里的代码刚好够说明如何轮循列表中的文件名并进行操作.

void CProgressDlg::DoWork(){HRESULT (STDAPICALLTYPE* pfn)();string_list::const_iterator it, itEnd;HINSTANCE hinst;LPCSTR pszFnName;HRESULT hr;WORD wCmd; wCmd = LOWORD ( m_pCmdInfo->lpVerb ); // 我们只支持两个命令, 所以检查传进来的lpVerb值. if ( wCmd > 1 ) return; // 决定我们该调用哪个命令. 注意这些字符串没有使用 _T 宏, 因为 GetProcAddress() 只接收一个 // ANSI 函数名字符串. pszFnName = wCmd ? "DllUnregisterServer" : "DllRegisterServer"; for ( it = m_pFileList->begin(), itEnd = m_pFileList->end(); it != itEnd; it++ ) { // 试着加载下一个文件. hinst = LoadLibrary ( it->c_str() ); if ( NULL == hinst ) continue; // 取得 register/unregister函数地址. (FARPROC&) pfn = GetProcAddress ( hinst, pszFnName ); // 如果没找到, 跳到下一个文件go on to the next file. if ( NULL == pfn ) continue; // 调用注册函数! hr = pfn();我要解释一下 for 循环, 因为如果你没有使用过 STL 集合类,它是有点古怪. m_pFileList 是个指向m_lsFiles 变量的指针. (该指针通过 CProgressDlg 的构造函数被传递.) STL list 集合有一种const_iterator类型, 它是一个类似于MFC中的POSITION 类型的抽象实体. 一个 const_iterator 变量在行为上类似于一个列表中的const对象的指针, 所以这个迭代器可以使用 -> 反引用以获取对象本身. 一个迭代器也可以使用++ 递增以在列表中移动指针.

所以, for 循环的初始代码调用list::begin() 取得指向列表中第一个字符串的迭代器, 而且调用 list::end() 取得指向列表末尾的迭代器, 即位于最后一个字符串之尾. (我将术语放在引号中以强调指向,开始,结束的概念,这些概念都被 const_iterator 类型所抽象化,而且必须通过const_iterator 的相应方法来获取 [如 begin()] 或进行操作 [如 ++].) 这两个迭代器分别赋值给it 和 itEnd. 循环一直继续到it 等于 itEnd; 亦即, 当 it 还没到列表末尾. 迭代器 it 将每次向前递增, 每次它都前进一个字符串.

表达式 it->c_str() 使用 -> 操作符. 因为it 的行为 正如一个 string 的指针(记住, m_pFileList 是STL字符串列表的指针), it->c_str() 调用 string类的 c_str() 函数. c_str() 返回一个C风格的字符串指针, 在这里其等同于LPCTSTR .

DoWork() 的剩余部分是清理和错误处理. 你可以在例子中的ProgressDlg.cpp 中找到完整的代码.

(我刚意识到谈论一个名为”it”的变量是多奇怪. 抱歉.) :)

注册 Shell扩展
DllReg 扩展对可执行文件进行操作, 所以其应该被注册到EXE, DLL,和OCX文件类型. 如第一节所说, 我们可以通过RGS脚本文件来完成, DllRegShlExt.rgs. 以下是注册我们的DLL为上下文菜单扩展所需的脚本.

HKCR
{
NoRemove dllfile
{
NoRemove shellex
{
NoRemove ContextMenuHandlers
{
ForceRemove DLLRegSvr = s '{8AB81E72-CB2F-11D3-8D3B-AC2F34F1FA3C}'
}
}
}
NoRemove exefile
{
NoRemove shellex
{
NoRemove ContextMenuHandlers
{
ForceRemove DLLRegSvr = s '{8AB81E72-CB2F-11D3-8D3B-AC2F34F1FA3C}'
}
}
}
NoRemove ocxfile
{
NoRemove shellex
{
NoRemove ContextMenuHandlers
{
ForceRemove DLLRegSvr = s '{8AB81E72-CB2F-11D3-8D3B-AC2F34F1FA3C}'
}
}
}
}RGS文件的格式及关键字NoRemove 和ForceRemove 的含义已在第一节中解释过.

正如上一节的例子一样,在 NT/2000上我们需要添加我们的扩展到 "approved" 扩展列表中去. 完成该工作的代码在 DllRegisterServer() 和 DllUnregisterServer() 函数中.我不在这写出这些代码, 那只是简单的注册表获取, 你可以在例子工程代码中找到它.

注册扩展的其它方法
到目前为止, 我们的扩展只被特定的文件类型调用. 但是我们可以通过在HKCR\* 键下注册上下文菜单扩展使我们的扩展被所有文件类型调用:

HKCR{ NoRemove * { NoRemove shellex { NoRemove ContextMenuHandlers { ForceRemove DLLRegSvr = s '{8AB81E72-CB2F-11D3-8D3B-AC2F34F1FA3C}' } } }}HKCR\* 键下列出了适用于所有文件类型的Shell扩展. 注意,文档上说此类扩展将被所有Shell对象激发 (如 文件, 目录, 虚拟文件夹, 控制面板, 等等.), 但是在我调试时并不是这样的. 该扩展只被实际的物理文件所激发.

当shell 版本为4.71+时, 这里还有个名为 HKCR\AllFileSystemObjects的键. 如果我们在这个键下注册, 我们的扩展将被文件系统中的所有的文件和文件夹所激发, 除了根目录. (要被根目录激发应该在 HKCR\Drive 下注册.) 然而, 这样做时,会出现一些反常的的行为. 因为发送到菜单也使用这个键, 所以DllReg 菜单项与 发送到菜单项交叉在一起:

你也可以写一个操作目录的上下文菜单扩展. 关于这个例子,请参考拙著 A Utility to Clean Up Compiler Temp Files.

最后,在shell版本 4.71+中, 你可以让上下文菜单在用户右击浏览器窗口(包括桌面)的背景时激发. 要让你的扩展在这种情况下被激发,需要在HKCR\Directory\Background\shellex\ContextMenuHandlers 键下进行注册. 使用该方法, 你可以添加定制菜单到桌面或任意目录上下文菜单. 这时传送到 IShellExtInit::Initialize()的参数有些不同,所以我将在以后的文章中讲述这方面的内容.

第三节-如何编写为文件对象弹出信息框的Shell扩展

在第 一 和 二 节中, 我说明了如何编写上下文菜单扩展. 在第三节中, 我将示范一种新的扩展类型, 解释如何使用Shell进程的内存, 及如何在ATL中使用MFC.

第三节假设你理解Shell扩展的基础知识 (参见 第一节), 并熟悉 MFC. 注意这一节的扩展需要 4.71或更高的版本, 所以你必须运行在 Windows 98 或 2000, 或者在 95 或 NT 4下安装活动桌面特性.

QueryInfo扩展
活动桌面引入一项新特性, 当你在某些特定对象上盘旋鼠标时,工具提示将显示它们的描述. 例如, 在我的电脑上盘旋鼠标时将显示如下提示:

其它对象如网络邻居和控制面板都有类似的提示. 我们可以使用QueryInfo扩展为Shell中的其它对象提供自定义的工具提示.

"QueryInfo 扩展"名称的含义: 这名称是我自己起的; 我用其使用的接口 IqueryInfo来命名该扩展. 到目前为止, 它还没有一个正式的名称. 我看了一下1999年10月的 MSDN 也没发现关于这种扩展的任何介绍! 但它确实是一个被支持的扩展,因为 Microsoft Office 也为它的文件类型安装了 QueryInfo 扩展, 如下所示:

WinZip 版本 8 也为压缩文件类型安装有 QueryInfo 扩展:

我发现这方面最好的文档是MSDN杂志2000年3月的 Dino Esposito的文章 "使用新的工具提示和图标覆盖Shell扩展改善你的用户界面" .

开始编写上下文菜单 – 它该做些什么?
本文的Shell扩展是个文本文件的快速浏览工具 – 它会显示文本文件的第一行以及文件大小. 我们的信息将显示在工具提示窗口中,当用户在 TXT 文件上盘旋鼠标.

使用 AppWizard 开始
运行 AppWizard 并生成一个名为 TxtInfo的ATL工程. 由于这次我们要使用MFC,所以要选中 Support MFC设置, 点击”完成”. 然后,在ClassView树中右击TxtInfo classes 项,在弹出的菜单中选择New ATL Object,添加一个COM 对象类到 DLL中.

在 ATL Object Wizard 中, 第一页面已经选中了 Simple Object , 因此, 单击 Next. 在第二页面中, 在Short Name编辑框中输入 TxtInfoShlExt点击 OK. (其余编辑框中的内容将自动填写.) 这样就创建了一个名为 CTxtInfoShlExt 的已实现基本的COM接口的新COM对象类. 我们将在这个类中添加实现代码.

如果你仔细看一下ClassView 树, 你会发现有个 CTxtInfoApp 类, 它是从CwinApp 派生的. 该类和全局变量theApp, 使得我们可以使用 MFC, 正像我们写一个非ATL的平常的 MFC DLL.

初始化接口
对于每个上下文菜单扩展, 我们都要实现 IShellExtInit 接口, 这也正是浏览器初始化我们对象的地方. 有些Shell扩展使用另外一个初始化接口, IPersistFile, QueryInfo 扩展就是这样. 有何区别呢?如果你还记得, IShellExtInit::Initialize() 接收一个IDataObject 指针,使用之可以列举所选的多个文件. 而一次只处理一个文件的扩展可以使用 IPersistFile. 由于鼠标不能同时在一个以上的对象上盘旋, 所以QueryInfo扩展一次只处理一个文件,因此它使用 IPersistFile.

开始我们需要添加IPersistFile 到 CTxtInfoShlExt 实现的接口列表中.打开 TxtInfoShlExt.h, 并添加如下代码:

#include #include class ATL_NO_VTABLE CTxtInfoShlExt : public CComObjectRootEx, public CComCoClass, public IDispatchImpl, public IPersistFile{BEGIN_COM_MAP(CTxtInfoShlExt) COM_INTERFACE_ENTRY(ITxtInfoShlExt) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY(IPersistFile)END_COM_MAP()我们需要一个保存浏览器给出的文件名的变量:protected: // ITxtInfoShlExt CString m_sFilename;注意我们可以在任何地方使用 MFC 对象.如果你看一下 IpersistFile 的文档, 你会看到很多方法. 幸运的是, 对于Shell扩展, 我们只用实现Load(), 而忽略其它方法. 以下是 IPersistFile 方法的原型:

public: // IPersistFile STDMETHOD(GetClassID)(LPCLSID) { return E_NOTIMPL; } STDMETHOD(IsDirty)() { return E_NOTIMPL; } STDMETHOD(Load)(LPCOLESTR, DWORD); STDMETHOD(Save)(LPCOLESTR, BOOL) { return E_NOTIMPL; } STDMETHOD(SaveCompleted)(LPCOLESTR) { return E_NOTIMPL; } STDMETHOD(GetCurFile)(LPOLESTR*) { return E_NOTIMPL; }除开 Load() 外的方法都只返回 E_NOTIMPL 以表明我们没有实现它们.更妙的是, Load() 方法也相当简单. 我们只需保存浏览器传给我们的文件名. 也就是当前鼠标在其上盘旋的文件.

HRESULT CTxtInfoShlExt::Load ( LPCOLESTR wszFilename, DWORD dwMode ){ AFX_MANAGE_STATE(AfxGetStaticModuleState()); // init MFC // 让CString 自动转化文件名为 ANSI 字符. m_sFilename = wszFilename; return S_OK;}请注意函数的第一行. 要让 MFC 正确地工作该行代码是必要的. 由于我们的 DLL 要被非MFC 程序所调用, 任一个使用MFC的输出函数必须手动初始化 MFC. 如果你不写这行代码, 则许多MFC函数 (大多是与资源处理有关的函数) 将失败或出错.

文件名被保存在 m_sFilename 以备后用. 注意我利用了 CString 的赋值操作符的特性来转化字符串为ANSI格式 - 如果该 DLL 以 ANSI方式建立.

创建工具提示的文本
在浏览器调用了我们的 Load() 方法之后, 它接着调用 QueryInterface() 获取另一个接口: IQueryInfo. IQueryInfo 是个相当简单的接口,只有两个接口 (而其中也只有一个被真正使用). 打开 TxtInfoShlExt.h ,添加如下标红的代码:

class ATL_NO_VTABLE CTxtInfoShlExt : public CComObjectRootEx, public CComCoClass, public IDispatchImpl, public IPersistFile, public IQueryInfo{BEGIN_COM_MAP(CTxtInfoShlExt) COM_INTERFACE_ENTRY(ITxtInfoShlExt) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY(IPersistFile) COM_INTERFACE_ENTRY(IQueryInfo)END_COM_MAP()然后添加 IQueryInfo 方法的实现:
// IQueryInfo
STDMETHOD(GetInfoFlags)(DWORD*) { return E_NOTIMPL; }
STDMETHOD(GetInfoTip)(DWORD, LPWSTR*);
GetInfoFlags() 方法当前并不使用, 所以我们只返回 E_NOTIMPL. GetInfoTip() 让我们返回工具提示文本
字符串. 首先是开头繁琐的代码:
HRESULT CTxtInfoShlExt::GetInfoTip (
DWORD dwFlags,
LPWSTR* ppwszTip )
{
AFX_MANAGE_STATE(AfxGetStaticModuleState()); // init MFC
LPMALLOC pMalloc;
CStdioFile file;
DWORD dwFileSize;
CString sFirstLine;
BOOL bReadLine;
CString sTooltip;
USES_CONVERSION;
}
接着, AFX_MANAGE_STATE 首先被调用以初始化 MFC. 这是每个函数都该做的第一件事, 甚至应该在变量声明之前,因为MFC构造函数可能调用其它的 MFC 函数.

dwFlags 当前并不被使用. ppwszTip 是个 LPWSTR (Unicode 字符串指针) 变量的指针,我们要将其赋值为我们所分配的字符串缓冲区的指针.(指向指针的指针)

首先, 我们试着打开文件读取. 由于我们在 Load() 中保存了文件名,现在就可以使用了.
if ( !file.Open ( m_sFilename , CFile::modeRead | CFile::shareDenyWrite ))
return E_FAIL;

现在, 我们需要使用Shell的内存分配器分配一个缓冲, 我们通过 SHGetMalloc() 函数获取一个 IMalloc 接口. :

if ( FAILED( SHGetMalloc ( &pMalloc ))) return E_FAIL;关于Imalloc 稍后我有更多的要说. 下一步是取得文件大小并读取第一行:
// 取得文件大小.
dwFileSize = file.GetLength();
// 读取第一行.
bReadLine = file.ReadString ( sFirstLine );
bReadLine 总是为真, 除非文件不可获取或长度为 0 . 下一步是创建工具提示的第一部分:文件大小.
sTooltip.Format ( _T("File size: %lu"), dwFileSize );

现在, 我们读取第一行并添加到工具提示中.
if ( bReadLine )
{
sTooltip += _T("\n");
sTooltip += sFirstLine;
}

现在我们完成了工具提示, 我们要分配一个缓冲.在这我们将使用 Imalloc 接口. 由 SHGetMalloc() 返回的指针是一个Shell的Imalloc接口指针的拷贝. 我们用这个接口分配的任何内存都位于Shell的进程空间内, 所以Shell可以使用它. 更重要的是, Shell可以释放它. 所以我们所作的就是分配缓冲区,然后忘掉它. Shell将在完成操作时释放该内存.

要认识到的一件事是我们返回给Shell的字符串必须是 Unicode 格式的. 这就是为什么下面的Alloc()中的计算要乘以 sizeof(wchar_t); 只分配lstrlen(sToolTip)长的内存只够一半所需的内存.

*ppwszTip = (LPWSTR) pMalloc->Alloc ( (1 + lstrlen(sTooltip)) * sizeof(wchar_t) ); if ( NULL == *ppwszTip ) { pMalloc->Release(); return E_OUTOFMEMORY; } // 使用 Unicode 字符串拷贝函数将工具提示文本拷入缓冲区. wcscpy ( *ppwszTip, T2COLE((LPCTSTR) sTooltip) );最后我们释放先前获取得 IMalloc 接口.
pMalloc->Release();
return S_OK;
}

完了! 浏览器将从 *ppwszTip 中获得字符串并显示在工具提示上.

注册Shell扩展
QueryInfo 扩展的注册与上下文菜单有所不同. 我们的扩展注册在 HKEY_CLASSES_ROOT 下的一个以文件扩展名为名称的子键. 在这个例子中是 HKCR\.txt. 但等一等, 有些奇怪! 你可能认为 ShellEx 子键会是某些有意义的字符串如 "TooltipHandlers". 但,这个键名为 "{00021500-0000-0000-C000-000000000046}".

我认为微软是对我们有意隐瞒一些Shell扩展! 如果你研究一下注册表, 你会发现其它的一些以GUID 为名的ShellEx 子键. 上面的GUID 恰巧就是IqueryInfo 的GUID of .

不论怎样, 以下是我们的Shell扩展所需的 RGS 脚本文件:

HKCR{ NoRemove .txt { NoRemove shellex { NoRemove {00021500-0000-0000-C000-000000000046} = s ''{F4D78AE1-05AB-11D4-8D3B-444553540000}'' } }}你也可以通过改变".txt"为你想要的扩展名,而让扩展为其它文件类型所激发. 不幸的是,你不能在* 或 AllFileSystemObjects 下注册 QueryInfo 扩展来让你的扩展被所有文件类型激发.

正如上一节的例子一样,在 NT/2000上我们需要添加我们的扩展到 "approved" 扩展列表中去. 完成该工作的代码在 DllRegisterServer() 和 DllUnregisterServer() 函数中.我不在这写出这些代码, 因为这只是简单的注册表获取, 你可以在例子工程代码中找到它.

第四节 - 如何编写提供定制拖放功能的Shell扩展

在 第一节 和 第二节 中, 我示范了如何编写上下文菜单扩展. 在这一节中, 我将说明另一扩展类型, 拖放目标处理器,添加右键拖放文件时弹出的菜单项. 我也将演示更多使用 MFC 的例子.

第四节假设你理解Shell扩展的基础知识 (参见 第一节), 并熟悉 MFC. 该扩展是一个在Windows 2000上创建硬链接(hard link)的工具, 但即使你没有运行Windows 2000也仍可以继续. 这些代码使用了一些版本4.71的shlwapi.dll的函数, 因此你需要 IE 4 或更高 (但你不必需要安装活动桌面)的版本.

拖放目标处理器
正如每个高级用户所知,你可以在浏览器中用右键拖放文件. 当你松开鼠标键时, 浏览器会弹出一个上下文菜单列出所有可能的操作. 通常有移动,复制,和创建快捷方式:

浏览器让我们使用拖放目标扩展处理器添加菜单项到该菜单中. 这个类型的扩展当任何拖放操作发生时被激发, 并且当必要时可以添加菜单项. 一个拖放目标扩展处理器的例子是 WinZip. 以下是 WinZip 所添加的拖放菜单项:

WinZip的扩展对任何一个拖放操作都激活, 但它只在压缩文件被拖放时才添加菜单项.

开始编写上下文菜单 – 它该做些什么?
这个Shell扩展将使用新的Windows 2000 API CreateHardLink() 来创建到NTFS卷上的文件的硬链接. 我们将添加一个定制菜单项来建立链接, 所以用户可以用常规的创建快捷方式的方法来创建硬链接.

使用 AppWizard 开始
运行 AppWizard 并生成一个名为 HardLink的ATL工程. 由于这次我们要使用MFC,所以要选中 Support MFC设置, 点击”完成”. 然后,在ClassView树中右击 HardLink classes 项,在弹出的菜单中选择New ATL Object,添加一个COM 对象类到 DLL中.

在 ATL Object Wizard 中, 第一页面已经选中了 Simple Object , 因此, 单击 Next. 在第二页面中, 在Short Name编辑框中输入HardLinkShlExt点击 OK. (其余编辑框中的内容将自动填写.) 这样就创建了一个名为CHardLinkShlExt的已实现基本的COM接口的新COM对象类. 我们将在这个类中添加实现代码.

初始化接口
如我们前面的上下文菜单扩展一样, 浏览器通过IShellExtInit 接口让我们进行初始化.我们需要添加IShellExtInit 接口到 CHardLinkShlExt 实现的接口列表中. 打开 HardLinkShlExt.h ,并添加如下标红的代码:

#include #include class ATL_NO_VTABLE CTxtInfoShlExt : public CComObjectRootEx, public CComCoClass, public IDispatchImpl, public IShellExtInit{BEGIN_COM_MAP(CTxtInfoShlExt) COM_INTERFACE_ENTRY(ITxtInfoShlExt) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY(IShellExtInit)END_COM_MAP()public: // IShellExtInit STDMETHOD(Initialize)(LPCITEMIDLIST, LPDATAOBJECT, HKEY);我们需要一些变量来保存位图及被拖放的的文件名:protected: // IHardLinkShlExt CBitmap m_bitmap; TCHAR m_szFolderDroppedIn [MAX_PATH]; CStringList m_lsDroppedFiles;我们也需要在 stdafx.h中添加一些 #define 以方便获取 CreateHardLink() 及 shlwapi.dll 输出的函数原型:

#define WINVER 0x0500#define _WIN32_WINNT 0x0500#define _WIN32_IE 0x0400定义WINVER 为0x0500 使得可以使用 Win 98 和 2000的一些特性, 而定义 _WIN32_WINNT 为 0x0500 使得可以使用 Win 2000独有的特性. 定义 _WIN32_IE 为 0x0400 使得可以利用 IE 4的特性功能.

现在, 讨论Initialize() 方法. 这一次, 我将示范怎样使用 MFC 获取被拖放的文件列表. MFC 有个类, COleDataObject, 其包装了IDataObject 接口. 先前, 我们不得不直接调用 IDataObject 方法. 担幸运的是, MFC 使得这些工作简单了很多. 以下是 Initialize()的原型:

HRESULT IShellExtInit::Initialize ( LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hProgID );对于拖放目标扩展处理扩展, pidlFolder 是被拖放的项目所在的文件夹. (稍后我将深入讨论PIDL.) pDataObj 是个 IDataObject 接口指针,使用其我们可以列举被拖放的所有项目. hProgID 是个打开的我们的Shell扩展的注册键.

第一步我们为菜单加载位图. 接着, 我们将传进来的 IDataObject 接口指针赋值给ColeDataObject变量.

HRESULT CHardLinkShlExt::Initialize ( LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hProgID ){ AFX_MANAGE_STATE(AfxGetStaticModuleState()); // init MFC COleDataObject dataobj; HDROP hdrop; TCHAR szRoot [MAX_PATH]; TCHAR szFileSystemName [256]; TCHAR szFile [MAX_PATH]; UINT uFile, uNumFiles; m_bitmap.LoadBitmap ( IDB_LINKBITMAP ); dataobj.Attach ( pDataObj, FALSE );传递 FALSE 作为第二个参数给 Attach() 意思是当dataobj 变量析构时不要释放IDataObject 接口. 下一步要取得被拖放项目所在的文件夹. 我们有该文件夹的 PIDL , 但我们如何能取得路径名? 这要另外花点时间讲...

"PIDL" 是 pointer to an ID list 的缩写. 一个PIDL 唯一地标识在Shell等级结构空间中的任一对象. 每个在Shell空间中的对象, 不论它是不是真实文件系统的一部分, 都有个 PIDL. PIDL的准确结构依赖于对象所处的位置, 但除非你正写一个你自己的名字空间扩展, 你不用去考虑 PIDL的内部结构.

我们可以使用Shell API 来从 PIDL 中解析出路径名. SHGetPathFromIDList() 函数实现此功能. 如果目标目录不是真实文件系统中的目录(例如, 控制面板目录), SHGetPathFromIDList() 将会返回失败.

if ( !SHGetPathFromIDList ( pidlFolder, m_szFolderDroppedIn )) { return E_FAIL; }接着,我们检查目标文件夹是否在 NTFS 卷上. 我们获取路径名的根目录部分 (如, E:\), 并获取该卷的信息. 若其不在NTFS上, 我们不能生成硬链接.简单返回.

lstrcpy ( szRoot, m_szFolderDroppedIn ); PathStripToRoot ( szRoot ); if ( !GetVolumeInformation ( szRoot, NULL, 0, NULL, NULL, NULL, szFileSystemName, 256 )) { // 不能决定文件系统类型. return E_FAIL; } if ( 0 != lstrcmpi ( szFileSystemName, _T("ntfs") )) { // 文件系统不是NTFS, 所以不支持硬链接. return E_FAIL; }接着, 我们从数据对象中获取 HDROP 句柄, 我们将使用它来列举被拖放的文件名. 这类似于第三节中使用的方法, 不同的是我们使用 MFC类来获取数据. COleDataObject 为我们处理了 FORMATETC 和 STGMEDIUM 结构的设置工作.

hglobal = dataobj.GetGlobalData ( CF_HDROP ); if ( NULL == hglobal ) return E_INVALIDARG; hdrop = (HDROP) GlobalLock ( hglobal ); if ( NULL == hdrop ) return E_INVALIDARG;我们使用 HDROP 句柄来列举被拖放的文件. 对每一个对象, 我们检查是否为一个文件夹. 由于文件夹不能被硬链接,所以要是发现有文件夹就退出不作处理.


uNumFiles = DragQueryFile ( hdrop, 0xFFFFFFFF, NULL, 0 ); for ( uFile = 0; uFile < uNumFiles; uFile++ ) { if ( DragQueryFile ( hdrop, uFile, szFile, MAX_PATH )) { if ( PathIsDirectory ( szFile )) { // 发现一个文件夹,退出. m_lsDroppedFiles.RemoveAll(); break; }我们还得检查目标文件是否也在同一个卷上. 我所做的是比较每个文件的根目录与目标文件夹, 如果不同就退出. 这不是个完善的解决方案,因为在Win 2000 上,你可以把一个卷映射到另一个卷中. 例如, 你有个 C: 卷,然后映射另一个卷为 C:\dev. 这里的代码不会拒绝建立一个从C:\dev 到C上某个地方的链接.

下面是根目录的检查代码:
if ( !PathIsSameRoot ( szFile, m_szFolderDroppedIn ))
{
// 被放置的文件来自于另外一个卷 – 退出.
m_lsDroppedFiles.RemoveAll();
break;
}

如果传进的文件都通过了检查,我们就将其加入 m_lsDroppedFiles, 它是一个 CStringList 变量.

// 添加文件名道列表. m_lsDroppedFiles.AddTail ( szFile ); } } // end for循环后, 我们释放资源并返回. 如果文件名列表不为空, 我们返回 S_OK 以表明我们需要更变上下文菜单. 否则,我们返回 E_FAIL 这样在文件拖放时我们的代码就不会被调用.

GlobalUnlock ( hglobal ); return ( m_lsDroppedFiles.GetCount() > 0 ) ? S_OK : E_FAIL;}修改上下文菜单
正如其它上下文菜单扩展, 拖放目标处理器实现 IContextMenu 接口与菜单进行交互. 要添加 IContextMenu 到我们的扩展, 打开 HardLinkShlExt.h 添加如下标红的代码:

class ATL_NO_VTABLE CHardLinkShlExt : public CComObjectRootEx, public CComCoClass, public IDispatchImpl, public IShellExtInit, public IContextMenu{BEGIN_COM_MAP(CHardLinkShlExt) COM_INTERFACE_ENTRY(IHardLinkShlExt) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY(IShellExtInit) COM_INTERFACE_ENTRY(IContextMenu)END_COM_MAP()public: // IContextMenu STDMETHOD(GetCommandString)(UINT, UINT, UINT*, LPSTR, UINT); STDMETHOD(InvokeCommand)(LPCMINVOKECOMMANDINFO); STDMETHOD(QueryContextMenu)(HMENU, UINT, UINT, UINT, UINT);浏览器将调用 QueryContextMenu() 函数来让我们修改上下文菜单. 这里没什么新鲜玩意; 我们添加一个菜单项并设好它的图标.

HRESULT CHardLinkShlExt::QueryContextMenu ( HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags ){ AFX_MANAGE_STATE(AfxGetStaticModuleState()); // init MFC // 如果标志包含 CMF_DEFAULTONLY 我们不作任何事情. if ( uFlags & CMF_DEFAULTONLY ) { return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 ); } // 添加硬链接菜单项. InsertMenu ( hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION, uidFirstCmd, _T("Create hard link(s) here") ); if ( NULL != m_bitmap.GetSafeHandle() ) { SetMenuItemBitmaps ( hmenu, uMenuIndex, MF_BYPOSITION, (HBITMAP) m_bitmap.GetSafeHandle(), NULL ); } // 返回 1 通知Shell我们添加了一个菜单项. return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 1 );}下面是新加菜单项的样子:

创建链接
如果用户点击我们的菜单项, 浏览器就调用我们的 InvokeCo... 什么? 我漏掉一个函数功能? 哦, 抱歉.

提供提示帮助
HRESULT CHardLinkShlExt::GetCommandString ( UINT idCmd, UINT uFlags, UINT* pwReserved, LPSTR pszName, UINT cchMax ){ return E_NOTIMPL;}我是认真的. :) 对于拖放目标扩展处理器, 浏览器不会调用 GetCommandString(). 现在, 回到原处...

创建链接
如我上面所说, 当用户点击我们的菜单项浏览器调用 InvokeCommand() . 我们将为所有被拖放的文件创建链接. 链接文件的名称将是 "Hard link to <文件名>", 或, 如果该文件名已被使用, "Hard link (2) to <文件名>". 这个序数将可以上升到 99的上限.

首先, 检查 lpVerb 参数,它一定是 0 ,因为我们只有一个添加的菜单项.

HRESULT CHardLinkShlExt::InvokeCommand ( LPCMINVOKECOMMANDINFO pInfo ){ AFX_MANAGE_STATE(AfxGetStaticModuleState()); // init MFCTCHAR szNewFilename [MAX_PATH+32];CString sSrcFile;TCHAR szSrcFileTitle [MAX_PATH];CString sMessage;UINT uLinkNum;POSITION pos; // 检查示是不是我们的菜单项被单击了 – lpVerb必须为0 if ( 0 != pInfo->lpVerb ) { return E_INVALIDARG; }接着, 我们获取指向字符串列表头的 POSITION 值. 一个 POSITION 是一个你不直接使用的抽象类型,而只是将其传递给 CstringList 类的其它成员函数. 它与STL iterator 类不同, iterator 有直接获取数据的操作符. 要取得列表头的 POSITION , 我们调用 GetHeadPosition():

pos = m_lsDroppedFiles.GetHeadPosition(); ASSERT ( NULL != pos );如果列表为空pos 将是 NULL, 但列表决不能为空, 所以,我加了ASSERT 来检查. 接下来循环列表中的每个文件名并为之创建链接.

while ( NULL != pos ) { // 取得下一个文件名. sSrcFile = m_lsDroppedFiles.GetNext ( pos ); // 移除该路径 – 这缩减 "C:\xyz\foo\stuff.exe" 为 "stuff.exe" lstrcpy ( szSrcFileTitle, sSrcFile ); PathStripPath ( szSrcFileTitle ); // 生成硬链接的文件名 – 我们先试一下 "Hard link to stuff.exe" wsprintf ( szNewFilename, _T("%sHard link to %s"), m_szFolderDroppedIn, szSrcFileTitle );GetNext() 返回下一个由pos指出的 CString, 并递增 pos 以指向下一个字符串. 如果 pos 已在末尾, pos 将变成 NULL (这样循环就会结束).

所以在这时, szNewFilename 存放着硬链接的全路径名. 我们检查一下该文件名是否已存在, 如果是, 我们试着将序数从2加到99, 以找到没被使用的文件名. 我们也要确认文件名长度不得超过 (包含NULL结束符) MAX_PATH个字符.

for ( uLinkNum = 2; PathFileExists ( szNewFilename ) && uLinkNum < 100; uLinkNum++ ) { // 试用另外的文件名. wsprintf ( szNewFilename, _T("%sHard link (%u) to %s"), m_szFolderDroppedIn, uLinkNum, szSrcFileTitle ); // 如果得出的文件名超过 MAX_PATH 个字符, 显示一个错误框. if ( lstrlen ( szNewFilename ) >= MAX_PATH ) { sMessage.Format ( _T("Failed to make a link to %s. The resulting filename would be too long.\n\nDo you want to continue making links?"), (LPCTSTR) sSrcFile ); if ( IDNO == MessageBox ( pInfo->hwnd, sMessage, _T("Create Hard Links"), MB_ICONQUESTION | MB_YESNO )) break; else continue; } }弹出的消息框可以让你取消整个操作. 接着, 我们检查我们是否到了99 个链接的上限. 同样,我们让用户来取消操作.

if ( 100 == uLinkNum ) { sMessage.Format ( _T("Failed to make a link to %s. Reached limit of 99 links in a single directory.\n\nDo you want to continue making links?"), (LPCTSTR) sSrcFile ); if ( IDNO == MessageBox ( pInfo->hwnd, sMessage, _T("Create Hard Links"), MB_ICONQUESTION | MB_YESNO )) break; else continue; }剩下的只是创建硬链接. 我略掉了错误检查的代码.
CreateHardLink ( szNewFilename, sSrcFile, NULL );
} // end while loop
return S_OK;
}

硬链接看上去没什么不同, 它就像普通的文件一样. 但是如果你修改一个拷贝, 更改会反映在其它拷贝中(好像嵌入对象特性).

最后, 总结一下 CStringList 类的使用:

声明一个 POSITION 变量, 如pos.
调用 CStringList::GetHeadPosition() 取得列表头位置.
使用条件 (pos != NULL)开始循环.
调用 CStringList::GetNext(pos) 并将返回值赋给一 Cstring 变量.
在循环内利用文件名做你想要的操作. (很难想象理解了COM的人不会用Clist,作者真是有点…)
注册Shell扩展
注册拖放扩展处理器比其它扩展都要简单. 所有的处理器注册在 HKCR\Directory 键下. 然而, 文档未谈及的是在HKCR\Directory 下注册不能满足所有情况. 你需要在 HKCR\Folder 下注册以处理桌面上的拖放操作, 以及在 HKCR\Drive 下注册处理跟目录下的拖放.

以下是处理上述三种情况的的RGS 脚本:

HKCR{ NoRemove Directory { NoRemove shellex { NoRemove DragDropHandlers { ForceRemove HardLinkShlExt = s ''{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}'' } } } NoRemove Folder { NoRemove shellex { NoRemove DragDropHandlers { ForceRemove HardLinkShlExt = s ''{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}'' } } } NoRemove Drive { NoRemove shellex { NoRemove DragDropHandlers { ForceRemove HardLinkShlExt = s ''{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}'' } } }}正如上一节的例子一样,在 NT/2000上我们需要添加我们的扩展到 "approved" 扩展列表中去. 完成该工作的代码在 DllRegisterServer() 和 DllUnregisterServer() 函数中.我不在这写出这些代码, 因为这只是简单的注册表获取, 你可以在例子工程代码中找到它.

如果你没有Windows 2000
你仍然可以在早先的Windows版本上创建例子工程. 然后打开stdafx.h 文件, 删除下行代码:

//#define NOT_ON_WIN2K这会使扩展跳过文件系统检查 (因此它会在任何系统上运行,而不只是 NTFS), 并显示消息框而非作实际的链接.

第五节-如何编写添加属性页到文件属性对话框中的Shell扩展

在本节,我们将讨论属性页. 当你查看文件系统对象的属性时, 浏览器会显示标签视属性页. Shell让我们可以使用一种Shell扩展来扩展属性页的功能.

本文假设你理解Shell扩展的基础知识, 并熟悉 STL的集合类. 如果你需要复习一下 STL, 你可以阅读 第二节, 因为本文将使用同样的技巧. 该代码使用shlwapi.dll 输出的一些函数, 所以你需要安装IE 4 或更高的版本 (但你不必安装活动桌面).

属性页处理器
大家都熟悉浏览器的属性对话框. 更确切的讲, 它们是包含多个页面的属性页. 每一个页面都有标签Tab 列出文件路径, 修改时间, 及其它一些东西. 浏览器让我们可以添加自己的属性页面. 属性页处理器也可以添加或替换某些控制面板对象的属性页,但本文不会讨论这方面的内容.

本文提供了一个可以让你修改文件的创建时间,访问时间,和修改时间的属性页. 我将用纯粹的SDK API来完成编码,不用MFC或 ATL/WTL. 我没试过在扩展中使用 MFC 或 WTL的属性页; 这样做需要一定的技巧,因为Shell需要接收一个属性页的句柄 (HPROPSHEETPAGE), 而 MFC 将这些细节隐藏在 CPropertyPage 的实现中. (我不熟悉WTL实现的方式.)

如果你查看一个.URL 文件的属性框, 你会看到一个属性页扩展实例. "CodeProject" 标签Tab 是本文要实现的. "Web Document" 标签Tab 显示由 IE安装的一个扩展.

使用 AppWizard 开始
运行 AppWizard 并生成一个名为 FileTime的ATL工程.保留所有默认设置, 点击”完成”. 然后,在ClassView树中右击 FileTime classes 项,在弹出的菜单中选择New ATL Object,添加一个COM 对象类到 DLL中.

在 ATL Object Wizard 中, 第一页面已经选中了 Simple Object , 因此, 单击 Next. 在第二页面中, 在Short Name编辑框中输入FileTimeShlExt点击 OK. (其余编辑框中的内容将自动填写.) 这样就创建了一个名为CFileTimeShlExt的已实现基本的COM接口的新COM对象类. 我们将在这个类中添加实现代码.

初始化接口
由于属性页处理器一次处理选中的多个文件, 它使用IShellExtInit 作为初始化接口. 我们需要添加IShellExtInit 到 CFileTimeShlExt 实现的接口列表中. 具体细节见 第四节. 该类还需要一个字符串列表来保存所选文件名.

typedef std::list > string_list;protected: // IFileTimeShlExt string_list m_lsFiles;Initialize() 方法的行为同 第二节 – 读取选中的文件名并将其加入列表. 下面是该函数的开头:

HRESULT CFileTimeShlExt::Initialize ( LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hProgID ){TCHAR szFile [MAX_PATH];UINT uNumFiles;HDROP hdrop;FORMATETC etc = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };STGMEDIUM stg;INITCOMMONCONTROLSEX iccex = { sizeof(INITCOMMONCONTROLSEX), ICC_DATE_CLASSES }; // 初始化通用控件使用. InitCommonControlsEx ( &iccex );由于我们要使用Date/Time picker (DTP)控件,所以我们需要初始化通用控件. 接着我们使用IDataObject 接口指针获取 HDROP 句柄来列举所选文件.

// 重数据对象中读取项目. 它们以 HDROP格式存放, 因此只需取得 HDROP 句柄并对它使用拖放 APIs. if ( FAILED( pDataObj->GetData ( &etc, &stg ))) return E_INVALIDARG; // 取得 HDROP 句柄. hdrop = (HDROP) GlobalLock ( stg.hGlobal ); if ( NULL == hdrop ) { ReleaseStgMedium ( &stg ); return E_INVALIDARG; } // 判断操作涉及几个文件. uNumFiles = DragQueryFile ( hdrop, 0xFFFFFFFF, NULL, 0 );循环列举所选择的文件. 该扩展将只对文件进行操作,对文件夹不起作用, 所以忽略调文件夹.

for ( UINT uFile = 0; uFile < uNumFiles; uFile++ ) { // 取得下一文件名. if ( 0 == DragQueryFile ( hdrop, uFile, szFile, MAX_PATH )) continue; // 跳过文件夹. 其实我们可以处理文件夹,因此它们也有创建时间,但我选择对之不作处理. if ( PathIsDirectory ( szFile )) continue; // 添加文件名到我们的文件名列表. m_lsFiles.push_back ( szFile ); } // end for // 释放资源. GlobalUnlock ( stg.hGlobal ); ReleaseStgMedium ( &stg );这里有些新东西! 一个属性表的页面个数是有限制的, 在头文件prsht.h中定义为常数MAXPROPPAGES. 在该扩展中,每一个文件都有其页面, 所以如果我们的列表有多于 MAXPROPPAGES 个文件, 多出部分将被截去. (即使MAXPROPPAGES 为100, 属性表也不能显示这么多Tab. 它最多能有34个左右.)

// 检查有几个文件被选中. 如果大于属性表能有的最大属性页个数, 删减列表. if ( m_lsFiles.size() > MAXPROPPAGES ) { m_lsFiles.resize ( MAXPROPPAGES ); } // 如果我们发现可以操作的任一文件返回 S_OK. 否则返回 E_FAIL. return ( m_lsFiles.size() > 0 ) ? S_OK : E_FAIL;}添加属性页
如果 Initialize() 返回 S_OK, 浏览器查询另一个新接口:IShellPropSheetExt. IShellPropSheetExt 很简单,只有一个方法需要实现. 要添加 IShellPropSheetExt 接口到我们的类, 打开文件 FileTimeShlExt.h 并添加如下标红的代码:

class ATL_NO_VTABLE CFileTimeShlExt : public CComObjectRootEx, public CComCoClass, public IDispatchImpl, public IShellExtInit, public IShellPropSheetExt{BEGIN_COM_MAP(CFileTimeShlExt) COM_INTERFACE_ENTRY(IFileTimeShlExt) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY(IShellExtInit) COM_INTERFACE_ENTRY(IShellPropSheetExt)END_COM_MAP()public: // IShellPropSheetExt STDMETHOD(AddPages)(LPFNADDPROPSHEETPAGE, LPARAM); STDMETHOD(ReplacePage)(UINT, LPFNADDPROPSHEETPAGE, LPARAM) { return E_NOTIMPL; }AddPages() 是我们要实现的方法. ReplacePage() 只用于控制面板的属性页扩展的页面替换, 所以在这我们不需要实现它. 浏览器调用 AddPages() 函数来让我们添加页面到浏览器设置的属性表中.

AddPages() 的参数是一个函数指针和一个LPARAM变量, 这两个都只被Shell使用. lpfnAddPageProc 指向一个Shell进程内的函数以添加页面. lParam 参数是一个重要的由Shell使用的值. 我们不深究它, 我们仅将其直接传回lpfnAddPageProc 函数即可.

HRESULT CFileTimeShlExt::AddPages ( LPFNADDPROPSHEETPAGE lpfnAddPageProc, LPARAM lParam ){PROPSHEETPAGE psp;TCHAR szPageTitle [MAX_PATH];string_list::const_iterator it, itEnd; for ( it = m_lsFiles.begin(), itEnd = m_lsFiles.end(); it != itEnd; it++ ) { // ''it'' 指向下一个文件名. 分配一个给页面使用的字符串拷贝. LPCTSTR szFile = _tcsdup ( it->c_str() );我们首先复制一份文件名变量.

下一步要创建我们的Tab页要使用的字符串. 如不带扩展名的文件名. 另外如果其大于24个字符将被删减. 这是绝对的; 我选择24 是因为我喜欢这个数字. 这要有一些限制以防止名字超出Tab的范围.

// 从文件名中截去路径和扩展名 – 用其作为页面标题. 该名称截取为 24 个字符以适合Tab的大小. lstrcpy ( szPageTitle, it->c_str() ); PathStripPath ( szPageTitle ); PathRemoveExtension ( szPageTitle ); szPageTitle[24] = ''\0'';由于我们使用直接的SDK 调用完成属性页, 我们不得不亲自处理 PROPSHEETPAGE 结构. 以下是对该结构的设置: psp.dwSize = sizeof(PROPSHEETPAGE); psp.dwFlags = PSP_USEREFPARENT | PSP_USETITLE | PSP_DEFAULT | PSP_USEICONID | PSP_USECALLBACK; psp.hInstance = _Module.GetModuleInstance(); psp.pszTemplate = MAKEINTRESOURCE(IDD_FILETIME_PROPPAGE); psp.pszIcon = MAKEINTRESOURCE(IDI_ICON); psp.pszTitle = szPageTitle; psp.pfnDlgProc = PropPageDlgProc; psp.lParam = (LPARAM) szFile; psp.pfnCallback = PropPageCallbackProc; psp.pcRefParent = (UINT*) &_Module.m_nLockCnt;在这我们要注意一些重要的细节以让扩展正确地工作:pszIcon 成员变量应该设为一个 16x16 图标的资源ID, 该图标将被显示在Tab标签上. 要不要图标是可选的, 但我添加了一个使得我们的页面更突出.
pfnDlgProc 成员变量应该设为我们页面所在的对话框的窗口函数地址.
lParam 成员变量设为szFile, 是该页面所关联的文件名.
pfnCallback 成员变量设为一个回调函数的地址,该函数将在页面被创建或销毁时调用. 稍后将详细解释该函数.
pcRefParent 成员变量设为一个从CcomModule 继承下来的成员变量的地址. 其为DLL的锁定值. 当属性也显示时Shell 会增加该引用计数, 以使得当页面打开时我们的DLL始终在内存中. 该计数在页面销毁时将递减.
设置了该结构后, 我们调用 API 来创建属性页.

hPage = CreatePropertySheetPage ( &psp );如果成功, 我们调用Shell的回调函数添加新创建的属性页到属性对话框中. 该回调函数返回 BOOL值以表明成功与否. 如果失败,我们将销毁创建的页面资源.

if ( NULL != hPage ) { // 调用Shell的回调函数添加新创建的属性页到属性对话框中 if ( !lpfnAddPageProc ( hPage, lParam )) { DestroyPropertySheetPage ( hPage ); } } } // end for return S_OK;}一个对象存活期的棘手问题
该到了我实现解释关于字符串复制问题的承诺的时候了. 在这复制是必须的,因为AddPages() 返回之后, Shell释放它的IShellPropSheetExt 接口, 这会导致销毁 CFileTimeShlExt 对象. 这样属性页的窗口回调函数就不能获取CfileTimeShlExt的成员变量m_lsFiles 了.

我的解决方法是复制每一个文件名, 并将之传给页面. 页面将拥有该内存, 它有责任释放它. 如果有多个选择文件, 每一个页面都有一份对应文件名的拷贝. 该内存在 PropPageCallbackProc 函数中释放. 在 AddPages()中下行代码

psp.lParam = (LPARAM) szFile;是很重要的. 它保存指针于 PROPSHEETPAGE 结构中, 使得可以为页面窗口函数所用.

属性页回调函数
现在, 谈论属性页本身. 以下是新页面的样子. 在你读完本文前请记住下图.

注意这里没有文件最后获取时间的显示. FAT 只支持最后文件获取日期. 其它文件系统支持该时间, 但我没有实现判断文件系统的逻辑. 如果获取最后访问时间失败,时间总保存为午夜12 点.

该页面有两个回调函数和两个消息处理器. 函数原型见 FileTimeShlExt.cpp 开头部分:

BOOL CALLBACK PropPageDlgProc ( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam );UINT CALLBACK PropPageCallbackProc ( HWND hwnd, UINT uMsg, LPPROPSHEETPAGE ppsp );BOOL OnInitDialog ( HWND hwnd, LPARAM lParam );BOOL OnApply ( HWND hwnd, PSHNOTIFY* phdr );该对话框相当简单. 它处理三个消息: WM_INITDIALOG, PSN_APPLY 和 DTN_DATETIMECHANGE. 以下是WM_INITDIALOG 部分:

BOOL CALLBACK PropPageDlgProc ( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam ){ switch ( uMsg ) { case WM_INITDIALOG: bRet = OnInitDialog ( hwnd, lParam ); break;OnInitDialog() 将在稍后解释. 接着是 当用户按下 确定 或 应用 按钮时发送的通知 PSN_APPLY,. case WM_NOTIFY: { NMHDR* phdr = (NMHDR*) lParam; switch ( phdr->code ) { case PSN_APPLY: bRet = OnApply ( hwnd, (PSHNOTIFY*) phdr ); break;最后是 DTN_DATETIMECHANGE. 这个简单 – 我们发送一个消息给属性表使能 “应用” 按钮.

case DTN_DATETIMECHANGE: // 如果用户改变任一DTP 控件的值就使能 应用 按钮. SendMessage ( GetParent(hwnd), PSM_CHANGED, (WPARAM) hwnd, 0 ); break; } } break; } return bRet;}到目前为止, 一切OK. 另一个回调函数在页面创建或销毁时被调用. 我们只关心后者, 因为在销毁时我们可以释放AddPages()中所分配的字符串. ppsp 参数指向用于创建页面的PROPSHEETPAGE 结构, 并且lParam 成员变量仍指向所拷贝的字符串.

UINT CALLBACK PropPageCallbackProc ( HWND hwnd, UINT uMsg, LPPROPSHEETPAGE ppsp ){ if ( PSPCB_RELEASE == uMsg ) { free ( (void*) ppsp->lParam ); } return 1;}该函数总是返回 1,因为该函数在页面创建时被调用, 它可以返回0阻止该页面的创建. 返回1 则让该页面正常创建. 当页面销毁调用该函数时返回值将被忽略.

属性页消息处理器
OnInitDialog() 里有很多重要的代码. lParam 参数再一次指向 PROPSHEETPAGE 结构用以创建页面. 该结构的成员lParam 指向先前提供的文件名. 因为我们需要在 OnApply() 函数里获取文件名, 我们使用SetWindowLong()来保存该字符串指针.

BOOL OnInitDialog ( HWND hwnd, LPARAM lParam ){ PROPSHEETPAGE* ppsp = (PROPSHEETPAGE*) lParam;LPCTSTR szFile = (LPCTSTR) ppsp->lParam;HANDLE hFind;WIN32_FIND_DATA rFind; // 在窗口用户数据区内保存文件名,已被后用. SetWindowLong ( hwnd, GWL_USERDATA, (LONG) szFile );接着, 我们使用 FindFirstFile()获取文件创建,修改及访问时间. 若成功, 使用正确的数据初始化DTP 控件.

hFind = FindFirstFile ( szFile, &rFind ); if ( INVALID_HANDLE_VALUE != hFind ) { // 初始化 DTP 控件. SetCombinedDateTime ( hwnd, IDC_MODIFIED_DATE, IDC_MODIFIED_TIME, &rFind.ftLastWriteTime ); SetCombinedDateTime ( hwnd, IDC_ACCESSED_DATE, 0, &rFind.ftLastAccessTime ); SetCombinedDateTime ( hwnd, IDC_CREATED_DATE, IDC_CREATED_TIME, &rFind.ftCreationTime ); FindClose ( hFind ); }SetCombinedDateTime() 是设置DTP控件内容的功能函数. 你可以在FileTimeShlExt.cpp文件的尾部找到这些代码.

另外, 文件的全路径名显示在页面的顶部Static控件里

PathSetDlgItemPath ( hwnd, IDC_FILENAME, szFile ); return FALSE; // Take the default focus handling.}OnApply() 函数处理相反的操作 – 它读取 DTP 控件的内容并修改文件的创建,修改及访问时间. 首先使用GetWindowLong() 获取文件名,再打开文件进行写入.

BOOL OnApply ( HWND hwnd, PSHNOTIFY* phdr ){LPCTSTR szFile = (LPCTSTR) GetWindowLong ( hwnd, GWL_USERDATA );HANDLE hFile;FILETIME ftModified, ftAccessed, ftCreated; // 打开文件. hFile = CreateFile ( szFile, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );如果打开文件成功, 我们就读取 DTP 控件的内容将时间回写到文件里.
GetCombinedDateTime() 是SetCombinedDateTime()函数的反操作. if ( INVALID_HANDLE_VALUE != hFile ) { // 从DTP 控件中获取日期时间数据. GetCombinedDateTime ( hwnd, IDC_MODIFIED_DATE, IDC_MODIFIED_TIME, &ftModified ); GetCombinedDateTime ( hwnd, IDC_ACCESSED_DATE, 0, &ftAccessed ); GetCombinedDateTime ( hwnd, IDC_CREATED_DATE, IDC_CREATED_TIME, &ftCreated ); //修改文件的创建,修改及访问时间 SetFileTime ( hFile, &ftCreated, &ftAccessed, &ftModified ); CloseHandle ( hFile ); } else { // <<忽略错误处理>> } // 返回 PSNRET_NOERROR 让页表正常关闭. SetWindowLong ( hwnd, DWL_MSGRESULT, PSNRET_NOERROR ); return TRUE;}注册Shell扩展
注册一个拖放目标扩展处理器类似于注册上下文菜单扩展. 该处理器可以为特定的文件类型所激发, 例如文本文件. 该扩展适用于任一文件, 所以我们可以在 HKEY_CLASSES_ROOT\* 键下注册.以下是扩展注册的 RGS 脚本:

HKCR{ NoRemove * { NoRemove shellex { NoRemove PropertySheetHandlers { {3FCEF010-09A4-11D4-8D3B-D12F9D3D8B02} } } }}你可能注意到扩展的 GUID 作为注册键名存储在这, 而不是一个字符串值. 我所看过的文档和书籍在命名习惯上有冲突,但在我测试时两者都能工作. 我决定按 Dino Esposito的书中的方法 (Visual C++ Windows Shell Programming), 将 GUID 作为注册键的名称.

正如上一节的例子一样,在 NT/2000上我们需要添加我们的扩展到 "approved" 扩展列表中去. 完成该工作的代码在 DllRegisterServer() 和 DllUnregisterServer() 函数中.我不在这写出这些代码, 因为这只是简单的注册表获取, 你可以在例子工程代码中找到它.

第六节-如何编写定制”发送到”菜单的Shell扩展

简介
本节我将介绍一种比较少用到的扩展类型, 放置目标扩展处理器(drop handler). 这种类型可用于浏览器上的拖放功能, 由被放置到的文件(即放置的目标)类型决定所激发的扩展 .

第三节假设理解Shell扩展的基础知识 (参见 第一节), 并熟悉 MFC. 如果你需要复习一下要使用的MFC 类, 请阅读 第四节, 因为本文将使用相同的技巧. 该代码使用shlwapi.dll 输出的一些函数, 所以你需要安装IE 4 或更高的版本 (但你不必安装活动桌面).

放置目标扩展处理器
在 第四节 中, 我讨论了拖放目标扩展处理器, 它在用右键拖放文件时被激发. 浏览器同时也让我们编写扩展来处理用左键拖放文件的操作, 当文件被放下时扩展开始工作. 如, WinZip 包含一个放置处理器来让你添加文件到压缩文件中. 当你拖动一个文件到Zip文件上时, 浏览器会高亮显示Zip文件并显示一个带加号的鼠标以表明zip文件可以是一个放置目标:

如果没有安装放置目标处理器, 当你将文件拖过Zip文件时就不会发生什么特别的情况:

放置扩展处理器只在当你有自定义的文件类型时有用, 如 WinZip. 而使用放置目标处理器所作的更有趣的是添加菜单项到发送到菜单. 发送到菜单显示了 \Windows\SendTo 文件夹中的内容. 一般而言, 发送到文件夹包含快捷方式, 但微软的 Power Toys 工具添加几个特殊项,如下所示:

如果你不清楚放置处理器如何处理, 看一下发送到文件夹的内容:

12-02-98 0:27 129 3?Floppy (A).lnk11-26-98 10:27 0 Any Folder....OtherFolder11-26-98 10:27 0 Clipboard as Contents.ContentsOnClipboard11-26-98 10:27 0 Clipboard as Name.NameOnClipboard11-26-98 10:27 0 Command Line.CommandLine 3-26-99 8:42 0 Desktop (create shortcut).DeskLink 4-22-99 23:30 0 Norton Wipe Slack Space.WipeSlack 4-22-99 23:30 0 Norton WipeInfo.WipeInfo11-26-98 10:26 285 Notepad.lnk 1-07-00 9:01 212 xfer directory on zip drive.lnk注意那些古怪的扩展名如 ".ContentsOnClipboard". 这些0字节的文件被放到该目录下使得在发送到菜单中能显示其相应项目, 而实际的扩展存在注册表中. 但是它们没有正常的文件关联, 因为文件并没有像打开或打印之类的动作. 它们所知的只是放置扩展处理器. 当你点击发送到菜单中的一项, 浏览器会激发相应的放置扩展处理器. 以下标出放置目标的发送到菜单:

本文的例子工程模仿Send To Any Folder Powertoy – 它可以复制或移动文件到系统中的任一文件夹.

使用 AppWizard 开始
运行 AppWizard 并生成一个名为 SendToClone的ATL工程.保留所有默认设置, 点击”完成”. 然后,在ClassView树中右击 SendToClone classes 项,在弹出的菜单中选择New ATL Object,添加一个COM 对象类到 DLL中.

在 ATL Object Wizard 中, 第一页面已经选中了 Simple Object , 因此, 单击 Next. 在第二页面中, 在Short Name编辑框中输入SendToShlExt点击 OK. (其余编辑框中的内容将自动填写.) 这样就创建了一个名为CSendToShlExt的已实现基本的COM接口的新COM对象类. 我们将在这个类中添加实现代码.

初始化接口
由于放置处理器由放置目标所激发, 使用IPersistFile接口进行初始化. (记住 IPersistFile 用于一次只操作一个文件的扩展.) IPersistFile 接口有多个方法, 但在Shell扩展中只有 Load() 方法需要实现.

我们需要添加IPersistFile 接口到 CSendToShlExt 实现的接口中. 打开 SendToShlExt.h 文件添加以下标红的代码:

#include #include class ATL_NO_VTABLE CSendToShlExt : public CComObjectRootEx, public CComCoClass, public IDispatchImpl, public IPersistFile{BEGIN_COM_MAP(CSendToShlExt) COM_INTERFACE_ENTRY(ISendToShlExt) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY(IPersistFile)END_COM_MAP() public: // IPersistFile STDMETHOD(GetClassID)(LPCLSID) { return E_NOTIMPL; } STDMETHOD(IsDirty)() { return E_NOTIMPL; } STDMETHOD(Load)(LPCOLESTR, DWORD) { return S_OK; } STDMETHOD(Save)(LPCOLESTR, BOOL) { return E_NOTIMPL; } STDMETHOD(SaveCompleted)(LPCOLESTR) { return E_NOTIMPL; } STDMETHOD(GetCurFile)(LPOLESTR*) { return E_NOTIMPL; }注意 Load() 方法除了返回 S_OK外什么都不做. Load() 方法可以接收放置目标文件名, 但对于这个扩展我们不关心这个名称,所以我们忽略它.

参与拖放操作
为了完成操作我们的扩展得跟拖放源进行通信, 即浏览器本身. 我们的扩展会取得一个被拖放的文件列表, 接着它通知浏览器它是否接受这些被放置的文件. 这些通信通过另一个接口: IDropTarget. IDropTarget 接口方法有:

DragEnter(): 当用户第一次拖动文件经过时被调用. 该方法通知浏览器扩展是否接受当前被拖动的文件.
DragOver(): 在Shell 扩展中不会被调用.
DragLeave(): 当用户拖动并离开文件时被调用.
Drop(): 当用户放下拖动文件时被调用. Shell扩展的主要工作就在这里.
要添加 IDropTarget 接口到 CSendToShlExt, 打开 SendToShlExt.h 文件并添加下面标红代码:

class ATL_NO_VTABLE CSendToShlExt : public CComObjectRootEx, public CComCoClass, public IDispatchImpl, public IPersistFile, public IDropTarget{BEGIN_COM_MAP(CSendToShlExt) COM_INTERFACE_ENTRY(ISendToShlExt) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY(IPersistFile) COM_INTERFACE_ENTRY(IDropTarget)END_COM_MAP() protected: // ISendToShlExt CStringList m_lsDroppedFiles; public: // IDropTarget STDMETHOD(DragEnter)(IDataObject* pDataObj, DWORD grfKeyState, POINTL pt, DWORD* pdwEffect); STDMETHOD(DragOver)(DWORD grfKeyState, POINTL pt, DWORD* pdwEffect) { return E_NOTIMPL; } STDMETHOD(DragLeave)(); STDMETHOD(Drop)(IDataObject* pDataObj, DWORD grfKeyState, POINTL pt, DWORD* pdwEffect);}如以前的版本, 我们将使用文件列表来保存被拖放的文件. DragOver() 方法不需要实现 因为它不会被调用. 我将说明其余三个方法.

DragEnter()
DragEnter()的原型为:

HRESULT IDropTarget::DragEnter ( IDataObject* pDataObj, DWORD grfKeyState, POINTL pt, DWORD* pdwEffect );PDataObj 是一个 IDataObject 接口的指针,使用之我们可以列举被拖动的文件. grfKeyState 是一组表明Shift键和鼠标键状态的标志. pt 是个 POINTL 结构 (其实就是POINT) 保存鼠标的当前位置. pdwEffect 是个DWORD 值的指针,我们将用这个值返回通知浏览器我们是否接受该放置文件, 如果接受,该显示什么特别图标覆盖在鼠标上.

如前述, DragEnter() 当用户拖动文件到目标位置上时被调用. 然而, 当用户点击一项发送到菜单时也被调用,所以我们仍可以在 DragEnter() 中完成工作即使这里其实并没有拖放操作发生.

我们的 DragEnter() 实现填充一个拖放的文件名列表. 该扩展将接受所有文件或文件夹, 因为任一文件系统对象都可以被复制或移动.

DragEnter() 开始部分你一定熟悉 – 我们将IdataObject接口赋值给一个 COleDataObject 变量, 并列举被拖放的文件.

HRESULT CSendToShlExt::DragEnter ( IDataObject* pDataObj, DWORD grfKeyState, POINTL pt, DWORD* pdwEffect ){ AFX_MANAGE_STATE(AfxGetStaticModuleState()); // init MFC COleDataObject dataobj;TCHAR szItem [MAX_PATH];UINT uNumFiles;HGLOBAL hglobal;HDROP hdrop; dataobj.Attach ( pDataObj, FALSE ); // attach to the IDataObject, don''t auto-release it // 从数据对象中读取文件列表. 他们存储在HDROP 格式中 // 因此,取得 HDROP 句柄,并使用拖放API hglobal = dataobj.GetGlobalData ( CF_HDROP ); if ( NULL != hglobal ) { hdrop = (HDROP) GlobalLock ( hglobal ); uNumFiles = DragQueryFile ( hdrop, 0xFFFFFFFF, NULL, 0 ); for ( UINT uFile = 0; uFile < uNumFiles; uFile++ ) { if ( 0 != DragQueryFile ( hdrop, uFile, szItem, MAX_PATH )) { m_lsDroppedFiles.AddTail ( szItem ); } } GlobalUnlock ( hglobal ); }现在返回 pdwEffect值. 我们可以返回的效果有:

DROPEFFECT_COPY: 通知浏览器我们的扩展将复制拖放的文件.
DROPEFFECT_MOVE: 通知浏览器我们的扩展将移动拖放的文件.
DROPEFFECT_LINK: 通知浏览器我们的扩展将为拖放的文件创建快捷方式.
DROPEFFECT_NONE: 通知浏览器我们的扩展不接受拖放的文件.
我们返回的唯一效果是 DROPEFFECT_COPY. 我们不能返回 DROPEFFECT_MOVE, 因为这会使浏览器删除被拖放的文件. 我们可以返回 DROPEFFECT_LINK, 但光标会显示为创建快捷方式的样子, 这会误导用户. 如果文件列表为空, 我们返回 DROPEFFECT_NONE 告诉浏览器我们不接受拖放的文件.

if ( m_lsDroppedFiles.GetCount() > 0 ) { *pdwEffect = DROPEFFECT_COPY; return S_OK; } else { *pdwEffect = DROPEFFECT_NONE; return E_INVALIDARG; }}DragLeave()
DragLeave() 当用户拖动并离开文件时被调用. 发送到菜单扩展不使用该方法,但如果你打开发送到菜单文件夹的浏览器窗口并将文件拖到该文件夹下它会被调用. 我们没有什么清理工作要做(CStringList 析构器会自动完成), 所以我们只要返回S_OK即可:

HRESULT CSendToShlExt::DragLeave(){ return S_OK;}Drop()
如果用户选中发送到菜单项, 浏览器调用 Drop(), 其原型为:

HRESULT IDropTarget::Drop ( IDataObject* pDataObj, DWORD grfKeyState, POINTL pt, DWORD* pdwEffect );头三个参数同DragEnter(). Drop() 应该使用pdwEffect 参数返回操作的最终效果. 我们的 Drop() 函数创建主对话框并传给它文件名列表. 由该对话框完成所有的工作, 而当DoModal() 返回时, 我们设置最终的放置效果.

HRESULT CSendToShlExt::Drop ( IDataObject* pDataObj, DWORD grfKeyState, POINTL pt, DWORD* pdwEffect ){ AFX_MANAGE_STATE(AfxGetStaticModuleState()); // init MFC CSendToCloneDlg dlg ( &m_lsDroppedFiles ); dlg.DoModal(); *pdwEffect = DROPEFFECT_COPY; return S_OK;}对话框如下所示:

这是个简单的 MFC 对话框, 你可以在 SendToCloneDlg.cpp 文件中找到源代码. 我使用 CshellFileOp类完成实际的移动和复制,具体参见我的文章 "CShellFileOp - Wrapper for SHFileOperation."

但是等等? 我们如何告诉浏览器我们编写的放置处理器的? 我们又怎样获取发送到菜单中的一项? 我会在下一段解释.

注册Shell 扩展
注册放置处理器与其它扩展有所不同, 因为它需要在 HKEY_CLASSES_ROOT 创建一个新的关联. AppWizard生成的RGS 脚本如下所示. 你应该添加标红的代码.

HKCR{ .SendToClone = s ''CLSID\{B7F3240E-0E29-11D4-8D3B-80CD3621FB09}'' SendToClone.SendToShlExt.1 = s ''SendToShlExt Class'' { CLSID = s ''{B7F3240E-0E29-11D4-8D3B-80CD3621FB09}'' } SendToClone.SendToShlExt = s ''SendToShlExt Class'' { CLSID = s ''{B7F3240E-0E29-11D4-8D3B-80CD3621FB09}'' CurVer = s ''SendToClone.SendToShlExt.1'' } NoRemove CLSID { ForceRemove {B7F3240E-0E29-11D4-8D3B-80CD3621FB09} = s ''Send To Any Folder Clone'' { ProgID = s ''SendToClone.SendToShlExt.1'' VersionIndependentProgID = s ''SendToClone.SendToShlExt'' ForceRemove ''Programmable'' InprocServer32 = s ''%MODULE%'' { val ThreadingModel = s ''Apartment'' } ''TypeLib'' = s ''{B7F32400-0E29-11D4-8D3B-80CD3621FB09}'' val NeverShowExt = s '''' DefaultIcon = s ''%MODULE%,0'' shellex { DropHandler = s ''{B7F3240E-0E29-11D4-8D3B-80CD3621FB09}'' } } }}第一行是创建关联. 其创建一个新的放置目标扩展, .SendToClone. 注意.SendToClone 键的默认值以"CLSID\"为前缀. 这告诉浏览器描述关联的数据在 HKCR\CLSID 下的一个键里. 跟一般的关联一样, 其就保存在 HKEY_CLASSES_ROOT 键下(如, .txt 键指向 txtfile 键), 但把放置目标处理器关联数据注册在它自己的 CLSID 键下似乎更好, 这样可以保证数据的统一存放.

字符串 "Send To Any Folder Clone" 是当你浏览发送到文件夹时显示在浏览器中的文件类型描述. NeverShowExt 值用于告诉浏览器不要显示 ".SendToClone" 扩展名. DefaultIcon 键列出了.SendToClone文件所使用的图标的位置. 最后生成带DropHandler子键的 shellex 键. 由于一种文件类型只能有一个放置扩展处理器, 处理器的 GUID 就存在DropHandler 键里, 而不是 DropHandler 下的子键.

剩余的细节是要在发送到文件夹中创建一个文件好让显示我们的菜单项 . 我们可以在 DllRegisterServer() 中完成并在 DllUnregisterServer() 中删除文件. 下面是创建文件的代码:

LPITEMIDLIST pidl;TCHAR szSendtoPath [MAX_PATH];HANDLE hFile;LPMALLOC pMalloc; if ( SUCCEEDED( SHGetSpecialFolderLocation ( NULL, CSIDL_SENDTO, &pidl ))) { if ( SHGetPathFromIDList ( pidl, szSendtoPath )) { PathAppend ( szSendtoPath, _T("Some other folder.SendToClone") ); hFile = CreateFile ( szSendtoPath, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL ); CloseHandle ( hFile ); } if ( SUCCEEDED( SHGetMalloc ( &pMalloc ))) { pMalloc->Free ( pidl ); pMalloc->Release(); } }下面是发送菜单的样子:

DllUnregisterServer() 删除”发送到”文件夹中的文件. 上面的代码对任何版本的 Windows 都适用. 如果你事先知道你的代码会在高于Shell 4.71版本的情况下运行, 你可以使用 SHGetSpecialFolderPath() 函数而不用 SHGetSpecialFolderLocation().

正如上一节的例子一样,在 NT/2000上我们需要添加我们的扩展到 "approved" 扩展列表中去. 完成该工作的代码在 DllRegisterServer() 和 DllUnregisterServer() 函数中.我不在这写出这些代码, 因为这只是简单的注册表获取, 你可以在例子工程代码中找到它.

第七节-如何编写自画上下文菜单项的Shell扩展,
以及如何使上下文菜单扩展响应文件夹窗口背景上的鼠标右击事件

本指南开始新的一章! 在这一节里, 我将回答一些读者的要求并讨论两个话题: 在上下文菜单中使用
自画功能, 并使上下文菜单扩展响应文件夹窗口背景上的鼠标右击事件. 你应该先读一下第一节和第
二节, 学习一下基本的上下文菜单扩展的编写.

扩展 1 – 自画菜单项
在本节, 我将只讨论实现自画菜单的额外工作.

由于该扩展要实现自画菜单, 这得作些画图的工作. 我决定从程序 PicaView 里复制一些代码: 在上下文菜单上显示一个图象文件的缩略图. 如下所示:

该扩展将创建 BMP 文件的缩略图, 为了使代码尽量简单, 我将不考虑图象的比例和颜色问题. 读者可自行完善这些代码. ;)

使用 AppWizard 开始
运行 AppWizard 并生成一个名为 BmpViewerExt的ATL工程. 由于这次我们要使用MFC,所以要选中 Support MFC设置, 点击”完成”. 然后,在ClassView树中右击 BmpViewerExt classes 项,在弹出的菜单中选择New ATL Object,添加一个COM 对象类到 DLL中.

在 ATL Object Wizard 中, 第一页面已经选中了 Simple Object , 因此, 单击 Next. 在第二页面中, 在Short Name编辑框中输入BmpCtxMenuExt 点击 OK. (其余编辑框中的内容将自动填写.) 这样就创建了一个名为 CBmpCtxMenuExt的已实现基本的COM接口的新COM对象类. 我们将在这个类中添加实现代码.

初始化接口
如我们前面的上下文菜单扩展一样, 浏览器通过IShellExtInit 接口让我们进行初始化.我们需要添加IShellExtInit 接口到 CBmpCtxMenuExt 实现的接口列表中. 打开 BmpCtxMenuExt.h ,并添加如下标红的代码:

这里还有几个成员变量用于菜单项作图.

#include /////////////////////////////////////////////////////////////////////////////// CBmpCtxMenuExt class ATL_NO_VTABLE CBmpCtxMenuExt : public CComObjectRootEx, public CComCoClass, public IDispatchImpl, public IShellExtInit{BEGIN_COM_MAP(CBmpCtxMenuExt) COM_INTERFACE_ENTRY(IBmpCtxMenuExt) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY(IShellExtInit)END_COM_MAP() public: // IShellExtInit STDMETHOD(Initialize)(LPCITEMIDLIST, LPDATAOBJECT, HKEY); protected: TCHAR m_szFile[MAX_PATH]; CBitmap m_bmp; UINT m_uOurItemID; LONG m_lItemWidth, m_lItemHeight; LONG m_lBmpWidth, m_lBmpHeight; static const LONG m_lMaxThumbnailSize; static const LONG m_l3DBorderWidth; static const LONG m_lMenuItemSpacing; static const LONG m_lTotalBorderSpace; // Helper functions for handling the menu-related messages. STDMETHOD(MenuMessageHandler)(UINT, WPARAM, LPARAM, LRESULT*); STDMETHOD(OnMeasureItem)(MEASUREITEMSTRUCT*, LRESULT*); STDMETHOD(OnDrawItem)(DRAWITEMSTRUCT*, LRESULT*);};在 IShellExtInit::Initialize() 中我们取得被选择右击的文件名, 如果其扩展名为 .BMP就为它创建一幅缩略图.

在 BmpCtxMenuExt.cpp 中添加如下静态变量的声明. 这些变量将控制缩略图的外观. 你也可以随意更改这些值来看一下作图后有何效果.

const LONG CBmpCtxMenuExt::m_lMaxThumbnailSize = 64;const LONG CBmpCtxMenuExt::m_l3DBorderWidth = 2;const LONG CBmpCtxMenuExt::m_lMenuItemSpacing = 4;const LONG CBmpCtxMenuExt::m_lTotalBorderSpace = 2*(m_lMenuItemSpacing+m_l3DBorderWidth);这些变量的含义如下:

m_lMaxThumbnailSize: 如果位图的任何一边大于这个值, 该位图就会被缩小为一个正方形, 每一边长都为 m_lMaxThumbnailSize 个象素. 如果位图的每边都小于这个值,位图就按原样显示.
m_l3DBorderWidth: 缩略图的3D 边框的凹度,以象素为单位.
m_lMenuItemSpacing: 3D边框周围留空的大小. 这给缩略图与周围的菜单项之间留下间距.
添加 IShellExtInit::Initialize() 函数的实现定义:

STDMETHODIMP CBmpCtxMenuExt::Initialize ( LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDO, HKEY hkeyProgID ){ AFX_MANAGE_STATE(AfxGetStaticModuleState()); COleDataObject dataobj;HGLOBAL hglobal;HDROP hdrop;bool bOK = false; dataobj.Attach ( pDO, FALSE ); // FALSE表示当该对象销毁时,不要释放IDataObject 接口 // 取得第一个所选择的文件名. 我只简单地检查一下文件名是不是.BMP文件. hglobal = dataobj.GetGlobalData ( CF_HDROP ); if ( NULL == hglobal ) return E_INVALIDARG; hdrop = (HDROP) GlobalLock ( hglobal ); if ( NULL == hdrop ) return E_INVALIDARG; // 取得文件名. if ( DragQueryFile ( hdrop, 0, m_szFile, MAX_PATH )) { // 是否以 .BMP为扩展名? if ( PathMatchSpec ( m_szFile, _T("*.bmp") )) { // 加载位图并赋值给一个 CBitmap 对象 HBITMAP hbm = (HBITMAP) LoadImage ( NULL, m_szFile, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE ); if ( NULL != hbm ) { // 我们加载位图, 赋给CBitmap 对象. m_bmp.Attach ( hbm ); bOK = true; } } } GlobalUnlock ( hglobal ); return bOK ? S_OK : E_FAIL;}这是很直接的代码.我们加载位图并赋值给一个 CBitmap 对象以备后用.

与上下文菜单进行交互
如前, 如果IShellExtInit::Initialize() 返回 S_OK, 浏览器会接着查询 IContextMenu 接口. 为了能添加自画菜单项功能, 它还会查询 IContextMenu3 接口. IContextMenu3 接口给 IContextMenu 接口加了一个自画功能的方法.

还有一个 IContextMenu2 接口, 微软说Shell 版本4.00支持它, 但在 Windows 95 上Shell从不查询IContextMenu2 接口, 以至于在Shell 版本 4.00上不能实现自画菜单. (NT 4 可能会不同; 但我没试过.) 你可以使菜单项直接显示一幅位图, 但当它被选择时很难看. (这也是 PicaView 的做法.)

IContextMenu3 从IContextMenu2 继承, 添加了 HandleMenuMsg2() 方法到 IcontextMenu 接口. 该方法让我们可以响应两条消息: WM_MEASUREITEM 和 WM_DRAWITEM. 文档说 HandleMenuMsg2() 在 WM_INITMENUPOPUP 和WM_MENUCHAR 消息发生时也会被调用, 但是我在Shell 4.72 (Win 95) 和 5.00 (Win 2K)中测试时发现不行, HandleMenuMsg2() 不会收到这两条消息.

由于 IContextMenu3 继承于 IContextMenu2 (其又继承于IContextMenu), 我们只需让我们的类继承 IContextMenu3 接口. 打开 BmpCtxMenuExt.h 添加如下标红的代码:

class ATL_NO_VTABLE CBmpCtxMenuExt : public CComObjectRootEx, public CComCoClass, public IDispatchImpl, public IShellExtInit, public IContextMenu3{BEGIN_COM_MAP(CSimpleShlExt) COM_INTERFACE_ENTRY(ISimpleShlExt) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY(IShellExtInit) COM_INTERFACE_ENTRY(IContextMenu) COM_INTERFACE_ENTRY(IContextMenu2) COM_INTERFACE_ENTRY(IContextMenu3)END_COM_MAP() public: // IContextMenu STDMETHOD(QueryContextMenu)(HMENU, UINT, UINT, UINT, UINT); STDMETHOD(InvokeCommand)(LPCMINVOKECOMMANDINFO); STDMETHOD(GetCommandString)(UINT_PTR, UINT, UINT*, LPSTR, UINT); // IContextMenu2 STDMETHOD(HandleMenuMsg)(UINT, WPARAM, LPARAM); // IContextMenu3 STDMETHOD(HandleMenuMsg2)(UINT, WPARAM, LPARAM, LRESULT*);修改上下文菜单
如前, 我们处理 IContextMenu 的三个方法. 我们在 QueryContextMenu() 中加入一个菜单项. 我们先检查一下Shell 的版本. 如果为 4.71 或更高那我们就可以添加一个自画菜单项. 否则, 我们直接添加一个位图菜单项. 对于后者, 没别的什么工作了; 菜单会自动管理位图的显示.

首先是检查版本的代码. 它调用输出的 DllGetVersion() 函数来获取版本信息. 如果没有输出该函数, 那它就是4.00版本, 因为版本4.00没有提供 DllGetVersion() 函数.

STDMETHODIMP CBmpCtxMenuExt::QueryContextMenu ( HMENU hmenu, UINT uIndex, UINT uidCmdFirst, UINT uidCmdLast, UINT uFlags ){// 如果 CMF_DEFAULTONLY 标志被设置我们不作任何操作. if ( uFlags & CMF_DEFAULTONLY ) { return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 ); } bool bUseOwnerDraw = false; HINSTANCE hinstShell; hinstShell = GetModuleHandle ( _T("shell32") ); if ( NULL != hinstShell ) { DLLGETVERSIONPROC pProc; pProc = (DLLGETVERSIONPROC) GetProcAddress(hinstShell, "DllGetVersion"); if ( NULL != pProc ) { DLLVERSIONINFO rInfo = { sizeof(DLLVERSIONINFO) }; if ( SUCCEEDED( pProc ( &rInfo ) )) { if ( rInfo.dwMajorVersion > 4 || rInfo.dwMinorVersion >= 71 ) { bUseOwnerDraw = true; } } } }bUseOwnerDraw 表明是否使用自画菜单项. 如果为真, 我们插入自画菜单项 (看一下设置 mii.fType
的那行代码). 如果为假, 我们添加位图菜单项并告诉菜单显示的位图句柄. 代码使用 InsertMenuItem() API 来添加该项; 使用旧的InsertMenu() API 需要你再调用 ModifyMenu() 来改变为自画菜单.

MENUITEMINFO mii; mii.cbSize = sizeof(MENUITEMINFO); mii.fMask = MIIM_ID | MIIM_TYPE; mii.fType = bUseOwnerDraw ? MFT_OWNERDRAW : MFT_BITMAP; mii.wID = uidCmdFirst; if ( !bUseOwnerDraw ) { // 注意: 这会将整幅位图放入菜单. mii.dwTypeData = (LPTSTR) m_bmp.GetSafeHandle(); } InsertMenuItem ( hmenu, uIndex, TRUE, &mii ); // 存储菜单项的ID以备而后处理WM_MEASUREITEM/WM_DRAWITEM消息时的检查. m_uOurItemID = uidCmdFirst; // 告诉Shell我们添加了一项菜单. return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 1 );}我们将菜单项ID 保存在 m_uOurItemID 中(作者的废话真多) 所以在而后的消息处理中我们将知道此ID. 这不是必要的,因为我们只有一个菜单项, 而如果有多个菜单项这就是必须的.

在状态栏中显示帮助提示
显示帮助提示与前面的扩展没有什么不同. 浏览器调用 GetCommandString() 获取帮助字符串.

#include // 供ATL 字符转化宏使用 STDMETHODIMP CBmpCtxMenuExt::GetCommandString ( UINT uCmd, UINT uFlags, UINT* puReserved, LPSTR pszName, UINT cchMax ){static LPCTSTR szHelpString = _T("Select this thumbnail to view the entire picture."); USES_CONVERSION; // 检查 idCmd, 必须为0因为我们只有一个菜单项. if ( 0 != uCmd ) return E_INVALIDARG; // 如果浏览器索取帮助字符串, 赋值我们的字符串到提供的缓冲区中. if ( uFlags & GCS_HELPTEXT ) { if ( uFlags & GCS_UNICODE ) { // 我们需要将 pszName 变为Unicode 字符串, 并使用Unicode 字符串拷贝 API. lstrcpynW ( (LPWSTR) pszName, T2CW(szHelpString), cchMax ); } else { // 使用 ANSI 字符串拷贝 API 返回帮助字符串. lstrcpynA ( pszName, T2CA(szHelpString), cchMax ); } } return S_OK;}执行用户选择
IContextMenu 最后一个方法为 InvokeCommand(). 当用户点击我们添加的菜单时该方法被调用. 该扩展调用ShellExecute() 来打开位图文件.

STDMETHODIMP CBmpCtxMenuExt::InvokeCommand ( LPCMINVOKECOMMANDINFO pInfo ){ // 如果lpVerb 指向字符串, 忽略此次调用,退出 if ( 0 != HIWORD( pInfo->lpVerb )) return E_INVALIDARG; // 命令 ID 必须为0因为我们只添加了一个菜单项. if ( 0 != LOWORD( pInfo->lpVerb )) return E_INVALIDARG; // 使用默认程序打开位图. ShellExecute ( pInfo->hwnd, _T("open"), m_szFile, NULL, NULL, SW_SHOWNORMAL ); return S_OK;}自画菜单项
OK, 我敢打赌你已经烦透了上面的代码. 新鲜玩意来了! IContextMenu2 和 IContextMenu3 添加的两个方法如下所示. 它们仅调用另一个帮助函数, 而帮助函数又调用了一个消息处理器. 这是我所设计的一种方法以适用各种不同版本的消息处理器 (分别为 IContextMenu2 和 IContextMenu3). HandleMenuMsg2() 的 LRESULT* 参数有点奇怪,我在注释中有说明.

STDMETHODIMP CBmpCtxMenuExt::HandleMenuMsg ( UINT uMsg, WPARAM wParam, LPARAM lParam ){ AFX_MANAGE_STATE(AfxGetStaticModuleState()); // res 只是个虚设的LRESULT 变量. 它并没被真的使用(IContextMenu2::HandleMenuMsg() // 没有一种提供返回值的方法), 这只是为了使MenuMessageHandler() 的调用能够统一,而不论它是被哪个接口调用 // (IContextMenu2 or 3). LRESULT res; return MenuMessageHandler ( uMsg, wParam, lParam, &res );} STDMETHODIMP CBmpCtxMenuExt::HandleMenuMsg2 ( UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT* pResult ){ AFX_MANAGE_STATE(AfxGetStaticModuleState()); // 对于不需要返回值的消息, pResult 为 NULL. 这正是微软的意思,因为它要求在使用该值之前检查一下// pResult 是否可用. 你可能会想一个指向”返回值”的指针总是有效的,但是不!// 如果其为 NULL, 我就创建一个虚设的 LRESULT 变量, 所以在MenuMessageHandler()中的代码 // 总有一个有效的 pResult 指针. if ( NULL == pResult ) { LRESULT res; return MenuMessageHandler ( uMsg, wParam, lParam, &res ); } else { return MenuMessageHandler ( uMsg, wParam, lParam, pResult ); }}MenuMessageHandler() 分派 WM_MEASUREITEM 和 WM_DRAWITEM 消息给的消息处理函数.

STDMETHODIMP CBmpCtxMenuExt::MenuMessageHandler ( UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT* pResult ){ switch ( uMsg ) { case WM_MEASUREITEM: return OnMeasureItem ( (MEASUREITEMSTRUCT*) lParam, pResult ); break; case WM_DRAWITEM: return OnDrawItem ( (DRAWITEMSTRUCT*) lParam, pResult ); break; } return S_OK;}如我以前所述, 文档说Shell让我们的扩展处理 WM_INITMENUPOPUP 和 WM_MENUCHAR 消息, 但我始终没检测到这些消息.

处理WM_MEASUREITEM消息
Shell 发送 WM_MEASUREITEM 消息给我们的扩展以索取菜单大小数据. 我们先要检查是否是我们添加的菜单项所激发的调用. 如果测试通过, 我们获取位图的大小, 并计算整个菜单项的大小.

先是位图大小:

STDMETHODIMP CBmpCtxMenuExt::OnMeasureItem ( MEASUREITEMSTRUCT* pmis, LRESULT* pResult ){BITMAP bm;LONG lThumbWidth;LONG lThumbHeight; // 检查是否是我们添加的菜单项的激发的调用. if ( m_uOurItemID != pmis->itemID ) return S_OK; m_bmp.GetBitmap ( &bm ); m_lBmpWidth = bm.bmWidth; m_lBmpHeight = bm.bmHeight;接着, 我们计算缩略图的大小, 并据此计算整个菜单项的大小. 如果位图小于缩略图的最大值 (64x64象素) 那么就按原样输出. 否则, 就缩放成 64x64. 这可能扭曲位图的显示, 要使位图更好看留给你去练习.

// 计算位图缩略图大小. lThumbWidth = (m_lBmpWidth <= m_lMaxThumbnailSize) ? m_lBmpWidth : m_lMaxThumbnailSize; lThumbHeight = (m_lBmpHeight <= m_lMaxThumbnailSize) ? m_lBmpHeight : m_lMaxThumbnailSize; // 计算菜单项的大小, 即缩略图大小 + 边框的大小+ 空白间距 m_lItemWidth = lThumbWidth + m_lTotalBorderSpace; m_lItemHeight = lThumbHeight + m_lTotalBorderSpace;现在我们有了菜单项的大小, 我们将尺寸大小存回 MENUITEMSTRUCT. 浏览器将为我们的菜单项保留足够的空间.

// 将菜单项大小存回 MEASUREITEMSTRUCT. pmis->itemWidth = m_lItemWidth; pmis->itemHeight = m_lItemHeight; *pResult = TRUE; // 我们处理了消息 return S_OK;}处理 WM_DRAWITEM消息
当我们接收到 WM_DRAWITEM 消息, 浏览器会要求我们来画出菜单项. 我们先计算缩略图周围3D边框的RECT. 该RECT不必与菜单项所占的RECT相同, 因为菜单可能会宽于我们在 WM_MEASUREITEM 消息处理器中所设置的值.

STDMETHODIMP CBmpCtxMenuExt::OnDrawItem ( DRAWITEMSTRUCT* pdis, LRESULT* pResult ){CDC dcBmpSrc;CDC* pdcMenu = CDC::FromHandle ( pdis->hDC );CRect rcItem ( pdis->rcItem ); // 我们的菜单项的RECTCRect rcDraw; // 我们作图的RECT // 检查是否是我们的菜单项激发的调用. if ( m_uOurItemID != pdis->itemID ) return S_OK; // rcDraw 首先被设为我们在WM_MEASUREITEM消息中设置的 RECT. // 该矩形将被缩小. rcDraw.left = (rcItem.right + rcItem.left - m_lItemWidth) / 2; rcDraw.top = (rcItem.top + rcItem.bottom - m_lItemHeight) / 2; rcDraw.right = rcDraw.left + m_lItemWidth; rcDraw.bottom = rcDraw.top + m_lItemHeight; // 缩小 rcDraw 以适应缩略图周围的间隔空间. rcDraw.DeflateRect ( m_lMenuItemSpacing, m_lMenuItemSpacing );第一个作画步骤是画出菜单项背景. DRAWITEMSTRUCT 的成员变量itemState表明我们的菜单项是否被选中. 下面的代码决定使用的背景色.

// 填充菜单项的背景色. if ( pdis->itemState & ODS_SELECTED ) pdcMenu->FillSolidRect ( rcItem, GetSysColor ( COLOR_HIGHLIGHT )); else pdcMenu->FillSolidRect ( rcItem, GetSysColor ( COLOR_3DFACE ));接着, 画出下沉态的边框使得缩略图看上去嵌在菜单中.

// 画出下沉的3D边框. for ( int i = 1; i <= m_l3DBorderWidth; i++ ) { pdcMenu->Draw3dRect ( rcDraw, GetSysColor ( COLOR_3DDKSHADOW ), GetSysColor ( COLOR_3DHILIGHT )); rcDraw.DeflateRect ( 1, 1 ); }最后一步画出缩略图本身. 我简单地使用 StretchBlt() 调用来完成. 效果不是很好, 但我的目的是使代码尽量简单.

// 创建 DC 并将位图选进DC中 dcBmpSrc.CreateCompatibleDC ( &dc ); dcBmpSrc.SelectObject ( &m_bmp ); // 在DC上画出位图. pdcMenu->StretchBlt ( rcDraw.left, rcDraw.top, rcDraw.Width(), rcDraw.Height(), &dcBmpSrc, 0, 0, m_lBmpWidth, m_lBmpHeight, SRCCOPY ); *pResult = TRUE; // 我们处理了消息 return S_OK;}注意在一个实际的扩展中, 最好使用防闪烁的作图类, 这样当你的鼠标在其上移动时,菜单项不会闪烁t.

下面是这些菜单的一些快照! 第一个是自画菜单, 未选态和选择态.




下面是在shell 版本4.00时图象的样子. 由于选择态时的高亮反转了所有的颜色使其很难看.

注册Shell扩展
注册我们的位图快速浏览工具与以前我们注册上下文菜单扩展相同. 下面是所需的 RGS 脚本文件:HKCR

{ NoRemove Paint.Picture { NoRemove ShellEx { NoRemove ContextMenuHandlers { BitmapPreview = s ''{D6F469CD-3DC7-408F-BB5F-74A1CA2647C9}'' } } }}注意在这硬编码了 "Paint.Picture" 文件类型. 如果你不是使用画笔作为你的BMP默认浏览器, 你需要改变"Paint.Picture" 为存储在HKCR\.bmp 键的默认值. 你应该在 DllRegisterServer()做这些注册工作, 这样你可以检查"Paint.Picture" 是不是正确的键. 在 第一节 我讲的更多.

扩展 2 – 处理文件夹窗口背景的右击事件
在shell版本 4.71 或更高中, 你可以修改右击桌面或任一浏览器窗口背景时显示的上下文菜单. 编程这种扩展类似于其它上下文菜单扩展. 但有两个重要区别:

4. IShellExtInit::Initialize() 的参数的使用不同.

5. 扩展在不同的注册键下注册

我不会再重复扩展的建立步骤. 如果你想看全部的处理请看示例工程代码.

IShellExtInit::Initialize()中的不同
Initialize() 有个 pidlFolder 参数, 直到目前, 我们都一直忽略其因为它总为NULL. 现在该参数有用了! 它是右击的浏览器窗口的文件夹的 PIDL. 而第二个参数 IDataObject* 为NULL, 因为在这并没有所选择的文件.

以下是 Initialize() 的实现:

STDMETHODIMP CBkgndCtxMenuExt::Initialize ( LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDO, HKEY hkeyProgID ){// pidlFolder是右击的浏览器窗口的文件夹的 PIDL. 而第二个参数 IDataObject* 为NULL, // 因为在这并没有所选择的文件. // 我们使用SHGetPathFromIDList() API来获得路径名 return SHGetPathFromIDList ( pidlFolder, m_szDirClickedIn ) ? S_OK : E_INVALIDARG;}SHGetPathFromIDList() 函数返回文件夹的全路径并将之保存已备后用. 它返回一个 BOOL 表明成功与否.

注册上的不同
该扩展注册在一个不同的键下, 即 HKCR\Directory\Background\ShellEx\ContextMenuHandlers. 下面是注册的 RGS 脚本:

HKCR{ NoRemove Directory { NoRemove Background { NoRemove ShellEx { NoRemove ContextMenuHandlers { ForceRemove SimpleBkgndExtension = s ''{9E5E1445-6CEA-4761-8E45-AA19F654571E}'' } } } }}除了这两个区别之外, 该扩展跟其它上下文菜单扩展一样工作. IContextMenu::QueryContextMenu()里有一点要注意. uIndex 参数似乎总是 -1 (0xFFFFFFFF). 传递-1 给InsertMenu() 为索引值意味着菜单项添加在菜单底部. 但是, 如果递增 uIndex, 它会溢出到0, 意思是如果你传送 uIndex 给 InsertMenu(), 第二个菜单项将出现在菜单顶部. 检查一下例子工程的代码QueryContextMenu() 看一下如何正确的放置添加的菜单项.

下面是所修改的上下文菜单的样子, 在底部添加了两个菜单项. 注意 IMHO, 添加菜单项到菜单底部还有个问题. 当用户选择属性时习惯选中最后一项. 当我们的菜单项添加在其之后, 我们会破坏用户的习惯, 而导致失败和烦人的EMail. ;)

你可能要使你的菜单项突出显示, 但你这样会破坏用户的使用习惯. 你会使用户的注意力分散去找正确的菜单项. 所以慎重使用该类型的扩展.

 

第八节-如何使用信息栏扩展添加定制的信息栏到资源浏览器详细资料列表中

读者要求继续这份指南! 在本节, 我将处理添加定制的信息栏到Windows2000资源浏览器详细资料列表中. 这种类型的扩展在NT 4 或 Win 9x上不能用, 所以你必须在 Win 2K 运行本文的示例程序.

Windows 2000中的详细资料列表
Windows 2000 添加了许多自定义选择到浏览器的详细资料列表. 共有 37 种不同的信息栏可以使用! 你可以用两个方法来开关这些信息栏. 首先, 当你右击栏标题弹出的菜单里有8个可选栏:



如果你选择 More... 项, 浏览器显示一对话框,在其中你可以选择所有可获取的栏:



浏览器让我们可以在一些栏里显示自己的数据, 甚至可以使用栏处理器扩展添加信息栏. 但浏览器好像不让添加的栏显示在上下文菜单中.

本文的示例工程是为 MP3 文件设计的栏处理器,显示MP3文件的 ID3 标签的各资料.

使用 AppWizard 开始
运行 AppWizard 并生成一个名为MP3TagViewer的ATL工程. 由于这次我们要使用MFC,所以要选中 Support MFC设置, 点击”完成”. 然后,在ClassView树中右击MP3TagViewer classes 项,在弹出的菜单中选择New ATL Object,添加一个COM 对象类到 DLL中.

在 ATL Object Wizard 中, 第一页面已经选中了 Simple Object , 因此, 单击 Next. 在第二页面中, 在Short Name编辑框中输入 MP3ColExt点击 OK. (其余编辑框中的内容将自动填写.) 这样就创建了一个名为 CMP3ColExt 的已实现基本的COM接口的新COM对象类. 我们将在这个类中添加实现代码.

扩展接口
一个栏扩展只需实现一个接口: IColumnProvider. 它没有使用分离的IShellExtInit 或 IPersistFile 接口. 这是因为栏扩展作用于文件夹对象, 与当前选中的文件项目无关. 而 IShellExtInit 和 IPersistFile 接口处理有项目选择时的情况. 该扩展确实需要初始化, 但它是通过 IcolumnProvider 的一个方法.

要添加IColumnProvider 接口到我们的 COM 对象, 打开 MP3ColExt.h 并添加下面标红的代码:

#include
#include
#include
struct __declspec(uuid("E8025004-1C42-11d2-BE2C-00A0C9A83DA1")) IColumnProvider;
/////////////////////////////////////////////////////////////////////////////
// CMP3ColExt
class ATL_NO_VTABLE CMP3ColExt :
public CComObjectRootEx,
public CComCoClass,
public IMP3ColExt,
public IColumnProvider
{
BEGIN_COM_MAP(CMP3ColExt)
COM_INTERFACE_ENTRY(IMP3ColExt)
COM_INTERFACE_ENTRY(IColumnProvider)
END_COM_MAP()
public:
// IColumnProvider
STDMETHOD (Initialize)(LPCSHCOLUMNINIT psci) { return S_OK; }
STDMETHOD (GetColumnInfo)(DWORD dwIndex, SHCOLUMNINFO* psci);
STDMETHOD (GetItemData)(LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd, VARIANT* pvarData);
};
注意在类声明之前的 IColumnProvider 接口声明. 这是为了让 COM_INTERFACE_ENTRY 宏正确工作的必须条件. 微软忘了在comdef.h给IColumnProvider 接口定义一个UUID, 所以我们需要自己来定义. ATL 有个 COM_INTERFACE_ENTRY_IID 宏专门适用这种没有用__declspec(uuid()) 语法给接口赋予一个符号标志的情况, 但当我使用该宏时, 浏览器会传给 IDispatch::GetTypeInfo() 一个错误的指针使得扩展运行崩溃.

我们同时要修改一下stdafx.h. 因为我们使用 Win 2000 的特性功能, 我们需要用 #define 定义一些值好让我们获取使用该特性的函数及结构:

#define WINVER 0x0500 // W2K/98
#define _WIN32_WINNT 0x0500 // W2K
#define _WIN32_IE 0x0500 // IE 5+
初始化
IColumnProvider 接口有三个方法. 头一个是 Initialize(), 其原型为:

HRESULT IColumnProvider::Initialize ( LPCSHCOLUMNINIT psci );
Shell传递给我们一个 SHCOLUMNINIT 结构, 此时它仅包含少量的信息, 即浏览器窗口中所所查看得文件夹的全路径. 由于我们的扩展不需要这个信息, 所以只要返回 S_OK.

列举新信息栏
当浏览器看到我们的注册的栏扩展处理器, 它将调用扩展获取添加的栏的数据. 这些工作在 GetColumnInfo() 中完成, 其原型为:

HRESULT IColumnProvider::GetColumnInfo ( DWORD dwIndex, SHCOLUMNINFO* psci );
dwIndex 是个从0开始的计数指出浏览器要获取数据的栏. 另一个参数 SHCOLUMNINFO 结构需要我们往里填充数据.

SHCOLUMNINFO 结构的第一个成员是另一个结构:SHCOLUMNID. SHCOLUMNID 结构是 GUID/DWORD 对, 这里GUID 称为"格式 ID" 而 DWORD 值称为 "属性 ID." 这对值唯一地标识系统中的任一栏. 可以重用已存在的栏(如, Author), 这时 格式 ID 和 属性 ID 都是预设的值. 如果扩展要添加新的栏, 可以使用它的CLSID 作为 属性 ID (因为 CLSID 也是唯一的), 并使用一个简单的计数作为属性 ID.

我们的扩展将同时使用这两种方法. 我们将重用 Author, Title,和 Comments 栏, 并添加另外三个: MP3 唱片名, MP3 年份, 和 MP3 歌曲类型.

下面是 GetColumnInfo() 方法的开头部分:

STDMETHODIMP CMP3ColExt::GetColumnInfo ( DWORD dwIndex, SHCOLUMNINFO* psci )
{
// 我们有6个栏, 所以如果dwIndex 大等于 6, 就返回S_FALSE表明我们列举完所有的列了.
if ( dwIndex > 5 )
return S_FALSE;
如果dwIndex 大等于6, 我们返回 S_FALSE 以停止列举. 否则, 我们填充 SHCOLUMNINFO 结构. 对0到2的 dwIndex 值, 我们返回我们新栏的数据. 对3到5的值, 我们返回重用的内建栏数据. 下面是我们对第一列的数据设定, 其显示ID3标签的唱片名:

switch ( dwIndex )
{
case 0: // MP3唱片名
{
psci->scid.fmtid = *_Module.pguidVer; // 好用的 GUID
psci->scid.pid = 0; // 任一ID都可以用, 但使用序数最简单
psci->vt = VT_LPSTR; // 将返回字符串数据
psci->fmt = LVCFMT_LEFT; // 文本左对齐
psci->csFlags = SHCOLSTATE_TYPE_STR; // 数据按字符串顺序排列
psci->cChars = 32; // 默认的栏宽度

lstrcpynW ( psci->wszTitle, L"MP3 Album", MAX_COLUMN_NAME_LEN );
lstrcpynW ( psci->wszDescription, L"Album name of an MP3", MAX_COLUMN_DESC_LEN );
}
break;
我们使用自己的模块的GUID 作为格式 ID, 并用栏序数为属性ID. SHCOLUMNINIT 结构的Vt成员变量标识我们返回给浏览器的数据类型. VT_LPSTR 表示一个 C风格的字符串. fmt 成员变量是 LVCFMT_* 常数之一, 表示文本在栏里的对齐方式. 在这个例子里文本将左对齐.

csFlags 成员包含一些关于栏信息的标志. 但是, 并非所有的标志都为Shell所实现. 下面解释了设置标志所产生效果:

SHCOLSTATE_TYPE_STR, SHCOLSTATE_TYPE_INT, 和 SHCOLSTATE_TYPE_DATE

表示当浏览器进行排序时应如何处理栏数据. 三个可能值为字符串, 整数, 和日期.

SHCOLSTATE_ONBYDEFAULT

文档中说包含该标志会使该栏默认显示在文件夹窗口中, 直到用户禁用该栏. 可是我没能做出这种效果.

SHCOLSTATE_SLOW

根据文档上说, 包含该标志表明获取栏数据要费一点时间, 这样浏览器会运行后台线程来使浏览器界面保持反映. 但我在测试中发现没什么区别. 浏览器总是只使用一个线程来收集扩展栏的数据.

SHCOLSTATE_SECONDARYUI

文档中说设置该标志可以防止栏显示在标题的右击上下文菜单中. 这意味着如果你不包含该标志, 栏将会显示在上下文菜单中. 但是, 额外添加的栏总不会显示上下文菜单中, 所以该标志无效.

SHCOLSTATE_HIDDEN

传递该标志防止该栏显示在 Column Settings 对话框中. 因为目前没有一种方法能隐藏一个栏,该标志会使一个栏无效.

char szComment[30];
char byGenre;
};
所有的字段都是简单的字符, 并且字符串都不需要以null为结束符, 这需要一些特殊处理. 第一个字段, szTag, 包含字符"TAG" 来标识ID3 标签. byGenre 是标识歌曲类型的数字. (歌曲类型和ID都是预设好的值,可以从 ID3.org 上获取.)

我们也需要另一个结构保存 ID3 标签和标签来源的文件名. 该结构的使用我稍后介绍.

#include
#include
typedef std::basic_string tstring; // TCHAR 字符串

struct CID3CacheEntry
{
tstring sFilename;
CID3v1Tag rTag;
};

typedef std::list list_ID3Cache;
CID3CacheEntry 对象保存文件名和ID3 标签. list_ID3Cache 是一个CID3CacheEntry 结构的列表.

OK, 回到扩展的话题上. 下面是 GetItemData() 函数的开始. 我们首先检查 SHCOLUMNID 结构以确认是我们定制的栏所激发的调用.

#include

STDMETHODIMP CMP3ColExt::GetItemData (
LPCSHCOLUMNID pscid,
LPCSHCOLUMNDATA pscd,
VARIANT* pvarData )
{
USES_CONVERSION;
LPCTSTR szFilename = OLE2CT(pscd->wszFile);
char szField[31];
TCHAR szDisplayStr[31];
bool bUsingBuiltinCol = false;
CID3v1Tag rTag;
bool bCacheHit = false;

// 检查是不是我们希望的格式ID和栏数字.
if ( pscid->fmtid == *_Module.pguidVer )
{
if ( pscid->pid > 2 )
return S_FALSE;
}
如果格式ID 是我们的GUID, 属性 ID 必须是 0, 1, or 2, 因为这些ID我们要在 GetColumnInfo() 中使用. 如果, 因为某些原因, ID超出这个范围, 我们返回 S_FALSE 告诉Shell 我们没有相应的数据, 该栏将显示为空.

我们接着比较该格式ID 与 FMTID_SummaryInformation, 并检查属性ID看是否是我们提供的属性.

else if ( pscid->fmtid == FMTID_SummaryInformation )
{
bUsingBuiltinCol = true;

if ( pscid->pid != 2 && pscid->pid != 4 && pscid->pid != 6 )
return S_FALSE;
}
else
{
return S_FALSE;
}
接着, 我们检查传进的文件的属性. 如果是个目录, 或其为离线文件 (即, 它被移到另一存储介质上), 退出. 同时我们检查其文件扩展名,如果不是 .MP3 就退出.

// 如果是文件夹,退出.
// 如果是离线文件,退出。
if ( pscd->dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_OFFLINE) )
return S_FALSE;

// 检查扩展名. 如果不是 .MP3, 退出.
if ( 0 != lstrcmpiW ( pscd->pwszExt, L".mp3" ))
return S_FALSE;
此时,我们要开始操作该文件. 下面是我们的 ID3 标签的使用. MSDN 文档说Shell以文件为组调用 GetItemData(), 意思是它将使用相同的文件名连续调用 GetItemData()(就是一组处理完一个文件的栏数据的显示,再接着处理下一个文件). 我们利用这一点缓存特殊文件的ID3 标签, 因此我们不用反复读取文件的标签.

我们首先轮循缓冲(保存为一个变量, m_ID3Cache), 并对比缓冲的文件名和传进来的文件名. 如果我们发现我们的文件名已在缓冲中, 我们直接获取关联的 ID3 标签.

// 在缓冲中查找文件名.
list_ID3Cache::const_iterator it, itEnd;

for ( it = m_ID3Cache.begin(), itEnd = m_ID3Cache.end();
!bCacheHit && it != itEnd; it++ )
{
if ( 0 == lstrcmpi ( szFilename, it->sFilename.c_str() ))
{
CopyMemory ( &rTag, &it->rTag, sizeof(CID3v1Tag) );
bCacheHit = true;
}
}
在循环后如果 bCacheHit 为假, 我们需要读取文件再看一下它是否有 ID3 标签. 该帮助函数 ReadTagFromFile() 读取该文件的最后128 个字节, 并返回TRUE表示成功或 FALSE 表示失败. 注意ReadTagFromFile() 返回最后128 字节而不管它们是否真是ID3 标签.

// 如果文件标签不再缓冲中, 从文件中读取.
if ( !bCacheHit )
{
if ( !ReadTagFromFile ( szFilename, &rTag ))
return S_FALSE;
所以现在我们有了ID3 标签. 我们检查缓冲的大小, 它包含5项, 最旧将被移出缓冲以腾出空间给新的项. 我们创建了一个新的 CID3CacheEntry 对象并将之添加进列表.

// 我们将保留最近5个文件的 – 移出最旧的
// entries if the cache is bigger than 4 entries.
while ( m_ID3Cache.size() > 4 )
{
m_ID3Cache.pop_back();
}

// 添加新的 ID3到缓冲.
CID3CacheEntry entry;

entry.sFilename = szFilename;
CopyMemory ( &entry.rTag, &rTag, sizeof(CID3v1Tag) );

m_ID3Cache.push_front ( entry );
} // end if(!bCacheHit)
下一步要测试头三个标志字节以决定有无 ID3 标签数据. 如果没有, 立即返回.

// 测试头三个标志字节以决定有无 ID3 标签数据
if ( 0 != StrCmpNA ( rTag.szTag, "TAG", 3 ))
return S_FALSE;
接着,我们读取ID3 标签中的Shell要求的属性字段. 这涉及到属性ID的测试. 下面是一个例子, 获取歌曲标题字段:

// 格式化数据字符.
if ( bUsingBuiltinCol )
{
switch ( pscid->pid )
{
case 2: // 歌曲标题
CopyMemory ( szField, rTag.szTitle, countof(rTag.szTitle) );
szField[30] = ''\0'';
break;
...
}
注意我们的 szField 缓冲是 31 个字符长, 比最长的 ID3v1 字段大1. 这样做可以保证以 null为字符串的结束符. The bUsingBuiltinCol 标志在先前我们测试 FMTID/PID 对时已设置. 我们需要该标志因为只有PID 不足以标识一个栏 – 标题及MP3 类型栏的属性ID均为2.

此时, szField 包含我们从 ID3 标签中读出的字符串. WinAmp 的 ID3 标签编辑器用空格隔开字符串而不用null 字符串, 所以我们通过删除尾部空格来修正这一点:

// // WinAmp 用空格隔开字符串而不用null 字符串, 我们通过删除尾部空格来修正这一点.
StrTrimA ( szField, " " );
最后, 我们创建一个 CComVariant 对象并储存 szDisplayStr 字符串. 接着我们调用 CComVariant::Detach() 将数据从CComVariant 拷贝到浏览器给出的 VARIANT 变量中.

// 创建一个详细字符串的 VARIANT, 并将之返回给shell.
CComVariant vData ( szField );

vData.Detach ( pvarData );

return S_OK;
}
它是什么样的?
我们添加的新栏出现在 Column Settings 对话框列表的末尾:



下面是栏的样子. 文件根据我们自定义Author 字段进行排序.



注册Shell扩展
因为栏扩展处理器扩展了文件夹, 其应该在 HKCR\Folders 键下进行注册. 下面是注册所需的 RGS 脚本文件:

HKCR
{
NoRemove Folder
{
NoRemove Shellex
{
NoRemove ColumnHandlers
{
ForceRemove {AC146E80-3679-4BCA-9BE4-E36512573E6C} = s ''MP3 ID3v1 viewer column ext''
}
}
}
}
另一个好东西 - InfoTips
栏扩展处理器能完成的另一个有趣的东西是为一种文件类型自定义InfoTip. 下面的 RGS 脚本文件为MP3文件类型创建一个自定义InfoTip:

HKCR
{
NoRemove .mp3
{
val InfoTip = s ''prop:Type;Author;Title;Comment;{AC146E80-3679-4BCA-9BE4-E36512573E6C},0;
{AC146E80-3679-4BCA-9BE4-E36512573E6C},1;{AC146E80-3679-4BCA-9BE4-E36512573E6C},2;Size''
}
}
注意Author, Title, 和Comment 字段出现在prop 中: 当你的鼠标盘旋于MP3 文件之上时, 浏览器将调用我们的扩展获取这些字段的显示字符串. MSDN 文档上说我们添加的定制字段也会出现在 InfoTips 上 (这是为什么我们的GUID 和属性ID出现在上面的字符串中), 然而在Win2K 下这不能工作. 只有内建的属性才出现在 InfoTips 中. 下面是 InfoTip 的样子:

第九节 - 如何编写定制文件类型显示图标的Shell扩展

啊, 我们到第九节了! 本文是应另一位读者的要求而写, 将讨论怎样为同一种文件类型的每一个文件显示定制的图标 (在本文中以文本文件为例). 示例代码适用于 Windows 9x 和NT/2000. (我还没用过 WinMe, 所以没在Me上测试过,但应该也可以)

浏览器中的文件图标
大家都知道每种文件类型在浏览器中都有一个特殊的图标来标识. 位图文件显示一个画筒图标, HTML文件显示IE页的图标,等等. 浏览器根据注册表中的数据决定该使用哪个图标, 并读取 HKEY_CLASSES_ROOT 下的对应于文件类型的键的数据. 这样的结果是每种类型文件显示同一个图标.

但是, 这并不是指定图标的唯一方法. 使用图标扩展处理,浏览器可以让我们自定义对应每一个文件的图标. 实际上, Windows内部就有一个这样的扩展. 打开 Windows 目录 (或任何一个有有许多 EXE 文件的目录) ,你会发现每一个 EXE 都有不同的图标(除开没有图标资源的 EXE文件). ICO 和 CUR 文件也都有不同的图标.

本文将编写一个图标扩展处理器,根据文本文件的大小为其显示4种图标. 图标如下:

- 8K 或更大

- 4K 到 8K

- 1 字节到 4K

- 0字节

使用 AppWizard 开始
运行 AppWizard 并生成一个名为TxtFileIcons的ATL工程. 由于这次我们要使用MFC,所以要选中 Support MFC设置, 点击”完成”. 然后,在ClassView树中右击 TxtFileIcons classes 项,在弹出的菜单中选择New ATL Object,添加一个COM 对象类到 DLL中.

在 ATL Object Wizard 中, 第一页面已经选中了 Simple Object , 因此, 单击 Next. 在第二页面中, 在Short Name编辑框中输入TxtIconShlExt 点击 OK. (其余编辑框中的内容将自动填写.) 这样就创建了一个名为 CTxtIconShlExt 的已实现基本的COM接口的新COM对象类. 我们将在这个类中添加实现代码.

扩展接口
图标扩展处理器实现两个接口 IPersistFile 和 IExtractIcon. 记得IPersistFile 用于初始化只涉及一个选择文件时的处理, 而 IShellExtInit 接口用于一次有多个选择文件时的处理. IExtractIcon 有两个方法,它们的作用是告诉浏览器所使用的图标.

记住:浏览器为显示的每一个文件都将创建一个COM 对象. 这就是说每一个文件都将有一个COM C++类对象对应. 因此在你的扩展中应该避免费时的操作以防止浏览界面反应迟滞.

初始化接口
要添加 IPersistFile 接口道 COM 对象, 打开 TxtIconShlExt.h 并添加如下标红的代码.

#include
#include
#include

/////////////////////////////////////////////////////////////////////////////
// CTxtIconShlExt

class ATL_NO_VTABLE CTxtIconShlExt :
public CComObjectRootEx,
public CComCoClass,
public IDispatchImpl,
public IPersistFile
{
BEGIN_COM_MAP(CTxtIconShlExt)
COM_INTERFACE_ENTRY(ITxtIconShlExt)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IPersistFile)
END_COM_MAP()

public:
// IPersistFile
STDMETHOD(GetClassID)( CLSID* ) { return E_NOTIMPL; }
STDMETHOD(IsDirty)() { return E_NOTIMPL; }
STDMETHOD(Save)( LPCOLESTR, BOOL ) { return E_NOTIMPL; }
STDMETHOD(SaveCompleted)( LPCOLESTR ) { return E_NOTIMPL; }
STDMETHOD(GetCurFile)( LPOLESTR* ) { return E_NOTIMPL; }
STDMETHOD(Load)( LPCOLESTR wszFile, DWORD /*dwMode*/ )
{
USES_CONVERSION;
lstrcpyn ( m_szFilename, OLE2CT(wszFile), MAX_PATH );
return S_OK;
}

protected:
TCHAR m_szFilename [MAX_PATH]; // Full path to the file in question.
DWORDLONG m_ldwFileSize; // File size; used by extraction method 2.
};
跟其它使用 IpersistFile 接口的扩展一样, 所需要实现的接口方法只有 Load(), 在这里浏览器将通知我们被选择操作的文件. Load() 的实现只是拷贝文件名到 m_szFilename 变量以备后用.

IExtractIcon 接口
图标扩展处理器实现IExtractIcon 接口, 当浏览器需要为文件显示一个图标时将调用该接口. 因为我们的扩展用于文本文件, 浏览器将在每次显示文本文件对象时调用 IExtractIcon 的方法. 要添加 IExtractIcon 接口, 打开TxtIconShlExt.h 并添加如下标红的代码:

/////////////////////////////////////////////////////////////////////////////
// CTxtIconShlExt

class ATL_NO_VTABLE CTxtIconShlExt :
public CComObjectRootEx,
public CComCoClass,
public IDispatchImpl,
public IPersistFile,
public IExtractIcon
{
BEGIN_COM_MAP(CTxtIconShlExt)
COM_INTERFACE_ENTRY(ITxtIconShlExt)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IPersistFile)
COM_INTERFACE_ENTRY(IExtractIcon)
END_COM_MAP()

public:
// IExtractIcon
STDMETHOD(GetIconLocation)( UINT uFlags, LPTSTR szIconFile, UINT cchMax,
int* piIndex, UINT* pwFlags );
STDMETHOD(Extract)( LPCTSTR pszFile, UINT nIconIndex, HICON* phiconLarge,
HICON* phiconSmall, UINT nIconSize );
};
有两种方法可将图标返回给浏览器. 第一种是 GetIconLocation() 可以返回文件名/索引对以指出包含图标的文件,和图标在该文件中索引位置(以0为基). 例如本例子中为 C:\windows\system\shell32.dll/9, 这就是告诉浏览器使用Shell32.dll的第9个图标(以0为基). 这不是说使用 ID 为9的图标, 而是使用第九个图标. Extract() 只需返回S_FALSE 给浏览器让它自己来解析图标.

该方法的特别之处在于浏览器在 GetIconLocation() 返回之后不一定会调用 Extract(). 浏览器会保持一个图标缓存以存储最近使用的图标. 如果 GetIconLocation() 返回最近已使用的文件名/索引对, 而且图标仍然在缓存中, 浏览器就可以直接使用缓存中的图标而不会去调用Extract().

第二种方法是从GetIconLocation() 中返回不要查看缓冲的标志, 这样会使浏览器去调用Extract(). Extract() 则负责加载图标资源并将其句柄返回给浏览器.

第一种解析方法
IExtractIcon 的GetIconLocation() 方法最先被调用. 该函数检查所选择的文件名并返回文件名/索引对. 其原型为:

HRESULT IExtractIcon::GetIconLocation (
UINT uFlags,
LPTSTR szIconFile,
UINT cchMax,
int* piIndex,
UINT* pwFlags );
其参数为:

uFlags

改变扩展行为的标志. GIL_ASYNC 表示询问扩展的处理要不要花费长时间, 如果要, 扩展可以要求扩展以后台线程运行, 这样浏览器界面不会出现迟滞. 其余的标志, GIL_FORSHELL 和 GIL_OPENICON, 只在名字空间扩展中有意义. 现在我们不必去管这些标志,因为我们的扩展不会耗时太长.


szIconFile, cchMax

szIconFile 是由shell 提供的一个缓冲要求我们填入包含所使用的图标的文件名. cchMax 是该缓冲区的大小.


piIndex

int 的指针,要求我们添入图标在文件中的索引.


pwFlags

UINT 的指针,要求我们返回影响浏览器行为的标志.

GetIconLocation() 填写 szIconFile 和piIndex 参数并返回 S_OK. 如果我们根本不想使用自定义的图标也可以返回S_FALSE ,此时浏览器会显示一个未知文件类型的图标 (). 可以在 pwFlags 中返回的标志有:

GIL_DONTCACHE

告诉浏览器不要检查图标缓冲而去使用最近的 szIconFile/piIndex 对. 其结果是IExtractIcon::Extract() 将被调用.

GIL_NOTFILENAME

根据 MSDN, 该标志告诉浏览器当GetIconLocation()返回时忽略 szIconFile/piIndex 的内容. 很显然, 这是扩展告诉浏览器去调用 IExtractIcon::Extract() 的方法, 然而实际上该标志对浏览器的行为并无影响

GIL_SIMULATEDOC

该标志告诉浏览器将返回的图标叠放在卷边文档图标上, 并使用生成的这个图标. 我将在下面解释这一点.

在方法1中, 我们的扩展的 GetIconLocation() 方法取得文件大小, 并根据文件大小, 返回0 到3 的一个索引. 这带来该方法的一个缺陷 – 你需要注意你的资源 ID 确保它们的顺序固定. 我们的扩展只有 4 个图标, 所以这不会困难, 但如果你有多个图标, 或者你在工程中添加/删除图标时, 你必须注意你的资源ID.

下面是 GetIconLocation() 函数.我们首先打开文件,获取大小. 如果中途发生错误, 我们返回S_FALSE 让浏览器使用默认的图标.

STDMETHODIMP CTxtIconShlExt::GetIconLocation (
UINT uFlags,
LPTSTR szIconFile,
UINT cchMax,
int* piIndex,
UINT* pwFlags )
{
DWORD dwFileSizeLo, dwFileSizeHi;
DWORDLONG ldwSize;
HANDLE hFile;

hFile = CreateFile ( m_szFilename, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );

if ( INVALID_HANDLE_VALUE == hFile )
return S_FALSE; //让浏览器使用默认的图标

dwFileSizeLo = GetFileSize ( hFile, &dwFileSizeHi );

CloseHandle ( hFile );

if ( (DWORD) -1 == dwFileSizeLo && GetLastError() != NO_ERROR )
return S_FALSE; //让浏览器使用默认的图标

ldwSize = ((DWORDLONG) dwFileSizeHi)<<32 | dwFileSizeLo;
接着我们取得我们的DLL的路径名,因为它包含图标. 并将其拷贝到 szIconFile 缓冲里.

TCHAR szModulePath[MAX_PATH];

GetModuleFileName ( _Module.GetModuleInstance(), szModulePath, MAX_PATH );
lstrcpyn ( szIconFile, szModulePath, cchMax );
接着, 我们检查文件大小并设置 piIndex.

if ( 0 == ldwSize )
*piIndex = 0;
else if ( ldwSize < 4096 )
*piIndex = 1;
else if ( ldwSize < 8192 )
*piIndex = 2;
else
*piIndex = 3;
最后我们设置 pwFlags 为0让浏览器执行默认动作. 就是检查图标缓冲看szIconFile/piIndex 对是否已在缓冲里. 如果在, 就不调用IExtractIcon::Extract(). 我们返回 S_OK 表示 GetIconLocation() 成功了.

*pwFlags = 0;
return S_OK;
}
由于我们已经告诉浏览器图标的位置,Extract() 返回S_FALSE 即可.

STDMETHODIMP CTxtIconShlExt::Extract (
LPCTSTR pszFile,
UINT nIconIndex,
HICON* phiconLarge,
HICON* phiconSmall,
UINT nIconSize )
{
return S_FALSE; // 告诉浏览器自己去解析图标.
}

注意大图标视图, 使用了图标的 16x16 版本. 在小图标视图, 浏览器缩小图标, 这样显示不太准确.

解析方法2
方法 2 涉及我们的扩展自己解析图标, 并忽略浏览器缓冲的图标. 使用这个方法, IExtractIcon::Extract() 总被调用,并负责加载图标并返回两个图标句柄 HICON 给浏览器 – 一个是大图标, 一个是小图标. 该方法的好处是你不必考虑你的图标资源在文件中的顺序位置. 其缺陷在于它忽略了浏览器的图标缓冲,这会使显示速度减慢,特别是在有浏览有无数个文件的目录时.

GetIconLocation() 同方法 1, 但在这里它只要获得文件的大小即可.

STDMETHODIMP CTxtIconShlExt::GetIconLocation (
UINT uFlags,
LPTSTR szIconFile,
UINT cchMax,
int* piIndex,
UINT* pwFlags )
{
DWORD dwFileSizeLo, dwFileSizeHi;
HANDLE hFile;

hFile = CreateFile ( m_szFilename, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );

if ( INVALID_HANDLE_VALUE == hFile )
return S_FALSE; //让浏览器使用默认的图标

dwFileSizeLo = GetFileSize ( hFile, &dwFileSizeHi );

CloseHandle ( hFile );

if ( (DWORD) -1 == dwFileSizeLo && GetLastError() != NO_ERROR )
return S_FALSE; //让浏览器使用默认的图标

m_ldwFileSize = ((DWORDLONG) dwFileSizeHi)<<32 | dwFileSizeLo;
一旦我们保存了文件大小, 我们就可以设置 pwFlags 为 GIL_DONTCACHE 以告诉浏览器不要检查图标内存. 我们必须设置该标志因为我们不填写 szIconFile/piIndex 对并且通知浏览器忽略它们.

GIL_NOTFILENAME 标志也应该被设置, 尽管在当前版本中其没有任何效果. 文档上是说这会告诉浏览器我们没有填写 szIconFile/piIndex 对, 似乎其并没有被浏览器所检查. 但包含该标志总是有好处的, 以防未来的版本会测试这个标志.

*pwFlags = GIL_NOTFILENAME | GIL_DONTCACHE;
return S_OK;
}
现在仔细看一下 Extract(). 其原型为:

HRESULT IExtractIcon::Extract (
LPCTSTR pszFile,
UINT nIconIndex,
HICON* phiconLarge,
HICON* phiconSmall,
UINT nIconSize );
其参数为:

pszFile/nIconIndex

文件名和索引指定图标位置. 其值与从 GetIconLocation() 返回的一样.

phiconLarge, phiconSmall

HICON 的指针,由 Extract() 返回指向大图标和小图标的句柄数组.

nIconSize

指定要求的图标大小. 高字为小图标的长度 (长宽一致), 低字为大图标的长度. 在一般情况下, 其值为0x00100020 (高字16, 低字 32) 表示小图标应该是 16x16, 大图标为 32x32.

在我们的扩展中, 我们并没有在 GetIconLocation() 里填写 pszFile 和 nIconIndex 所以在这忽略之. 我们只加载图标并返回给浏览器.

STDMETHODIMP CTxtIconShlExt::Extract (
LPCTSTR pszFile,
UINT nIconIndex,
HICON* phiconLarge,
HICON* phiconSmall,
UINT nIconSize )
{
UINT uIconID;

// 根据文件大小决定使用哪个图标.
if ( 0 == m_ldwFileSize )
uIconID = IDI_ZERO_BYTES;
else if ( m_ldwFileSize < 4096 )
uIconID = IDI_UNDER_4K;
else if ( m_ldwFileSize < 8192 )
uIconID = IDI_UNDER_8K;
else
uIconID = IDI_OVER_8K;

// 加载图标
*phiconLarge = (HICON) LoadImage ( _Module.GetResourceInstance(),
MAKEINTRESOURCE(uIconID), IMAGE_ICON,
32, 32, LR_DEFAULTCOLOR );

*phiconSmall = (HICON) LoadImage ( _Module.GetResourceInstance(),
MAKEINTRESOURCE(uIconID), IMAGE_ICON,
16, 16, LR_DEFAULTCOLOR );

return S_OK;
}
终于完了! 浏览器将显示我们返回的图标.

要注意的一个细节是当使用方法2, 从 GetIconLocation() 返回 GIL_SIMULATEDOC 标志没有任何效果.

注册扩展
图标扩展处理器注册在文件类型键下, 所以在我们的例子中它在 HKCR\txtfile 下. 正如其它例子, 在txtfile键下有个 ShellEx. 接着是个 IconHandler 键, 该键的默认值为我们的扩展的 GUID. 注意每种文件类型只能有一个图标扩展处理器. 我们也必须更改 DefaultIcon 键的默认值为 "%1" 以激活我们的扩展.

下面是注册所需的 RGS 脚本文件:

HKCR
{
NoRemove txtfile
{
NoRemove DefaultIcon = s '%%1'
NoRemove ShellEx
{
ForceRemove IconHandler = s '{DF4F5AE4-E795-4C12-BC26-7726C27F71AE}'
}
}
}
注意为了指定 "%1" 字符串我们要在脚本中写 "%%1" , 因为 % 是个表示可替换参数的特殊字符(如, "%MODULE%").

覆盖现存的DefaultIcon 值会产生一个问题. 当我们注销扩展时怎样回写DefaultIcon的原值? 答案是我们在 DllRegisterServer()时将原来DefaultIcon 的值保存下来, 并在 DllUnregisterServer() 中还原. 我们必须这样做使注册清除干净,恢复原来文件图标的显示.

你可以看一下注册/注销函数是如何工作的. 我们调用ATL处理RGS脚本进行备份, 因为如果我们按其它方法, DefaultIcon 在我们有机会备份之前就会被覆盖.

要设置目录的特别图标,只要在该目录下生成一个desktop.ini文件,格式如下:

[.ShellClassInfo]

IconFile=C:\VcFolder.ico

IconIndex=0



<< Home

This page is powered by Blogger. Isn't yours?