我们在使用各种软件时,常常可以发现,许多软件在资源管理器的右键菜单中增加了自己的功能菜单,比如WinRAR、WinZIP、7Z等压缩软件等。本文将介绍如何编程实现增加自己的右键菜单。
阅读本文要求读者熟悉VC,了解COM,同时由于示例代码是用ATL编写,因此读者需要对ATL有一定了解。本文示例是以VC6为开发环境,但所有代码在VC6、VC2005、VC2008下均可编译通过。
本文将一步一步的介绍如何编写一个简单的右键菜单示例,完成此示例后,右键点击文本文件,将弹出我们的右键菜单,选择此菜单将弹出对话框显示所选择的文件名称。
右键菜单扩展一般可实现为一个COM组件,Windows将载入这些COM组件并调用一些特定的接口。因此,我们需要创建一个COM组件。
首先,使用VC的AppWizard新建一个ATL COM项目,项目名称为“SimpleShlExt”,项目设置就使用默认设置。
完成后,向此项目加入一个新的ATL对象,对象类型为“Simple Object”
输入对象名称为“ShlExtObj”
Shell扩展对象不需要实现双接口,不需要实现聚集,因此可按下图设置:
确定后,类向导生成一个名称为“CShlExtObj”的类,其中包含一些用于实现COM组件的基本代码。
当Windows加载我们的Shell扩展时,Explorer.exe进程将首先通过QueryInterface()方法获取指向IShellExtInit接口的指针。IShellExtInit接口只有一个方法,名称为Initialize(),其原型为:
HRESULT IShellExtInit::Initialize (
LPCITEMIDLIST pidlFolder,
LPDATAOBJECT pDataObj,
HKEY hProgID
)
在explorer.exe进程调用此方法时,pidlFolder是PIDL指针(PIDL是一个用于唯一标示Shell中的对象的数据结构),pDataObj是指向IDataObject接口的指针,通过此指针,我们可以获取右键点击时所选中的文件及目录的名称,hProgID是HKEY句柄,通过它我们可以访问保护我们的扩展DLL的注册表信息。
在本例中,我们只需要使用pDataObj即可。
现在,我们需要让我们的CShlExtObj对象实现IShellExtInit接口。首先,打开ShlExtObj.h文件,键入标为粗体的代码以实现IShellExtInit接口,因为此COM组件不需要实现自己的接口,因此可以删除标注有删除线的代码
class ATL_NO_VTABLE CShlExtObj :
public CComObjectRootEx,
public CComCoClass,
public IShlExtObj
public IShellExtInit
{
public:
CShlExtObj()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_SHLEXTOBJ)
DECLARE_NOT_AGGREGATABLE(CShlExtObj)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CShlExtObj)
COM_INTERFACE_ENTRY(IShlExtObj)
COM_INTERFACE_ENTRY(IShellExtInit)
END_COM_MAP()
// IShlExtObj
public:
};
现在,需要在ShlExtObj.h中增加对IShellExtInit::Initialize()方法的声明,此外还需要声明一个变量以保存文件名
private:
TCHAR m_szFilename[MAX_PATH];
public:
STDMETHODIMP Initialize(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
在ShlExtObj.cpp中实现IShellExtInit::Initialize()方法如下:
HRESULT CShlExtObj::Initialize(LPCITEMIDLIST pidlFolder,
LPDATAOBJECT pDataObj,
HKEY hProgID)
{
FORMATETC fmt={CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
STGMEDIUM stg={TYMED_HGLOBAL};
HDROP hDrop=NULL;
// 从IDataObject接口中获取CF_HDROP数据
if (FAILED(pDataObj->GetData(&fmt, &stg)))
return E_INVALIDARG;
// 获取指向实际数据的指针
hDrop=(HDROP)GlobalLock(stg.hGlobal);
if (hDrop==NULL)
{
ReleaseStgMedium(&stg);
return E_INVALIDARG;
}
// 查询选择的文件数量
UINT uFilesCount=DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0);
HRESULT hr=S_OK;
if (0==uFilesCount)
{
GlobalUnlock(stg.hGlobal);
ReleaseStgMedium(&stg);
return E_INVALIDARG;
}
// 将文件名称保存在变量中
if (0==DragQueryFile(hDrop, 0, m_szFilename, MAX_PATH))
hr=E_INVALIDARG;
GlobalUnlock(stg.hGlobal);
ReleaseStgMedium(&stg);
return S_OK;
}
如果IShellExtInit::Initialize()方法返回E_INVALIDARG,则Windows将不再为此次右键单击继续调用此扩展DLL,如果返回S_OK,则Windows将调用QueryInterface(),获取指向IContextMenu接口的指针。
为了使用IShellExtInit接口,我们还需要打开stdafx.h文件,添加如下一行代码
#include
#include
在Windows调用IShellExtInit::Initialize()后,就会调用IContextMenu接口上的方法,通过这些方法,我们可以在右键菜单中添加自己的菜单项,并相应菜单点击命令。
首先在ShlExtObj.h中添加实现IContextMenu接口的代码,如下粗体所示:
class ATL_NO_VTABLE CShlExtObj :
public CComObjectRootEx,
public CComCoClass,
public IShellExtInit,
public IContextMenu
{
// 省略了原有的代码......
BEGIN_COM_MAP(CShlExtObj)
COM_INTERFACE_ENTRY(IShellExtInit)
COM_INTERFACE_ENTRY(IContextMenu)
END_COM_MAP()
public:
// IContextMenu
STDMETHODIMP GetCommandString(UINT, UINT, UINT*, LPSTR, UINT);
STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO);
STDMETHODIMP QueryContextMenu(HMENU, UINT, UINT, UINT, UINT);
};
IContextMenu接口有三个方法,QueryContextMenu()让我们可以向右键菜单中添加菜单项,GetCommandString()方法用于返回菜单项的提示信息,InvokeCommand()方法在用户选择了我们添加的菜单项时被调用。
首先看一下实现QueryContextMenu()方法的代码:
HRESULT CShlExtObj::QueryContextMenu (HMENU hmenu,
UINT uMenuIndex,
UINT uidFirstCmd,
UINT uidLastCmd,
UINT uFlags )
{
if (uFlags & CMF_DEFAULTONLY)
return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);
InsertMenu (hmenu,
uMenuIndex,
MF_BYPOSITION,
uidFirstCmd,
_T("显示文件名称"));
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 1 );
}
在这段代码中,首先我们需要检查uFlags标志是否包含CMF_DEFAULTONLY,如包含则直接返回成功。随后我们添加了自己的菜单项,然后返回成功。注意此方法的返回值使用了MAKE_HRESULT宏,该宏的第一个参数表示成功或失败,如失败,则第二个参数表示错误类别,如成功,则第三个参数为我们所添加的菜单项的ID减去uidFirstCmd加1,简单的说就是我们占据的菜单项的个数。比如,uidFirstCmd为5,我们插入的菜单项的ID为5,6,8,则MAKE_HRESULT宏的第三个参数应为8-5+1=4,因为我们的菜单项实际占据的ID个数为4个——包括5,6,7,8——其中7虽然没有使用,但由于我们使用了8,则7也无法被其他扩展菜单项使用了。
GetCommandString()方法用于返回菜单项的提示信息,在此例中笔者直接返回E_NOTIMPL,表示没有提示信息。
STDMETHODIMP CShlExtObj::GetCommandString (UINT idCmd,
UINT uFlags,
UINT* pwReserved,
LPSTR pszName,
UINT cchMax )
{
return E_NOTIMPL;
}
InvokeCommand()方法表示用户选择了我们添加的菜单项。在此例中,我们将弹出一个对话框显示所选择的文件名称。
HRESULT CShlExtObj::InvokeCommand (LPCMINVOKECOMMANDINFO pCmdInfo )
{
if (0!=HIWORD(pCmdInfo->lpVerb))
return E_INVALIDARG;
switch (LOWORD(pCmdInfo->lpVerb))
{
case 0:
MessageBox (pCmdInfo->hwnd, m_szFilename, _T("文件名"), MB_ICONINFORMATION );
break;
default:
return E_INVALIDARG;
}
return S_OK;
}
由于Shell的扩展DLL COM组件不需要完整的COM注册信息,我们需要打开ShlExtObj.rgs,按如下修改
HKCR
{
SimpleShlExt.ShlExtObj.1 = s 'ShlExtObj Class'
{
CLSID = s '{BEB94AA2-06D6-4674-A5F9-635A202947BD}'
}
SimpleShlExt.ShlExtObj = s 'ShlExtObj Class'
{
CLSID = s '{BEB94AA2-06D6-4674-A5F9-635A202947BD}'
CurVer = s 'SimpleShlExt.ShlExtObj.1'
}
NoRemove CLSID
{
ForceRemove {BEB94AA2-06D6-4674-A5F9-635A202947BD} = s 'ShlExtObj Class'
{
ProgID = s 'SimpleShlExt.ShlExtObj.1'
VersionIndependentProgID = s 'SimpleShlExt.ShlExtObj'
InprocServer32 = s '%MODULE%'
{
val ThreadingModel = s 'Apartment'
}
'TypeLib' = s '{6BEB8C88-9F1E-4F7C-ACF3-84962AFD5DF4}'
}
}
}
然后增加Shell扩展注册信息如下粗体所示:
HKCR
{
NoRemove txtfile
{
NoRemove shellex
{
NoRemove ContextMenuHandlers
{
ForceRemove SimpleShlExt = s '{BEB94AA2-06D6-4674-A5F9-635A202947BD}'
}
}
}
}
HKLM
{
NoRemove Software
{
NoRemove Microsoft
{
NoRemove Windows
{
NoRemove CurrentVersion
{
NoRemove 'Shell Extensions'
{
NoRemove Approved
{
val {BEB94AA2-06D6-4674-A5F9-635A202947BD} = s 'ShlExtObj'
}
}
}
}
}
}
}
由于此COM组件不需要在脚本或VB中调用,因此我们可以将类型库删除。点击【View】下的【Resource Includes...】菜单,在对话框中删除“1 TYPELIB "SimpleShlExt.tlb"”,如图
如VC显示警告信息,点击OK即可。
删除类型库后,还需修改DllRegisterServer()和DllUnregisterServer,如下:
STDAPI DllRegisterServer(void)
{
return _Module.RegisterServer(TRUE FALSE);
}
STDAPI DllUnregisterServer(void)
{
return _Module.UnregisterServer(TRUE FALSE);
}
现在,我们可以编译此程序,编译成功后VC将自动注册此COM组件。如一切都没有问题,此时打开资源管理器,在任意后缀名为txt的文本文件上点击右键,就可以看到我们的菜单项“显示文件名称”
点击此菜单,会弹出对话框显示文件名称
试试看,你成功了吗?