ATL实现windows右键菜单扩展(ContextMenu)
右键菜单,即用户右击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); }