参考资料:
windows右键菜单开发--with vs2008(2005) or vc9(vc8)
最近项目中有利用IContexMenu实现右键菜单的例子,想试验一下,然而参照了很多资料,出现了很多错误,捣鼓了几天,还是没有出现想要的效果。
在这里,把程序的编写过程写下来,希望能得到熟悉Shell的高手指点。
编译工具:VS2005(据传说,各个编译工具的区别主要在于所用的SDK。VS2008用的是SDK3.5,VS2005用的是SDK2.0,两者差别不大。对于Shell编程来讲,VC6就已经足够了。只是VS2005(2008)会与VC6或者VS2003有所区别)。
开发环境:Win7 32位,中文旗舰版
期待功能:在Windows的右键菜单里加入自己的菜单,点击之后能够发出消息,会有消息响应。效果如图:

程序编写:
第一步:打开VS2005,新建ATL工程,命名为MyShell。各选项设置如图:
点击【完成】,完成项目的创建。
可以看到,在解决方案MyShell下,生成了两个工程:MyShell和 MyShellPS。
此时,编译报错。详见错误处理一
第二步:建立接口。
我们的dll是要加载到win外壳程序中的,这是通过注册表来实现的(后面讲具体实现方法),当右键点击弹出菜单前,系统会根据注册表信息判断要去加载哪些dll,当发现我们的dll时,加载,并根据shell的接口来实现我们定义的函数。
shell的接口是固定的,他不会在意你的函数是什么,只要你实现了他的接口,他就会去执行。
右键点击工程MyShell,选择【添加 类】,在向导中选择ATL->ATL简单对象(Simple Objec),点击【添加】。
在简称(Short Name)中写上你的接口名字,写为【MyMenu】,其他选项程序会自动填充。
此时点击【完成】就好。
这时候会看到,类视图区多了2个东西,一个CMyMenu的类,还一个IMyMenu的接口。
(可选)为接口添加方法(Add method)
右键单击IMyMenu,选择 添加方法(Add method),自定一个方法名(函数),比如'MyMenuMethod',参数类型自定义,看你程序需求了。
在方法MyMenuMethod',我只是做了简单的message
STDMETHODIMP CMyMenu::MyMenuMethod(void)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
// TODO: 在此添加实现代码
::MessageBox( NULL, _T("In MyMenuMethod"), _T("MyShell"), MB_OK);
return S_OK;
}
此时,可以编译一下。在不同编译环境下可能会出现不同问题,因此应该养成完成一项修改,编译一下的习惯。
得到出错信息如下:
error PRJ0050: 未能注册输出。请确保您有修改注册表的相应权限。
见错误处理,错误一的处理。
error C2787: 'IContextMenu' : no GUID has been associated with this object
error C2440: 'initializing' : cannot convert from 'DWORD_PTR' to 'const IID *'
error C2440: 'initializing' : cannot convert from 'ATL::_ATL_CREATORARGFUNC (__stdcall *)' to 'DWORD_PTR'
见错误处理,错误二的处理。
第三步:在CMyMenu的基类中添加IShellExtInit、IContextMenu。
要实现shell的所有接口,但ATL为我们自动添加的接口里不包含IShellExtInit、IContextMenu,所以要手动补充上。
<span style="color:#454545;">class ATL_NO_VTABLE CMyMenu :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CMyMenu, &CLSID_MyMenu>,
public IDispatchImpl<IMyMenu, &IID_IMyMenu, &LIBID_MyShellLib, /*wMajor =*/ 1, /*wMinor =*/ 0></span><span style="color:#33cc00;">,
public IShellExtInit,
public IContextMenu</span>
并在宏声明那里添加2个接口
<span style="color:#454545;">BEGIN_COM_MAP(CMyMenu)
COM_INTERFACE_ENTRY(IMyMenu)
COM_INTERFACE_ENTRY(IDispatch)
</span><span style="color:#33cc00;">COM_INTERFACE_ENTRY(IShellExtInit)
COM_INTERFACE_ENTRY(IContextMenu)</span><span style="color:#454545;">
END_COM_MAP()</span>
注意:新添加的两个接口基类,需要头文件支持,我们把这2个头文件加到总声明 stdafx.h 里。
#include <shlobj.h>
#include <shobjidl.h>
在类视图中便会显示出CMyMenu类里的2个新接口。
此时编译会报错,因为接口没有被实现。
错误 1 error C2259: 'ATL::CComObject<Base>' : cannot instantiate abstract class c:\program files\microsoft visual studio 8\vc\atlmfc\include\atlcom.h 1792
第四步: 实现接口
1. IShellExtInit 鼠标选中它,可以看到它里面只有一个纯虚函数
函数原型为:
<pre name="code" class="cpp"> virtual HRESULT STDMETHODCALLTYPE Initialize(
/* [in] */ LPCITEMIDLIST pidlFolder,
/* [in] */ IDataObject *pdtobj,
/* [in] */ HKEY hkeyProgID) = 0;
LPCITEMIDLIST 这个类型名2008和2005是有区别的,2008中会显示PCIDLIST_ABSOLUTE,其实他们是本质是一个东西(#define PCIDLIST_ABSOLUTE LPCITEMIDLIST),他是指向一个ITEMIDLIST的结构体指针。这个结构体标示了右键点击时对应的文件夹。如果属性页扩展则这个值为NULL即可。
pdtobj 指向 IDataObject 接口的指针。
在我们的类中重写这个虚函数,直接把定义复制过来即可,用来做些初始化动作。
MyMenu.h中:
virtual HRESULT STDMETHODCALLTYPE Initialize(
/* [in] */ LPCITEMIDLIST pidlFolder,
/* [in] */ IDataObject *pdtobj,
/* [in] */ HKEY hkeyProgID);
(由于在objbase.h中有如下定义:
define STDMETHOD(method) virtual HRESULT STDMETHODCALLTYPE method
因此上述代码也可写为:
STDMETHOD(Initialize)(
/* [in] */ LPCITEMIDLIST pidlFolder,
/* [in] */ IDataObject *pdtobj,
/* [in] */ HKEY hkeyProgID) = 0;)
)
MyMenu.cpp中:
HRESULT STDMETHODCALLTYPE CMyMenu::Initialize(
/* [in] */ LPCITEMIDLIST pidlFolder,
/* [in] */ IDataObject *pdtobj,
/* [in] */ HKEY hkeyProgID)
{
return S_OK;
}
(由于在objbase.h中存在如下定义,
#define STDMETHODIMP HRESULT STDMETHODCALLTYPE因此上述代码也可写为
STDMETHODIMP CMyMenu::Initialize(LPCITEMIDLIST pidlFolder, IDataObject *pdtobj, HKEY hkeyProgID)
{
return S_OK;
})
2.IContextMenu: 这里有3个接口,方法同上,鼠标左键单击点中,可以查看到。
GetCommandString InvokeCommand QueryContextMenu
直接在把基类中的声明复制粘贴过来,将这3个都实现。暂时都直接返回 return S_OK。
目前为止我们已经重写了4个虚函数(没有其他代码,只简单的return S_OK;),所以框架结构已经完全建立好了。点击 编译,查看下有没有笔误之类的错误。
第四步: 补充代码。
要补充的就是上面4个接口函数而已,在补充之前我们要清楚他们分别是做什么用的。
1.Initialize
顾名思义。。。初始化用的。此文章不做介绍,简单来说在这里可以得到当前文件(夹)路径。具体可以参照msdn。
2.GetCommandString
再次顾名思义下,获得右键菜单命令信息,包括帮助信息,命令的名称(详见MSDN)
STDMETHODIMP CMyMenu::GetCommandString(UINT_PTR idCmd, UINT uType, UINT * pwReserved, LPSTR pszName, UINT cchMax)
{
if ( (idCmd != 102) && (idCmd != 103))
{
return E_INVALIDARG;
}
lstrcpynA(pszName, _T"This is a test info", cchMax);
return S_OK;
}
这个接口没有实际意义,我们甚至可以不深究它。
idCmd是传进来的菜单命令id的偏移量。 这个值是我们自己定义给要添加菜单的值。 102 和 103都是这样,当点击我们添加的菜单后,命令id的偏移量肯定是我们自定义的,后面会介绍,如果不是,将会返回错误,return E_INVALIDARG。如果是,则向参数 pszName 赋值。返回S_OK。
3.QueryContextMenu
想菜单中添加内容,详见msdn。
这个函数是最为关键的函数之一。(其实关键函数有2个,下面那个就是另一个之一)
要增加几个菜单,在什么位置,叫什么名字,带不带图标,点击后命令id等等都是这里完成的。
虽然东西说起来挺多,但是函数确很少。。。。对,就一个函数!
InsertMenu,注意,我们这里并没有用到mfc。
WINUSERAPI
BOOL
WINAPI
InsertMenuW(
__in HMENU hMenu,
__in UINT uPosition,
__in UINT uFlags,
__in UINT_PTR uIDNewItem,
__in_opt LPCWSTR lpNewItem);
STDMETHODIMP CMyMenu::QueryContextMenu(HMENU hmenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags)
{
InsertMenu(hmenu,indexMenu,MF_SEPARATOR|MF_BYPOSITION,0,NULL);//分割线
InsertMenu(hmenu, indexMenu+1, MF_STRING | MF_BYPOSITION, idCmdFirst + 102, _T("XXXX"));
InsertMenu(hmenu, indexMenu+2, MF_STRING | MF_BYPOSITION, idCmdFirst + 103, _T("YYYY"));
InsertMenu(hmenu,indexMenu+3,MF_SEPARATOR|MF_BYPOSITION,0,NULL);//分割线
return MAKE_HRESULT(SEVERITY_SUCCESS, 0, USHORT(103 + 1));
}
InsertMenu 这个函数大家应该不陌生,初学mfc时候应该有学过类似的,关于菜单操作MF_BYPOSITION还是MF_BYCOMMAND,这些mfc中的菜单操作也用到过。idCmdFirst 是系统传进来的id首值,我们自定义一个偏移量,表示我们的菜单id(这里用了102,103),最后将下一个命令id返回给系统,告诉系统后面的菜单的命令从哪里开始偏移。
(可选)在这里我们可以给菜单前面加个图:将图导入资源(bitmap),或是自己画一个,注意大小不要超过13x13
LPTSTR lptStr =MAKEINTRESOURCE( IDB_BITMAP1);HBITMAP hBitmap=LoadBitmapW(ghInstance,lptStr);//gHinstance 是一个HINSTANCE类型的全局变量,在dll入口函数(DllMain)中,将系统传进来的参数hInstance保存。
</pre><pre name="code" class="cpp" style="color: rgb(69, 69, 69); font-size: 14px; line-height: 21px; "><pre name="code" class="cpp">LPTSTR lptStr =MAKEINTRESOURCE( IDB_BITMAP1)
;HBITMAP hBitmap=LoadBitmapW(ghInstance,lptStr);//gHinstance 是一个HINSTANCE类型的全局变量,在dll入口函数(DllMain)中,将系统传进来的参数hInstance保存。
SetMenuItemBitmaps(hmenu,indexMenu+1,MF_BYPOSITION,hBitmap,hBitmap);
4.InvokeCommand
关键函数的另一个之一,定义菜单的消息响应。
请msdn下查下CMINVOKECOMMANDINFOEX里的 lpVerb参数的意思,你会发现他的高位标示了是字符串还是命令id偏移量。我们对字符串不关心,我们关心的是命令id. 这个函数是要实现具体点击菜单后所要实现的功能。所以判断是点了 XXXX(102),还是YYYY(103)菜单呢。
<span style="font-family:tahoma, helvetica, arial;">STDMETHODIMP CMyMenu::InvokeCommand(LPCMINVOKECOMMANDINFO lpici)
{
if(HIWORD(((CMINVOKECOMMANDINFOEX *) lpici)->lpVerbW))
{
return S_OK;
}
else
{
if(LOWORD(lpici->lpVerb)==102)
{
MessageBox(NULL,_T("1111"),_T("MyShell"),MB_OK);
}
if(LOWORD(lpici->lpVerb)==103)
{
MessageBox(NULL,_T("2222"),_T("MyShell"),MB_OK);
}
}
return S_OK;
}</span>
在这里也可以用先前添加的 接口方法,比如在103里注释掉messagebox,换成MenuFuctionb();可以试试。
至今为止,代码部分已经编写完成。编译一下,生成dll。
下面可以看一下怎么注册我们的dll。
注册DLL
截止到目前,我们所有shell扩展接口都实现了,编译(release版),讲dll考入客户机(可以是虚拟机,或者你自己的机器)。
1、首先是注册我们的dll
在cmd里写
Regsvr32 MyShell.dll (这么写的前提是MyShell.dll所在位置是系统盘的system32文件夹下,如果没在这里,你可以把全路径写上)
2、然后是修改注册表
我们想让所有文件夹和文件点击右键时候都看到我们的菜单。
注册表位置:HKEY_CLASSES_ROOT -> * -> shellex -> ContextMenuHandlers添加新键 MyShell (这个名字你随意起)然后设置默认键值为 REG_SZ 类型,数据写 {xxxx.........} 具体数据写什么呢,当然是我们产品的guid,他的位置在我们程序中,新建工程时候vc已经帮我们定义好了,他是一个唯一(概率上来讲)的值。
在解决方案视图中,源文件目录下,找到MyShell. idl文件,打开查看, library 下的 uuid就是我们要的号码,把它复制下来,
我这里是{11B5203F-0C25-47E5-AAAC-9C959E46BCAE},你那里肯定不是这个数,因为他是vc随机生成的,目的就是为了避免产品间重名冲突。所以我们的注册表修改完后应该是这个样子的
她不包括文件夹。所以我们还要修改文件夹的信心。注册表位置:HKEY_CLASSES_ROOT-->Directory --> shellex --> ContextMenuHandlers建立一个 MyShell键,设置值与‘*’相同
目前为止都操作完毕了,对于注册表操作,程序员最好去写一个简单的安装程序,win控制台程序就可以,用我们的函数来进行注册dll和修改注册表。当然想对应的你要写好你的卸载程序,去移除这些注册表。
调试方法,待续……
本文详细介绍了如何使用Visual Studio 2005和IContextMenu接口在Windows系统中开发自定义的右键菜单功能,包括创建ATL项目、实现Shell扩展接口、注册DLL以及修改注册表等步骤。

2108

被折叠的 条评论
为什么被折叠?



