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);
  }

本文为@艺文笔记原创文章,转载请注明出处:https://www.xuwenyan.com/archives/1692,非常感谢!
已是最前文章 下一篇: