第一节:windows shell扩展初步:上下文菜单扩展
作者:michael dunn
译者:yesaidu
目录
● readme
● 系列绪言
● 第一部分绪言
● 从appwizard开始
● 初始化接口
● 上下文菜单交互接口
○ 更改上下文菜单
○ 在状态栏显示拉线式(fly-by)帮助
○ 执行用户选择
○ 其他代码细节
● 注册shell扩展
● 调试shell扩展
● 所有的外观
● 版权与许可
● 修订历史
readme
我想,你在行动之前,或者你在本手册的讨论板发帖之前应该阅读这份材料。
本手册最初是用vc 6编写的。现在,vc8都出来了,我感觉是时候对本手册进行升级到vc7.1了。(通过vc7.1自动升级vc6项目,并不一定会完全地完成代码转换;因此,vc7.1用户可能碰到这样的现象,即在转换、编译示例代码后,运行时可能没有效果或出错。)只要我仔细检查并更新本手册,本手册将体现vc7.1的新特点。我将会提供vc7.1项目的源码下载。
vc2005用户要注意了:vc2005体验版(express edition)没有一同发布atl或mfc。既然本手册用到了atl,有时还使用了mfc,因此,你不能用vc2005体验版来编译示例代码。
vc7用户注意了:如果你没有更新psdk,必须改变默认的include路径。确信“vc++目录”-“包含文件”列表的第一项是<code>$(vcinstalldir)platformsdk\include</code>,它在<code>($vcinstalldir)include</code>前面,如下图:
由于一直没有使用过vc 8,因此我不确定示例代码在vc 8上是否可以通过编译。只是希望,把vc7项目升级到vc8的自动转换功能比从vc6到vc7的要好些。如果你使用vc8编译示例时遇到了任何疑惑,请在讨论板发帖。
手册绪言
第一节介绍了shell扩展的概要,并提供了一个上下文菜单扩展的示例,使你对后面的章节充满兴趣。
从字面上看,shell扩展包括两个方面:shell和扩展。所谓shell,就是资源管理器explorer;而扩展就是指在预定的事件发生时由explorer调用执行的代码(比如,在.doc文件上右击)。因此,shell扩展就是为explorer增添功能的com对象。
shell扩展是一个进程内服务器,它实现了跟explorer通信的接口。atl是设计一个shell扩展,并使之运行的最简单办法;这样你就不用为一遍又一遍的编写<code>queryinterface()</code>和<code>addref()</code>而大伤脑筋。在windows nt下调试shell扩展要更容易些,这点,我在后面还会谈到。
shell扩展有很多种类型,每一类型都有其被调用的时机:即每种类型在不同的事件发生时被调用执行。下表列出了一些较常见的类型,以及它们被调用的情况:
类型
被调用的时机
它可以做什么
context menu扩展处理器
用户在文件对象或文件夹对象或目录窗口背景(需要shell v 4.71+以上)单击右键
在上下文菜单中添加菜单项
property sheet扩展处理器
文件属性对话框显示时
在属性对话框中定制属性页
drag and drop扩展处理器
用户用右键拖放文件到文件夹窗口或桌面时
drop handler扩展处理器
用户拖对象并将其放到文件上时
任何你想做的
queryinfo 扩展处理器
(需要shell version 4.71+)
用户在文件、“我的电脑”等其他shell对象的图标上悬停时
返回一个explorer显示在工具提示中的字符串
第一节绪言
现在,你可能有很多的疑问:为什么扩展看起来像explorer?它到底是什么样的?一个例子就是winzip(或者winrar,我没安装winzip ^_^――译者)——它包含了多种shell扩展,其中之一就是上下文扩展。下图是winzip(其实是winra的 ^_^――译者)为压缩文件在上下文菜单中添加的菜单项:
winzip编写了增加菜单项的代码,提供了explorer状态栏上的菜单项帮助提示(fly-by help),并在用户选择一个winzip菜单命令时执行相应的操作。
winzip还提供了拖曳扩展处理,此类型跟上下文菜单扩展非常相似,但它是在用户通过右键拖曳文件时才被触发。下图是winzip(也是winra的 ^_^――译者)拖曳文件弹出的菜单项:
还有很多的shell扩展类型,microsoft不断向每一个新的windows版本中增加更多的类型。现在,让我们把注意力放到上下文菜单扩展上,因为它易于编写,效果也很明显(能够立即让你满意)。
在动手编码之前,有一些便于编码和调试的小技巧:当explorer调用shell扩展(由用户触发)后,shell扩展暂时驻于内存中;此时,你无法重新编译此扩展的dll文件。为让explorer更迅速卸载扩展,可以在注册表中创建下面的键:
<code>hkey_local_machine\software\microsoft\windows\currentversion\explorer\alwaysunloaddll</code>
并设置其默认值为“1”。在win9x平台上,这是最好的办法。在winnt上,可以在下面的键
<code>hkey_current_user\software\microsoft\windows\currentversion\explorer</code>
创建一个dword 值<code>desktopprocess</code>,也设置它的值为1。(译者:如下图,win9x系统太少见了)这使得“桌面”和“任务栏”运行于一个进程,其他的explorer窗口运行在其独立进程。这意味着,你可以调试单个explorer窗口,当你关闭该窗口时,相关的扩展dll就会被自动卸载,这样就避免了dll文件正被windows使用而无法替换的问题。要使注册表修改生效,需要注销后重新登录。
稍后,我将说明win9x下如何进行调试。
使用appwizard开始
我们先做一个简单的扩展,它仅仅弹出一个消息框以表明工作正常。我们把它关联到文本文件,这样,当我们在一个文本文件上右击时,该扩展就会被调用。
好了,让我们开始吧!什么?我还没有告诉你如何使用那些神秘的shell扩展接口?别着急,我会边进行边解释。我觉得,给出一个概念,紧跟着一个示例代码,这样做有助于理解。当然,我也可以先解释所有的概念,然后列出示例代码,不过这样很难吸引注意力。不管怎样,开启你的vc,我们要开始了。
运行appwizard,生成一个名为“simpleext”的atl com工程:
现在,我们有了一个空的atl项目,它可以编译生成一个dll,但我们还需要添加shell扩展com对象。在类视图中,右击“simpleext”项,选择“新建atl 对象” (vc7,选择“添加”→“添加类”,下图。本文的环境是windows xp+vc7.1,因此附图都是vc7的)。
在atl对象向导中,第一页已经选择了“简单对象”,点击“下一步”。
在第二页,在“简称”编辑框中输入“simpleshlext”(其他编辑框会自动完成):
在默认情况下,向导将创建以c和脚本客户端为基础的ole自动化兼容的com对象。我们的扩展仅仅由explorer调用,因此我们去掉自动化支持。在“属性”页,选择“接口”类型为“自定义”,并且选择“聚合”为“否”:
点击“完成”,就创建了<code>csimpleshlext</code>类,它包含了实现com对象的最基本代码。我们将向这个类加入代码。
初始化接口
当我们的shell扩展被加载时,explorer将调用<code>queryinterface()</code>函数,以取得<code>ishellextinit</code>接口指针。该接口仅有一个方法<code>initialize()</code>,其函数原型如下:
hresult ishellextinit::initialize (
lpcitemidlist pidlfolder,
lpdataobject pdataobj,
hkey hprogid )
explorer通过该方法向我们传递各种各样的信息。<code>pidlfolder</code>是用户所操作文件所在的文件夹的pidl(pidl [pointer to an id list],指向id列表的指针,是一个数据结构,它唯一标识了在shell空间的任何对象,这个对象是或者不是文件系统的对象。) <code>pdataobj</code>是一个<code>idataobj</code>接口变量,通过它可以取得用户正操作的文件名。<code>hprogid</code>是一个<code>hkey</code>接口变量,通过它可以取得扩展dll的注册信息。在本例中,仅仅需要<code>pdataobj</code>参数。
要添加这一方法到我们的com对象,打开文件simpleshlext,加入下列粗体的行。appwizard生成了一些不必需的com关系代码,既然我们不实现我们自己的接口,所以我指出这些失败的能被移除的代码(带删除线的那些):
<code> </code>#include <shlobj.h>
<code> </code>#include <comdef.h>
<code> </code>class atl_no_vtable csimpleshlext :
<code> </code>public ccomobjectrootex<ccomsinglethreadmodel>,
<code> </code>public ccomcoclass<csimpleshlext, &clsid_simpleshlext>,
<code> </code>public isimpleshlext,
<code> </code>public ishellextinit
<code> </code>{
<code> </code>begin_com_map(csimpleshlext)
<code> </code>com_interface_entry(isimpleshlext)
<code> </code>com_interface_entry(ishellextinit)
<code> </code>end_com_map()
<code>com_map</code>是atl实现<code>queryinterface</code>的宏,它告诉atl其它程序能从com对象取得哪些接口。
在类声明中,加入<code>initialize</code>函数。此外,还需要一个变量来保存文件名:
protected:
tchar m_szfile[max_path];
public:
// ishellextinit
stdmethodimp initialize(lpcitemidlist, lpdataobject, hkey);
接着,在文件simpleshlext.cpp中,添加<code>initialize</code>的实现代码:
stdmethodimp csimpleshlext::initialize (
lpcitemidlist pidlfolder,
lpdataobject pdataobj,
hkey hprogid)
我们要做的是取得右键单击选中的文件名,并把它显示在消息框中。如果选中了多个文件,可以通过<code>pdataobj</code>接口指针来访问它们,不过为了保持例子的简单,我们只获取第一个文件名。
文件名的格式和拖曳文件到<code>ws_ex_acceptfiles</code>风格的窗口是的文件名格式一致,这样说来,我们可以通过同样的api:<code>dragqueryfile</code>来取得文件名。我们先取得包含在<code>idataobject</code>中的数据句柄:
void csimpleshlext::initialize (lpcitemidlist pidlfolder, lpdataobject pdataobj, hkey hprogid)
{
formatetc fmt = { cf_hdrop, null, dvaspect_content, -1, tymed_hglobal };
stgmedium stg = { tymed_hglobal };
hdrop hdrop;
// 在数据对象内查找cf_hdrop类型数据。
// 如果没有数据,返回一个错误(“无效参数”)给explorer。
if ( failed( pdataobj->getdata ( &fmt, &stg ) ))
return e_invalidarg;
// 取得指向实际数据的指针。
hdrop = (hdrop) globallock ( stg.hglobal );
// 确保非null
if ( null == hdrop )
注意,错误检查是极其重要的,尤其是对指针的检查。因为我们的扩展运行于explorer进程空间,如果我们的程序挂了,explorer会跟着挂。在win9x系统上,这样的崩溃可能导致需要重启系统。
现在,我们有了<code>hdrop</code>句柄,可以取得所需的文件名了:
// 有效性检查,至少有一个文件名
uint unumfiles = dragqueryfile ( hdrop, 0xffffffff, null, 0 );
hresult hr = s_ok;
if ( 0 == unumfiles )
{
globalunlock ( stg.hglobal );
releasestgmedium ( &stg );
}
// 取得第一个文件名,保存到 m_szfile
if ( 0 == dragqueryfile ( hdrop, 0, m_szfile, max_path ) )
hr = e_invalidarg;
globalunlock ( stg.hglobal );
releasestgmedium ( &stg );
return hr;
}
如果返回<code>e_invalidarg</code>,explorer不会在右键事件时再调用我们的扩展。如果返回<code>s_ok</code>,explorer将再次调用<code>queryinterface()</code>,以取得另一接口:<code>icontextmenu</code>。
与上下文菜单交互的接口
一旦explorer初始化了我们的扩展,它将调用<code>icontextmenu</code>方法来增加菜单项、提供状态栏帮助(fly-by help),以及响应用户的选择。
添加<code>icontextmenu</code>接口与<code>ishellextinit</code>相类似。打开文件simpleshlext.h,加入下列加粗的行:
class atl_no_vtable csimpleshlext :
<code> </code>public ccomobjectrootex<ccomsinglethreadmodel>,
<code> </code>public ccomcoclass<csimpleshlext, &clsid_simpleshlext>,
<code> </code>public ishellextinit,
public icontextmenu
com_interface_entry(icontextmenu)
接着,添加<code>icontextmenu</code>方法的函数原型:
// icontextmenu
stdmethodimp getcommandstring (uint, uint, uint*, lpstr, uint);
stdmethodimp invokecommand (lpcminvokecommandinfo);
stdmethodimp querycontextmenu (hmenu, uint, uint, uint, uint);
修改上下文菜单
<code>icontextmenu</code>有三个方法。第一个<code>querycontextmenu()</code>修改上下文菜单。它的原型如下:
hresult icontextmenu::querycontextmenu (
hmenu hmenu,
uint umenuindex,
uint uidfirstcmd,
uint uidlastcmd,
uint uflags );
<code>hmenu</code>是上下文菜单句柄。<code>umenuindex</code>是我们要添加菜单项的开始位置。<code>uidfirstcmd</code> 和<code>uidlastcmd</code>是菜单命令id值范围。<code>uflags</code>表明explorer调用<code>querycontextmenu()</code>的缘由,这个后面还会谈到。
如果函数成功,返回的hresult值就是分配的菜单项命令id的最大差值加1。例如,<code>uidfirstcmd</code>是5,你添加了3个菜单项,它们的命令id分别是5、7、8。那么,返回值应该是make_hresult(severity_success, 0, 8 - 5 + 1)。否则,返回一个ole错误。
我们这里的扩展简单的加入一个菜单项,因此<code>querycontextmenu()</code>函数非常简单:
stdmethodimp 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("简单shell扩展测试") );
return make_hresult ( severity_success, facility_null, 1 );
}
首先,我们检查uflags的值。在msdn内,你能找到所有的标识和它们的解释,但对于上下文菜单扩展而言,仅仅一个值是有意思的:即cmf_defaultonly。该标识告诉shell命名空间扩展保留默认的菜单项;(如果设置了它的话,)shell扩展将不增加任何的菜单项。这也是我们为什么返回0的原因。如果未设置它,我们就可以通过句柄hmenu来修改菜单,并返回1告诉shell增加了一个菜单项。
在状态栏显示提示帮助(fly-by help)
下一个要被调用的<code>icontextmenu</code>方法是<code>getcommandstring()</code>。当用户在explorer窗口中右击文本文件,或者选中文本文件后点击“文件”菜单,鼠标指到我们添加的菜单项时,状态栏将显示提示信息。<code>getcommandstring()</code>函数返回一个字符串供explorer显示。
<code>getcommandstring()</code>原型如下:
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是上述缓冲区的尺寸。返回值是hresult常量,比如说s_ok或e_fail。
<code>getcommandstring()</code>也能用来取得菜单项的“动作”(verb)。动作是与语言无关的串,它标识了能作用于文件对象的动作。关于这点,<code>shellexecute()</code>的文档中有更详细的说明;有关动作的内容最好留待另外的文章(可以就这方面的内容另外一篇文章),这里简要的说,是列在注册表中的动作(比如“打开”和“打印”),或者有上下文菜单扩展动态创建的动作。这可以通过<code>shellexecute()</code>来调用shell扩展的动作。
总之,我7li8li的说了这么多,就是为了解释清楚<code>getcommandstring()</code>的作用。如果explorer要提示信息,我们就给它;如果explorer请求一个动作,就忽视它。这就是uflags的作用。如果uflags设置了<code>gcs_helptext</code>位,explorer请求提示信息。另外,如果uflags设置了<code>gcs_unicode</code>,我们必须给它一个unicode串。
本例中<code>getcommandstring()</code>如下:
#include <atlconv.h> // atl串转换宏
stdmethodimp csimpleshlext::getcommandstring (
uint idcmd, uint uflags, uint* pwreserved, lpstr pszname, uint cchmax )
uses_conversion;
// 由于这里只有一个菜单项,所以idcmd 必须为0
if ( 0 != idcmd )
// 如果explorer请求提示信息,拷贝串到提供的缓冲区
if ( uflags & gcs_helptext )
lpctstr sztext = _t("简单的sheel扩展帮助(fly-by help)");
if ( uflags & gcs_unicode )
{
// 这里,需要把 pszname 转换为 unicode
lstrcpynw ( (lpwstr) pszname, t2cw(sztext), cchmax );
}
else
// ansi版本
lstrcpyna ( pszname, t2ca(sztext), cchmax );
return s_ok;
return e_invalidarg;
一个需要注意的重要事项是,<code>lstrcpyn()</code>函数保证字符串是以null结束的。这和crt(c运行时)函数<code>strncpy()</code>不同,后者在源串的长度大于等于cchmax时并不在串最后插入null。我建议总是使用<code>lstrcpyn()</code>,这样就无需在调用<code>strncpy()</code>后总是检查以确保串是否以null结束。
执行用户的选择
<code>icontextmenu</code>接口最后的方法是<code>invokecommand()</code>。此方法在用户点击我们增加的菜单项后被调用,其函数原型如下:
hresult icontextmenu::invokecommand ( lpcminvokecommandinfo pcmdinfo );
结构cminvokecommandinfo有9个成员,就我们的目的而言,仅仅需要关注<code>lpverb</code>和<code>hwnds</code>。<code>lpverb</code>有两个用途:它既可以是被引发的动作名,也可以是被点击的菜单向索引。<code>hwnds</code>是用户引发我们的扩展时所在的explorer窗口句柄;我们可以将其作为我们用来显示信息的窗口的父窗口。
由于我们只有一个菜单项,所以只需要检查<code>lpverb</code>:如果它为0,那么我们的菜单项就被选中了。最简单的事情是弹出消息框,这里的代码也就能干这事儿。消息框显示了被选中的文件名,这表明代码工作正常。
stdmethodimp csimpleshlext::invokecommand ( lpcminvokecommandinfo pcmdinfo )
// 如果 lpverb 指向一个实际串,忽略此次调用并退出
if ( 0 != hiword( pcmdinfo->lpverb ) )
// 取得命令索引,这里,唯一有效的值为0
switch ( loword( pcmdinfo->lpverb) )
case 0:
tchar szmsg [max_path + 32];
wsprintf ( szmsg, _t("被选中的文件:\n\n%s"), m_szfile );
messagebox ( pcmdinfo->hwnd, szmsg, _t("simpleshlext"),
mb_iconinformation );
return s_ok;
break;
default:
其它代码细节
这里,集中说明如何移除appwizard生成的多余的ole自动化特性方面的代码。首先,可以移除simpleshlext.rgs(这个文件的用途在下一节详述)中些注册表入口:
hkcr
{
simpleext.simpleshlext.1 = s 'simpleshlext class'
clsid = s '{1ce1ebeb-1254-4880-b807-809cc31e8d2c}'
simpleext.simpleshlext = s 'simpleshlext class'
curver = s 'simpleext.simpleshlext.1'
noremove clsid
forceremove {1ce1ebeb-1254-4880-b807-809cc31e8d2c} = s 'simpleshlext class'
progid = s 'simpleext.simpleshlext.1'
versionindependentprogid = s 'simpleext.simpleshlext'
inprocserver32 = s '%module%'
val threadingmodel = s 'apartment'
val appid = s '%appid%'
'typelib' = s '{172391d4-b01e-4ef5-ac3e-34c99889d8b0}'
}
我们也能移除dll资源中的类型库。(vc7)在“资源视图”中,选中“simpleext.rc”,右击,选中“资源包括”:
在“资源包括”对话框的“编译时指令”中有一行类型库包括:
移除这行,vc弹出警告,点击“确定”:
移除类型库后,我们还需要修改两处代码,以告诉atl,它不应通过类型库来处理。在文件simpleext.cpp中的<code>dllregisterserver()</code>/<code>dllunregisterserver()</code>函数,设置<code>registerserver()</code>/<code>unregisterserver()</code>的参数为。
stdapi dllregisterserver (void)
// ...
return _module.registerserver(true false);
stdapi dllunregisterserver (void)
return _module.unregisterserver(true false);
注册shell扩展
现在,我们实现所有的com接口。不过,怎么才能让explorer使用我们的扩展呢?atl自动生成注册comd服务器dll 的代码,但那是给其它程序使用。为了让explorer知道扩展存在,需要在文本文件的下述注册表键下注册我们的扩展:
<code>hkey_classes_root\txtfile</code>
在这个注册表键下,名为<code>shellex</code>的键保存了有关文本文件的shell扩展列表;在它的下一级,名为<code>contextmenuhandlers</code>的键保存了上下文菜单扩展列表。每个扩展都拥有一个<code>contextmenuhandlers</code>的子键,其默认值为shell扩展的guid。本文简单扩展的示例,将创建如下子键:
<code>hkey_classes_root\txtfile\shellex\contextmenuhandlers\simpleshlext</code>
并设置其默认值为扩展com的guid,如“{1ce1ebeb-1254-4880-b807-809cc31e8d2c }”。
不必编写代码来完成com的注册。在“解决方案管理器”中有文件simpleshlext.rgs;这是一个文本文件,它被atl解析,指导atl在该服务器注册时添加哪些键,在卸载时删除哪些键。下面是注册该扩展所要添加的注册表键:
noremove txtfile
noremove shellex
noremove contextmenuhandlers
forceremove simpleshlext = s '{1ce1ebeb-1254-4880-b807-809cc31e8d2c}'
每一行都代表一个注册键名。“hkcr”是<code>hkey_classes_root</code>的缩写。关键字<code>noremove</code>表明这个键在服务器卸载时不用删除。关键字<code>forceremove</code>表明在写入新键之前,如果该键存在,那么就要先删除它;这行剩下的部分指定了一个字符串(这就是“s”的意思),它是<code>simpleshlext</code>的默认值。
这里,我插入几句。我们的扩展注册在<code>hkey_classes_root\txtfile</code>下;然而,“<code>txtfile</code>”并不是持久的或者预先定好的名字。如果你查看一下<code>hkey_classes_root\.txt</code>,它的默认值就是“<code>txtfile</code>”。这样,有两个副作用:
1、 既然“<code>txtfile</code>”可能不是正确的键名,因此rgs脚本并不可靠。
2、 某些文件编辑软件可能在安装到系统时,就把它自身关联到文本文件。如果它改变了<code>hkey_classes_root\.txt</code>的默认值,那么所有的shell扩展就不能用了。
在我看来,这确实是一个设计上的漏洞。microsoft可能也这么看,因为最新的扩展,如queryinfo扩展就注册在<code>hkey_classes_root\.txt</code>下。
好了,到此为止。还有一个注册的细节,即在winnt上,为了让非管理员帐号也能使用我们的扩展,得把它放到“approved ”扩展列表中:
<code>hkey_local_machine\software\microsoft\windows\currentversion\shell extensions\approved</code>
在这个键下,创建以扩展的guid为名的串值,其内容任意。代码在<code>dllregisterserver()</code>和<code>dllunregisterserver()</code>中,都是一些简单的注册表访问,我就不罗列了。你可以在示例代码中找到。
调试shell扩展
总有一天,你将写一个不是这么简单的shell扩展;那时,你不得不调试它。打开项目的属性对话框,在“调试”的“命令”编辑框输入“c:\windows\explorer.exe”。如果是winnt系统,设置<code>desktopprocess</code>键(前述),当你按f5时就启动了一个新的explorer窗口。只要是在这个窗口完成所有的工作,那么在关闭这个窗口时,扩展就会被卸载,这样就不影响后面的重建dll。
在win9x上,在运行调试器前,必须关闭shell:点击“开始”,点击“注销”。按下ctlr+alt+shift,并点“取消”。这将关闭explorer,任务栏(桌面)消失了。切换到msvc,按f5开始调试。要中止调试,按下“shift+f5”关闭explorer。完成调试后,可以从“开始”“运行”“explorer.exe”,让其正常启动。
扩展的外观
增加扩展后的上下文菜单项
explorer状态栏提示(fly-by help)
弹出的消息框,显示了所选中的文件名
版权与许可