(本文根据《 Windows Shell 扩展编程完全指南》改写)
开始编写上下文菜单 – 它该做些什么 ?
开头先让我们做简单一些 , 只弹出一个对话框以表明当前的扩展能够正常地工作 .
我们把扩展关联到 .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 "shlobj.h" |
COM_MAP是ATL实现 QueryInterface() 机制的宏,它包含的列表告诉ATL其它外部程序用QueryInterface()能从我们的 COM对象获取哪些接口.
接着,在类声明里, 加入Initialize() 的函数原型.
另外我们需要一个变量来保存文件名:
protected: |
然后, 在 SimpleShlExt.cpp 文件中, 加入该函数方法的实现定义:
HRESULT CSimpleShlExt::Initialize ( LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hProgID ) |
我们要做的是取得当前鼠标所在的窗口,并把它和桌面上的ListView
做比较,如果二者不同,则鼠标是在其他Dictionary上点击,不添加
菜单,直接返回:
{ Wnd=::GetDesktopWindow(); Wnd=FindWindowEx(Wnd, 0, "Progman", NULL); Wnd = ::FindWindowEx(Wnd, 0, "SHELLDLL_DefView", NULL); Wnd = ::FindWindowEx(Wnd, 0, "SysListView32", NULL);
POINT Point; ::GetCursorPos(&Point);
if(::WindowFromPoint(Point)!=Wnd) return E_INVALIDARG;
return S_OK; } |
要是我们返回 E_INVALIDARG , Explorer 将不会继续调用以后的扩展代码 .
要是返回 S_OK , Explorer 将再一次调用 QueryInterface() 获取另一个我们下面就要添加的接口指针 : IContextMenu .
与上下文菜单交互的接口
一旦 Explorer 初始化了扩展 , 它就会接着调用 IContextMenu 的方法让我们添加菜单项 , 提供状态栏上的提示 , 并响应执行用户的选择 .
添加 IContextMenu 接口到 Shell 扩展类似于上面 IshellExtInit 接口的添加 . 打开 SimpleShlExt.h ,添加下列标红的代码:
class ATL_NO_VTABLE CSimpleShlExt : |
添加 IContextMenu 方法的函数原型 :
public: |
修改上下文菜单 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, {
//
如果标志包含
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 "atlconv.h"
//
为使用
ATL
字符串转换宏而包含的头文件
HRESULT CSimpleShlExt::GetCommandString( UINT idCmd, UINT uFlags, {
USES_CONVERSION; // 检查 idCmd, 它必须是 0 ,因为我们仅有一个添加的菜单项 . if ( 0 != idCmd ) return E_INVALIDARG;
// 如果 Explorer 要求帮助字符串,就将它拷贝到提供的缓冲区中 . if ( uFlags & GCS_HELPTEXT ) { LPCTSTR szText = _T(" 透明图标 "); 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: { HWND Wnd; Wnd=::GetDesktopWindow(); Wnd=FindWindowEx(Wnd, 0, "Progman", NULL); Wnd = ::FindWindowEx(Wnd, 0, "SHELLDLL_DefView", NULL); Wnd = ::FindWindowEx(Wnd, 0, "SysListView32", NULL); ::SendMessage(Wnd, LVM_SETTEXTBKCOLOR, 0, 0xffffffff); ::InvalidateRect(Wnd, NULL, TRUE);
return S_OK; } break; default: return E_INVALIDARG; break; }
}
|
注册Shell扩展
现在我们已经实现了所有需要的 COM 接口 . 可是我们怎样才能让浏览器使用我们的扩展呢 ?
ATL 自动生成注册 COM DLL 服务器的代码 , 但这只是让其它程序可以使用我们的 DLL.
最后 ,在shell版本 4.71+中, 你可以让上下文菜单在用户右击浏览器窗口(包括桌面)的背景时激发.
要让你的扩展在这种情况下被激发,需要在HKCR/Directory/Background/shellex/ContextMenuHandlers 键下进行注册.
使用该方法, 你可以添加定制菜单到桌面或任意目录上下文菜单.
这时传送到 IShellExtInit::Initialize() 的参数有些不同,所以我将在以后的文章中讲述这方面的内容.