一、前言
win11更新后微软对右键菜单进行了重新设计,分为了新版右键菜单(简化菜单/快捷菜单)和旧版菜单(win10菜单)。而新版菜单和旧版菜单的设计有巨大差别,无法按照原本旧版菜单的方法(包括注册表直接改写、shellex程序等方式)。
二、简述
win11新版菜单需要两组项目,需要实现IExplorerCommand接口的组件,并将其编写msix的Package.appxmanifest使其注册对应的到右键菜单上并进行打包安装:
- 实现IExplorerCommand接口的右键拓展组件
- 通过VS的 使用 Visual Studio 从源代码打包桌面应用 - MSIX | Microsoft Learn 或 MSIX 打包工具概述 - MSIX | Microsoft Learn 将组件添加入安装资源并改写其中的Package.appxmanifest 文件使其正确被注册(msix的虚拟注册表)并添加到合适的Type中。打包成对应的MSIX安装包进行安装(建议虚拟机/沙盒环境测试,不合格将自动闪退)。
而对于旧版win10菜单,首先实现IContextMenu的右键拓展程序组件,之后可以regsvr32程序直接注册(msix项目不建议)到注册表中的<file_extension> Key (包括HKCR下的‘*’ 和 各后缀名、文件、文件夹及背景等)的指定位置。也可以类似win11,使用Package.appxmanifest 文件FileExplorerClassicContextMenuHandler标签(要求高版本)将组件注册到虚拟注册表中。
两者配合实现新旧菜单同时使用。
开发要求:
对于win11右键菜单:
- 开发环境:推荐使用windows系统环境下使用VisualStudio2022+的IDE
- VisualStudio负载:C++的桌面开发、.NET桌面开发
- 开发项目模板:ATL项目(支持包括IExplorerCommand接口在内的win32程序开发)、windows应用程序打包项目、空项目(用于处理应用程序打包项目的入口程序(可选))
对于win10右键菜单
- 开发环境:推荐使用windows系统环境下使用VisualStudio2017+的IDE
- VisualStudio负载:C++的桌面开发
- 开发项目模板:ATL项目
三、开发指南
1、准备项目
需要创建一个空白解决方案用来收拢子项目,再依次创建ATL项目和windows应用程序打包项目、空项目(如果没有exe程序需求可以直接int main(){return 0;}。
1、空白解决方案
2、创建子项目
3、创建ATL项目
配置新项目,细节可参考 windows ATL项目简介-CSDN博客
4、创建windows应用程序打包项目
配置版本,细节可参考 Windows 10中有什么不同 - Windows UWP applications | Microsoft Learn
2、配置ATL项目
创建ATL项目后,需要创建ATL模块来专门为接口实现注册等:
在添加新项中选择ATL->ATL简单对象,命名后确认下一步,一般无需改动直接下一步到完成,其他细节可参考 windows ATL项目简介-CSDN博客
IContextMenu和IExplorerCommand具体实现后文陈述。
3、配置windows应用程序打包项目
- 添加资源:直接在 解决方案资源管理器内右键->添加->现有项 将上文的dll等资源(成品)添加入项目中。
- 添加入口程序:右击
依赖项项目引用 将exe程序(可以是前面的空项目做的简单程序)引入,必须存在,否则打包程序无法运行。 - 配置Package.appxmanifest 文件,可参考应用包清单 - Windows UWP applications | Microsoft Learn ,此为示例:其中com命名空间用来注册组件本身、FileExplorerContextMenus用来注册到新版快捷菜单、FileExplorerClassicContextMenuHandler来支持旧版菜单(需求版本高,低版本应直接调用regsvr32注册程序),
以下为示例(有瑕疵需根据情况修正)
<?xml version="1.0" encoding="utf-8"?> <Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10" xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4" xmlns:desktop9="http://schemas.microsoft.com/appx/manifest/desktop/windows10/9" xmlns:desktop10="http://schemas.microsoft.com/appx/manifest/desktop/windows10/10" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:uap18="http://schemas.microsoft.com/appx/manifest/uap/windows10/18" IgnorableNamespaces="uap rescap desktop4 desktop9 uap18 com"> <Identity Name="9ae0e8eb-306d-4b54-bb9c-fd987a602a57" Publisher="CN=devptx" Version="1.0.13.0" /> <Properties> <DisplayName>WapProjTemplate1</DisplayName> <PublisherDisplayName>Dallas</PublisherDisplayName> <Logo>Images\StoreLogo.png</Logo> </Properties> <Dependencies> <TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.0.0" MaxVersionTested="10.0.0.0" /> <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.14393.0" MaxVersionTested="10.0.14393.0" /> </Dependencies> <Resources> <Resource Language="x-generate"/> </Resources> <Applications> <Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$" uap18:RuntimeBehavior="win32App"> <uap:VisualElements DisplayName="WapProjTemplate1" Description="WapProjTemplate1" BackgroundColor="transparent" Square150x150Logo="Images\Square150x150Logo.png" Square44x44Logo="Images\Square44x44Logo.png"> <uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" /> <uap:SplashScreen Image="Images\SplashScreen.png" /> </uap:VisualElements> <Extensions> <com:Extension Category="windows.comServer"> <com:ComServer> <com:SurrogateServer DisplayName="DllReg Extensions"> <com:Class Id="9dd00a4a-9701-4c02-813c-2fd842b33587" Path="DllRegExt.dll" ThreadingModel="STA" /> <com:Class Id="6f3d9fe2-7d96-4910-9d34-e7e57378bc64" Path="Sample.dll" ThreadingModel="STA" /> </com:SurrogateServer> </com:ComServer> </com:Extension> <desktop4:Extension Category="windows.fileExplorerContextMenus"> <desktop4:FileExplorerContextMenus> <desktop4:ItemType Type="*"> <desktop4:Verb Id="regs" Clsid="9dd00a4a-9701-4c02-813c-2fd842b33587" /> <desktop4:Verb Id="testATL" Clsid="6f3d9fe2-7d96-4910-9d34-e7e57378bc64" /> </desktop4:ItemType> </desktop4:FileExplorerContextMenus> </desktop4:Extension> <desktop9:Extension Category="windows.fileExplorerClassicContextMenuHandler"> <desktop9:FileExplorerClassicContextMenuHandler> <desktop9:ExtensionHandler Type="*" Clsid="9dd00a4a-9701-4c02-813c-2fd842b33587" /> <!--desktop9:ExtensionHandler Type="*" Clsid="6f3d9fe2-7d96-4910-9d34-e7e57378bc64" /--> </desktop9:FileExplorerClassicContextMenuHandler> </desktop9:Extension> </Extensions> </Application> </Applications> <Capabilities> <rescap:Capability Name="runFullTrust" /> </Capabilities> </Package>四、实现上下文菜单扩展
具体参考案例可参考lan2405/staticcontextmenu
参考代码:ATLSimpleObject.h
// ATLSimpleObject.h: CATLSimpleObject 的声明 #pragma once #include "resource.h" // 主符号 #include <ShObjIdl_core.h> #include <string> #include "Sample_i.h" #if defined(_WIN32_WCE) && !defined(_CE_DCOM) && !defined(_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA) #error "Windows CE 平台(如不提供完全 DCOM 支持的 Windows Mobile 平台)上无法正确支持单线程 COM 对象。定义 _CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA 可强制 ATL 支持创建单线程 COM 对象实现并允许使用其单线程 COM 对象实现。rgs 文件中的线程模型已被设置为“Free”,原因是该模型是非 DCOM Windows CE 平台支持的唯一线程模型。" #endif using namespace ATL; // CATLSimpleObject class ATL_NO_VTABLE CATLSimpleObject : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CATLSimpleObject, &CLSID_ATLSimpleObject>, public IDispatchImpl<IATLSimpleObject, &IID_IATLSimpleObject, &LIBID_SampleLib, /*wMajor =*/ 1, /*wMinor =*/ 0>, public IShellExtInit, public IContextMenu, public IExplorerCommand { public: CATLSimpleObject() { } DECLARE_REGISTRY_RESOURCEID(106) BEGIN_COM_MAP(CATLSimpleObject) COM_INTERFACE_ENTRY(IATLSimpleObject) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY(IShellExtInit) COM_INTERFACE_ENTRY(IContextMenu) COM_INTERFACE_ENTRY(IExplorerCommand) END_COM_MAP() DECLARE_PROTECT_FINAL_CONSTRUCT() HRESULT FinalConstruct() { return S_OK; } void FinalRelease() { } public: // IShellExtInit STDMETHOD(Initialize)(_In_ LPCITEMIDLIST pidlFolder, _In_ IDataObject* pdtobj, _In_ HKEY hkeyProgID) override; // IContextMenu STDMETHOD(QueryContextMenu)(_In_ HMENU hMenu, _In_ UINT indexMenu, _In_ UINT idCmdFirst, _In_ UINT idCmdLast, _In_ UINT uFlags) override; STDMETHOD(InvokeCommand)(_In_ LPCMINVOKECOMMANDINFO pici) override; STDMETHOD(GetCommandString)(_In_ UINT_PTR idCmd, _In_ UINT uFlags, _Out_opt_ UINT* pwReserved, _Out_writes_opt_(cchOut) LPSTR pszName, _In_ UINT cchOut) { return S_OK; }; // IExplorerCommand STDMETHOD(EnumSubCommands)(_Out_ IEnumExplorerCommand** ppEnum) override; STDMETHOD(GetCanonicalName)(_Out_ GUID* pguidCommandName) override; STDMETHOD(GetFlags)(_Out_ EXPCMDFLAGS* pFlags) override; STDMETHOD(GetIcon)(_In_ IShellItemArray* psiItemArray, _Out_ LPWSTR* ppszIcon) override; STDMETHOD(GetState)(_In_ IShellItemArray* psiItemArray, _In_ BOOL fOkToBeSlow, _Out_ EXPCMDSTATE* pCmdState) override; STDMETHOD(GetTitle)(_In_ IShellItemArray* psiItemArray, _Out_ LPWSTR* ppszName) override; STDMETHOD(GetToolTip)(_In_ IShellItemArray* psiItemArray, _Out_ LPWSTR* ppszInfotip) override; STDMETHOD(Invoke)(_In_ IShellItemArray* psiItemArray, _Out_ IBindCtx* pbc) override; }; OBJECT_ENTRY_AUTO(__uuidof(ATLSimpleObject), CATLSimpleObject)ATLSimpleObject.cpp
// ATLSimpleObject.cpp: CATLSimpleObject 的实现 #include "pch.h" #include "ATLSimpleObject.h" // CATLSimpleObject // IShellExtInit STDMETHODIMP_(HRESULT __stdcall) CATLSimpleObject::Initialize(LPCITEMIDLIST pidlFolder, IDataObject* pdtobj, HKEY hkeyProgID) { return S_OK; } // IContextMenu STDMETHODIMP_(HRESULT __stdcall) CATLSimpleObject::QueryContextMenu(HMENU hMenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags) { HMENU uhmenu = CreatePopupMenu(); MENUITEMINFO hmenu_lpmi{ sizeof(MENUITEMINFO),MIIM_TYPE | MIIM_STATE | MIIM_SUBMENU,MFT_STRING,MFS_ENABLED }; hmenu_lpmi.cch = idCmdFirst; hmenu_lpmi.hSubMenu = uhmenu; hmenu_lpmi.dwTypeData = _T("注册dlltest"); MENUITEMINFO uhmenu_lpmi0{ sizeof(MENUITEMINFO),MIIM_TYPE | MIIM_STATE | MIIM_ID ,MFT_STRING,MFS_ENABLED ,idCmdFirst + 1 }; MENUITEMINFO uhmenu_lpmi1{ sizeof(MENUITEMINFO),MIIM_TYPE | MIIM_STATE | MIIM_ID ,MFT_STRING,MFS_ENABLED, idCmdFirst + 2 }; uhmenu_lpmi0.dwTypeData = _T("注册test"); uhmenu_lpmi1.dwTypeData = _T("注销test"); InsertMenuItem(uhmenu, idCmdFirst + 1, TRUE, (LPCMENUITEMINFO)&uhmenu_lpmi0); InsertMenuItem(uhmenu, idCmdFirst + 2, TRUE, (LPCMENUITEMINFO)&uhmenu_lpmi1); InsertMenuItem(hMenu, indexMenu, TRUE, (LPCMENUITEMINFO)&hmenu_lpmi); return MAKE_HRESULT(SEVERITY_SUCCESS, 0, 2); } STDMETHODIMP_(HRESULT __stdcall) CATLSimpleObject::InvokeCommand(LPCMINVOKECOMMANDINFO pici) { return E_NOTIMPL; } //STDMETHODIMP_(HRESULT __stdcall) CATLSimpleObject::GetCommandString(UINT_PTR idCmd, UINT uFlags, UINT* pwReserved, LPSTR pszName, UINT cchOut) //{ // return E_NOTIMPL; //} // IExplorerCommand STDMETHODIMP_(HRESULT __stdcall) CATLSimpleObject::EnumSubCommands(_Out_ IEnumExplorerCommand** ppEnum) { *ppEnum = nullptr; return E_NOTIMPL; } STDMETHODIMP_(HRESULT __stdcall) CATLSimpleObject::GetCanonicalName(_Out_ GUID* pguidCommandName) { *pguidCommandName = GetObjectCLSID(); return S_OK; } STDMETHODIMP_(HRESULT __stdcall) CATLSimpleObject::GetFlags(_Out_ EXPCMDFLAGS* pFlags) { *pFlags = ECF_DEFAULT; return S_OK; } STDMETHODIMP_(HRESULT __stdcall) CATLSimpleObject::GetIcon(IShellItemArray* psiItemArray, _Out_ LPWSTR* ppszIcon) { return E_NOTIMPL; } STDMETHODIMP_(HRESULT __stdcall) CATLSimpleObject::GetState(IShellItemArray* psiItemArray, BOOL fOkToBeSlow, _Out_ EXPCMDSTATE* pCmdState) { *pCmdState = ECS_ENABLED; return S_OK; } STDMETHODIMP_(HRESULT __stdcall) CATLSimpleObject::GetTitle(IShellItemArray* psiItemArray, _Out_ LPWSTR* ppszName) { std::wstring titlename =_T( "test"); return SHStrDup(titlename.c_str(), ppszName); } STDMETHODIMP_(HRESULT __stdcall) CATLSimpleObject::GetToolTip(IShellItemArray* psiItemArray, _Out_ LPWSTR* ppszInfotip) { *ppszInfotip = nullptr; return E_NOTIMPL; } STDMETHODIMP_(HRESULT __stdcall) CATLSimpleObject::Invoke(IShellItemArray* psiItemArray, _Out_ IBindCtx* pbc) { return E_NOTIMPL; }多级菜单
与Package.appxmanifest 相关:
<com:Extension Category="windows.comServer"> <com:ComServer> <com:SurrogateServer DisplayName="DllReg Extensions"> <com:Class Id="9dd00a4a-9701-4c02-813c-2fd842b33587" Path="DllRegExt.dll" ThreadingModel="STA" /> <com:Class Id="6f3d9fe2-7d96-4910-9d34-e7e57378bc64" Path="Sample.dll" ThreadingModel="STA" /> </com:SurrogateServer> </com:ComServer> </com:Extension> <desktop4:Extension Category="windows.fileExplorerContextMenus"> <desktop4:FileExplorerContextMenus> <desktop4:ItemType Type="*"> <desktop4:Verb Id="regs" Clsid="9dd00a4a-9701-4c02-813c-2fd842b33587" /> <desktop4:Verb Id="testATL" Clsid="6f3d9fe2-7d96-4910-9d34-e7e57378bc64" /> </desktop4:ItemType> </desktop4:FileExplorerContextMenus> </desktop4:Extension>代表id为regs和testATL(与菜单显示文字无关,实际内容由其IExplorerCommand决定)两个子菜单并存:
生成MSIX程序包
右键打包项目->发布->创建应用程序包,配置后即可创建
ps.为了能够在其他环境中能正常测试,建议直接使用个人测试证书(可直接生成),微软商店发布不需要证书,此外渠道自主发布需要签名证书,如阿里云EV证书(1000+r)
六、测试
生成后在项目文件夹的\WapProjTemplate1\AppPackages存储生成的安装包, 执行Add-AppDevPackage.ps1脚本按照测试证书安装(自动安装测试证书)。
注意事项
1、 使用个人测试证书需要主动开启开发人员模式
2、报错cannot be loaded because running scripts is disabled on this system.:
Windows PowerShell 默认的安全策略禁止运行脚本,需要执行Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
参考资料
- 微软文档-快捷菜单(上下文)菜单和快捷菜单处理程序 - Win32 apps | Microsoft Learn
- 微软文档-Windows应用程序开发 - 最佳做法 - Windows apps | Microsoft Learn
- 微软文档-[应用程序打包项目](使用 Visual Studio 从源代码打包桌面应用 - MSIX | Microsoft Learn)
- 微软文档-[MSIX Packaging Tool](MSIX 打包工具概述 - MSIX | Microsoft Learn)
- 微软文档-Windows 10中有什么不同 - Windows UWP applications | Microsoft Learn
- 微软文档-IExplorerCommand (shobjidl_core.h) - Win32 apps | Microsoft Learn
- 微软文档-打包应用 - UWP applications | Microsoft Learn
- 微软文档-desktop4:FileExplorerContextMenus - Windows UWP applications | Microsoft Learn
- 微软文档-应用包清单 - Windows UWP applications | Microsoft Learn ,
- CSDN-注册表常见位置以及COM注册表项架构详解_右键菜单注册表-CSDN博客
- CSDN-windows ATL项目简介-CSDN博客