右键菜单,即用户右击shell对象时弹出的上下文菜单(context menu)。本文记录了如何创建右键菜单的基本过程,跟着步骤一步一步来,即可创建出一个右键菜单工程。
第一步,新建一个ATL工程
Visual Studio—>新建项目—>ATL—>使用默认配置(一直按下一步即可)。
注:如果生成的项目中多了一个以PS为后缀的子项目,可以选择将之移除,并删除下图标识的无用文件。

第二步,修改工程配置
右键工程属性,分别修改以下几项:
修改下图项支持xp系统

修改下图项避免出现缺失dll错误

修改下图项,避免编译后注册(vs如果是非管理员权限启动,编译完会提示regsvr32错误)

第三步,为项目添加一个简单的ATL对象,继承并实现相应的IContextMenu、IShellExtInit的接口方法。
右键项目—>添加(Add)—>类(Class)—>ATL—>ATL Simple Object—>添入下图标识的地方(其它向导会自动生成)—>点击Finish

打开刚才生成的.h文件,并在类继承后加上
public IShellExtInit,public IContextMenu
添加COM类型映射:在宏BEGIN_COM_MAP与宏BEGIN_COM_END之间的列表下手动加上
COM_INTERFACE_ENTRY(IShellExtInit)
COM_INTERFACE_ENTRY(IContextMenu)
然后覆盖并实现IContextMenu接口(QueryInterface,GetCommandString、InvokeCommand)以及IShellExtInit接口(Initialize)系统对这些接口的调用顺序、时刻为:
1:Initialize(用户右键点击某个Shell程序时)
2:QueryContextMenu(1.返回S_OK或其他表示初始化成功的HRESULT时。插入自定义菜单的入口。)
3:GetCommandString(用户光标盘旋(hover)在插入的菜单项时,系统status bar将显示的信息。Vista以后的系统不再有作用,不是实现的重点。)
4:InvokeCommand(用户点击新插入的菜单项时,将会调用这个方法。用户点击菜单项回调的入口。)

第四步,插入菜单
HRESULT STDMETHODCALLTYPE QueryContextMenu(_In_ HMENU hmenu, _In_ UINT indexMenu, _In_ UINT idCmdFirst, _In_ UINT idCmdLast, _In_ UINT uFlags) override {
if (CMF_DEFAULTONLY & uFlags) {
return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, USHORT(0));
}
UINT uid = idCmdFirst;
//当你插入菜单的时候,同时递增uid,同时保存uid值到对应菜单的映射。
//递增uid时,也需要判断一下uid<=idCmdLast
HBITMAP hbmpDemo = ::LoadBitmap(g_hInstance, MAKEINTRESOURCE(IDB_SHELL_ICON));
MENUITEMINFO mii;
::memset(&mii, 0, sizeof(MENUITEMINFO));
mii.cbSize = (UINT)sizeof(MENUITEMINFO);
mii.fMask = MIIM_STRING | MIIM_ID | MIIM_FTYPE | MIIM_BITMAP;
mii.fType = MFT_STRING;
mii.dwTypeData = L"Demo";
mii.wID = uid++;//id 为[idCmdFirst,idCmdLast]之间某个值
mii.hbmpItem = hbmpDemo; // xp系统这种方法是有问题的,图标会变色,请看底部解决办法
::InsertMenuItem(hmenu, indexMenu++, TRUE, (LPCMENUITEMINFO)&mii);
//插入菜单完毕
return MAKE_HRESULT(SEVERITY_SUCCESS, 0, uid - idCmdFirst);
}
第五步,实现菜单点击
HRESULT STDMETHODCALLTYPE InvokeCommand(_In_ CMINVOKECOMMANDINFO *pici) override {
//利用pici的lpVerb判断对应哪个自定义菜单项被点击了
WORD menuId = LOWORD(pici->lpVerb);
//一般选择switch case的方法来进行判断
switch (menuId) {
// 做某个菜单被点击的用户逻辑。
// menuId 就是 添加时 mii.wID - idCmdFirst 的值
}
return S_OK;
}
第六步,修改.rgs
修改添加对象时生成的.rgs文件,注意里面的GUID值保持一致
HKCR
{
ContextMenu.1 = s 'context_menu Class'
{
CLSID = s '{6170829F-B85F-4E53-AAFC-74C08A30E572}'
}
ContextMenu = s 'context_menu Class'
{
CurVer = s 'ContextMenu.1'
}
NoRemove CLSID
{
ForceRemove {6170829F-B85F-4E53-AAFC-74C08A30E572} = s 'context_menu Class'
{
ProgID = s 'ContextMenu.1'
VersionIndependentProgID = s 'ContextMenu'
ForceRemove Programmable
InprocServer32 = s '%MODULE%'
{
val ThreadingModel = s 'Apartment'
}
TypeLib = s '{EFD0F068-5260-4F4A-8110-A314FF5BB5FB}'
Version = s '1.0'
}
}
NoRemove *
{
NoRemove ShellEx
{
NoRemove ContextMenuHandlers
{
ForceRemove {6170829F-B85F-4E53-AAFC-74C08A30E572} = s '{6170829F-B85F-4E53-AAFC-74C08A30E572}'
}
}
}
NoRemove lnkfile
{
NoRemove ShellEx
{
NoRemove ContextMenuHandlers
{
ForceRemove {6170829F-B85F-4E53-AAFC-74C08A30E572} = s '{6170829F-B85F-4E53-AAFC-74C08A30E572}'
}
}
}
NoRemove Directory
{
NoRemove ShellEx
{
NoRemove ContextMenuHandlers
{
ForceRemove {6170829F-B85F-4E53-AAFC-74C08A30E572} = s '{6170829F-B85F-4E53-AAFC-74C08A30E572}'
}
}
}
NoRemove Folder
{
NoRemove ShellEx
{
NoRemove ContextMenuHandlers
{
ForceRemove {6170829F-B85F-4E53-AAFC-74C08A30E572} = s '{6170829F-B85F-4E53-AAFC-74C08A30E572}'
}
}
}
NoRemove Drive
{
NoRemove ShellEx
{
NoRemove ContextMenuHandlers
{
ForceRemove {6170829F-B85F-4E53-AAFC-74C08A30E572} = s '{6170829F-B85F-4E53-AAFC-74C08A30E572}'
}
}
}
}
第七步,注册、反注册,测试扩展dll
以管理员权限运行cmd
运行 (regsvr32 dll路径)实现注册
运行 (regsvr32 dll路径 /u)实现反注册
注意64位系统需要注册64位dll扩展


关于右键菜单所选中文件的获取
既然右键菜单是针对文件或文件夹做什么事,那我们肯定需要获取操作的文件。
HRESULT STDMETHODCALLTYPE Initialize(_In_opt_ PCIDLIST_ABSOLUTE pidlFolder, _In_opt_ IDataObject *pdtobj, _In_opt_ HKEY hkeyProgID) override {
FORMATETC fmt = { CF_HDROP,NULL,DVASPECT_CONTENT,-1,TYMED_HGLOBAL };
STGMEDIUM stg = { TYMED_HGLOBAL };
if (FAILED(pdtobj->GetData(&fmt, &stg)))
return E_INVALIDARG;
std::vector<std::wstring> files_;
HDROP hDrop = (HDROP)::GlobalLock(stg.hGlobal);
if (hDrop != nullptr) {
wchar_t szFile[MAX_PATH] = { 0 };
UINT uFileCount = ::DragQueryFile(hDrop, 0xFFFFFFFF, nullptr, 0);
for (UINT i = 0; i < uFileCount; ++i) {
::DragQueryFile(hDrop, i, szFile, MAX_PATH);
files_.push_back(szFile);
}
::GlobalUnlock(stg.hGlobal);
}
::ReleaseStgMedium(&stg);
return files_.size() == 0 ? E_INVALIDARG : S_OK;
}
关于xp图标异常处理
1:图标颜色异常
windows vista及以上系统支持直接设置一个PARGB32格式的 bitmap。但是windows xp 需要使用 HBITMAP_CALLBACK,HBITMAP_CALLBACK 触发 WM_MEARITEM & WM_DRAWITEM,需要实现 IContextMenu3/IContextMenu2 消息 处理以上消息。
插入菜单的代码改为:
MENUITEMINFO mii;
::memset(&mii, 0, sizeof(MENUITEMINFO));
mii.cbSize = (UINT)sizeof(MENUITEMINFO);
mii.fMask = MIIM_STRING | MIIM_ID | MIIM_FTYPE | MIIM_BITMAP;
mii.fType = MFT_STRING;
mii.dwTypeData = L"Demo";
mii.wID = uid++;//id 为[idCmdFirst,idCmdLast]之间某个值
mii.hbmpItem = ::IsWindowsVistaOrGreater() ? hbmpDemo : HBITMAP_CALLBACK;
::InsertMenuItem(hmenu, indexMenu++, TRUE, (LPCMENUITEMINFO)&mii);
继承IContextMenu3/IContextMenu2

实现IContextMenu3/IContextMenu2的方法HandleMenuMsg/HandleMenuMsg2
HRESULT STDMETHODCALLTYPE HandleMenuMsg(
/* [annotation][in] */
_In_ UINT uMsg,
/* [annotation][in] */
_In_ WPARAM wParam,
/* [annotation][in] */
_In_ LPARAM lParam) {
LRESULT result = TRUE;
return HandleMenuMsg2(uMsg, wParam, lParam, &result);
}
HRESULT STDMETHODCALLTYPE HandleMenuMsg2(
/* [annotation][in] */
_In_ UINT uMsg,
/* [annotation][in] */
_In_ WPARAM wParam,
/* [annotation][in] */
_In_ LPARAM lParam,
/* [annotation][out] */
_Out_opt_ LRESULT *plResult) {
switch (uMsg) {
case WM_MEASUREITEM:
{
MEASUREITEMSTRUCT* lpmis = (MEASUREITEMSTRUCT*)lParam;
if (lpmis == nullptr)
break;
lpmis->itemWidth = 16;
lpmis->itemHeight = 16;
*plResult = TRUE;
}
break;
case WM_DRAWITEM:
{
DRAWITEMSTRUCT* lpdis = (DRAWITEMSTRUCT*)lParam;
if ((lpdis == nullptr) || (lpdis->CtlType != ODT_MENU))
return S_OK;
HICON hicon_item = nullptr;// (HICON)::LoadImage(g_hInstance, MAKEINTRESOURCE(IDI_SHELL_ICON), IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR);
if (hicon_item == nullptr)
return S_OK;
::DrawIconEx(lpdis->hDC,
lpdis->rcItem.left,
lpdis->rcItem.top + (lpdis->rcItem.bottom - lpdis->rcItem.top - 16) / 2,
hicon_item, 16, 16,
0, NULL, DI_NORMAL);
*plResult = TRUE;
}
break;
default:
break;
}
return S_OK;
}
2:图标宽度异常
菜单图标要么前面要么后面,留白很严重,图标看起来偏移了很多。
只需在QueryContextMenu添加完菜单后调用TweakMenu函数即可。
void TweakMenu(HMENU hMenu) {
MENUINFO MenuInfo = { 0 };
MenuInfo.cbSize = sizeof(MenuInfo);
MenuInfo.fMask = MIM_STYLE | MIM_APPLYTOSUBMENUS;
MenuInfo.dwStyle = MNS_CHECKORBMP;
::SetMenuInfo(hMenu, &MenuInfo);
}