首页
/ PowerToys New+ 双上下文菜单处理器:Win10/Win11 资源管理器 Shell 扩展实现与调试实战

PowerToys New+ 双上下文菜单处理器:Win10/Win11 资源管理器 Shell 扩展实现与调试实战

2026-09-06 14:39:47作者:舒璇辛Bertina

本文以 PowerToys 的 New+ 模块为核心,详解它如何在一套代码中同时适配 Windows 10 的旧式 IContextMenu 与 Windows 11 的现代 IExplorerCommand 两种上下文菜单机制,并完整给出两类处理器在开发环境中的注册、签名、附加调试器与故障排查流程。读完本文,你可以理解 New+ 的选择性注册策略如何避免 Windows 11 下的菜单项重复,并能独立走通 Win10 处理器改注册表、Win11 处理器签 MSIX 包的全套调试链路。

一、New+ 是什么,以及它为什么要做两个处理器

New+ 是 PowerToys 中用于在文件资源管理器右键菜单里"基于模板一键新建文件/文件夹"的模块。它通过模板文件夹扫描实现动态菜单:把模板目录下的文件(及文件夹)逐一列成子菜单项,末尾附加一个"Open templates"入口直接打开模板目录。

与普通的新建菜单增强不同,New+ 的核心工程难点在于 Windows 10 与 Windows 11 的上下文菜单机制不同:

  1. Windows 10 处理器NewPlus.ShellExtension.win10.dll
    • 实现传统的 IContextMenu 接口,即"old-style"上下文菜单处理器,用于兼容 Windows 10;
    • 在 Windows 11 上不会显示——这一点是有意为之,由 QueryContextMenu 中的一个条件控制;
    • 通过注册表键完成注册。
  2. Windows 11 处理器NewPlus.ShellExtension.dll
    • 以 sparse MSIX 包(稀疏包)形式实现,对接 Windows 11 的现代上下文菜单(IExplorerCommand);
    • 只在 Windows 11 上注册和使用。

这种"选择性注册"与 ImageResizer 等模块的策略不同:后者在 Windows 11 上同时注册两个处理器,导致菜单项出现重复。New+ 用单一处理器换取更干净的用户体验,代价是:如果 Windows 11 处理器注册失败,整个菜单入口会消失(见后文"常见问题")。

二、源码结构:两个子项目与模块入口

对应文档中的 "Project Structure",仓库源码位于 src/modules/NewPlus 下,按平台拆成两个子项目:

2.1 模块入口:启用时的注册动作

模块类 NewModulepowertoys_module.cpp 中实现 PowertoyModuleIface。模块键名为 NewPlus(见 constants.h 中的 powertoy_key)。关键的注册逻辑在 enable() 中:

virtual void enable() override
{
    // ...
    if (package::IsWin11OrGreater())
    {
        newplus::utilities::register_msix_package();   // Win11+:安装/注册 MSIX 包
    }
    powertoy_new_enabled = true;
    UpdateRegistration(powertoy_new_enabled);          // 内部调用 EnsureRegisteredWin10()
}

也就是说,在 Windows 11 及以上系统里,用户启用 New+ 时,PowerToys Runner 会同时做两件事:调用 register_msix_package() 处理 MSIX 包(这就是文档调试章节提到"先启动 PowerToys 设置并启用 New+,可以替你安装 MSIX 包"的底层原因),再由 UpdateRegistrationNewPlusRuntimeRegistration::EnsureRegisteredWin10() 写注册表。禁用时调用 Unregister() 移除注册。这解释了文档中"Win11 处理器注册不上时,菜单项不显示"的常见现象——注册动作依赖 Runner 正常加载了 New+ 模块,而不是资源管理器去触发。

2.2 Win10 处理器:QueryContextMenu 里的"Win11 屏蔽开关"

文档特别强调 Win10 处理器"在 Windows 11 上不显示(有意为之)"。这个开关就在 shell_context_menu_win10.cpp 的 QueryContextMenu 开头:

IFACEMETHODIMP shell_context_menu_win10::QueryContextMenu(HMENU menu_handle, UINT menu_index, UINT menu_first_cmd_id, UINT, UINT menu_flags)
{
    if (!NewSettingsInstance().GetEnabled()
        || package::IsWin11OrGreater()     // 模块未启用 或 处于 Win11+:直接返回 E_FAIL
        )
    {
        return E_FAIL;
    }
    // ...
}

返回 E_FAIL 后资源管理器就当作该扩展没有提供菜单项,从而保证 Windows 11 上不会出现"新式 + 旧式"双份的 New+ 入口。

菜单构建流程(QueryContextMenu 主体)值得展开,它与 New+ 的实际行为一一对应:

  1. 提前捕获鼠标位置:在菜单打开瞬间调用 GetCursorPos,并用 DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 上下文保证取到的是物理像素坐标——这个位置稍后用于在桌面上准确放置新建图标的重命名框;
  2. 扫描模板目录:通过 utilities::get_new_template_folder_location() 定位模板根目录(不存在时自动创建),template_folder::rescan_template_folder() 枚举其中的文件与文件夹作为模板;
  3. 逐项插入菜单add_template_item_to_context_menu() 为每个模板生成带图标的菜单项,菜单标题由 template_item.cppget_menu_title() 按三个开关计算:是否隐藏扩展名(HideFileExtension)、是否剥离前导数字(HideStartingDigits)、是否解析变量(ReplaceVariables);
  4. 追加分隔线与 "Open templates" 项add_open_templates_to_context_menu() 插入最后一个入口,点击后直接打开模板目录。

点击菜单项时走 InvokeCommand():按 LOWORD(params->lpVerb) - 1 还原被点中的索引,模板项调用 utilities::copy_template() 复制模板并进入重命名模式,最后一个索引则调用 open_template_folder()

两个实现细节体现了对资源管理器环境的敬畏:图标获取使用不抛异常的 std::filesystem 查询(template_item.cpp#L158-L165 注释说明"这里一旦抛异常可能拖垮 Shell 扩展");复制模板使用 SHFileOperationFO_COPY + FOF_ALLOWUNDO),成功后通过 SHChangeNotify(SHCNE_CREATE, ...) 通知资源管理器刷新,并由后台线程轮询(30ms 起步、最长 2000ms 的指数退避)等待条目出现在视图里再触发重命名。

2.3 Win11 处理器:sparse MSIX 与 IExplorerCommand

Windows 11 处理器位于 NewShellExtensionContextMenu 子项目,包身份与扩展声明见 AppxManifest.xml

  • 包名 Microsoft.PowerToys.NewPlusContextMenu,发布者为 CN=Microsoft Corporation, ...
  • 通过 desktop4:Extension Category="windows.fileExplorerContextMenus" 声明对 DirectoryDirectory\Background 两类对象的菜单,Verb 指向 CLSID 69824FC6-4660-4A09-9E7C-48DA63C6CC0F
  • com:ExtensionSurrogateServer 形式导出 COM 类 PowerToys.NewPlus.ShellExtension.dllThreadingModel="STA"

包携带 runFullTrustunvirtualizedResources 受限能力,且 AllowExternalContent=true——"sparse"(稀疏)正是指包体本身很小,DLL 的实际文件从 PowerToys 安装目录外部加载,而不是全部打进 MSIX。

COM 端实现是 shell_context_menu.cpp 中的 shell_context_menu 类,实现 IExplorerCommand

  • GetState():读取模块启用状态,未启用时返回 ECS_HIDDEN,启用时返回 ECS_ENABLEDshell_context_menu.cpp#L45-L57)——这就是设置界面里 New+ 开关能实时让菜单项消失/出现的机制;
  • GetFlags():返回 ECF_HASSUBCOMMANDS,声明这是一个带子命令的父级菜单项(标题为 "New+");
  • EnumSubCommands():真正枚举子项。与 Win10 路径一样,它先以 per-monitor-DPI-aware 上下文捕获光标位置(用于桌面图标放置),再创建 shell_context_sub_menu 承载模板列表。

2.4 设置项与默认值

New+ 的设置保存在 %LocalAppData%\Microsoft\PowerToys\NewPlus\settings.json,总体启用状态则保存在 PowerToys 全局 settings.jsonenabled.NewPlus 字段。这一点在 settings.cpp 的注释与构造函数中明确:

// NewSettings are stored in PowerToys/New/settings.json
// The New PowerToy enabled state is stored in the general PowerToys/settings.json

各 JSON 键与默认值(来自 InitializeWithDefaultSettings(),见 settings.cpp#L69-L82)如下,键名常量定义在 constants.h

JSON 键 含义 默认值
HideFileExtension 菜单中隐藏模板文件扩展名 true
HideStartingDigits 剥离模板文件名前导数字(如 01. First entry.txt 显示为 First entry 由设置决定
ReplaceVariables 解析文件名中的变量(如 $PARENT_FOLDER_NAME false
TemplateLocation 模板目录路径 %LocalAppData%\Microsoft\PowerToys\NewPlus\Templates
BuiltInNewHidePreference 是否隐藏 Windows 自带"新建"菜单 false

GetEnabled() 等方法在读取本地配置前还会先检查 GPO 组策略值(getConfiguredNewPlusEnabledValue() 等),策略显式配置时优先于本地开关。另外,NewModule::init_settings() 在模块启用且 BuiltInNewHidePreference 为真时会调用 disable_built_in_new_via_registry() 通过注册表隐藏 Windows 内建 New 菜单——这与下文"恢复内建 New 菜单"一节直接相关。

三、调试 Windows 10 处理器

Win10 处理器是普通 COM DLL,无需签名,调试路径相对简单,文档给出的完整步骤如下。

1. 将注册表指向你的调试构建(把 <NewPlus-CLSID> 换成实际 CLSID,即 69824FC6-4660-4A09-9E7C-48DA63C6CC0F):

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\CLSID\{<NewPlus-CLSID>}]
@="PowerToys NewPlus Extension"

[HKEY_CLASSES_ROOT\CLSID\{<NewPlus-CLSID>}\InprocServer32]
@="x:\GitHub\PowerToys\x64\Debug\PowerToys.NewPlusExt.win10.dll"
"ThreadingModel"="Apartment"

[HKEY_CURRENT_USER\Software\Classes\Directory\Background\shellex\ContextMenuHandlers\NewPlus]
@="{<NewPlus-CLSID>}"

2. 重启资源管理器使新注册生效:

taskkill /f /im explorer.exe && start explorer.exe

3. 将调试器附加到 explorer.exe 进程; 4. 在 NewPlus 代码中设置断点; 5. 在资源管理器中右键,触发上下文菜单处理器即可命中断点。

由于 Win10 处理器 DLL 常驻在 explorer.exe 进程内(而非像 Win11 那样每次触发才加载),断点可以稳定命中,这也是文档最后建议"开发测试时用 Win10 处理器更容易,因为它不需要签名"的原因。

四、调试 Windows 11 处理器(sparse MSIX 全流程)

Win11 处理器以 MSIX 包分发,AppX 框架要求包必须签名,因此调试链路显著变长。文档给出的 12 步流程如下,全部命令原样保留。

1. 构建 PowerToys 以产出 MSIX 包。

2. 创建代码签名证书(若还没有):

New-SelfSignedCertificate -Subject "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" `
    -KeyUsage DigitalSignature `
    -Type CodeSigningCert `
    -FriendlyName "PowerToys SelfCodeSigning" `
    -CertStoreLocation "Cert:\CurrentUser\My"

3. 获取证书指纹

$cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.FriendlyName -like "*PowerToys*" }
$cert.Thumbprint

4. 将证书装入受信任根存储(需要管理员终端):

Export-Certificate -Cert $cert -FilePath "$env:TEMP\PowerToysCodeSigning.cer"
Import-Certificate -FilePath "$env:TEMP\PowerToysCodeSigning.cer" -CertStoreLocation Cert:\LocalMachine\Root

也可以改用证书导入向导手动安装(向导依次选择"本地计算机 → 受信任的根证书颁发机构 → 下一步并选择 .cer 文件 → 完成")。

5. 用 SignTool 签名 MSIX 包

SignTool sign /fd SHA256 /sha1 <THUMBPRINT> "x:\GitHub\PowerToys\x64\Debug\WinUI3Apps\NewPlusPackage.msix"

注意:SignTool 可能不在 PATH 中,需给出完整路径,例如:

& "C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\signtool.exe" sign /fd SHA256 /sha1 <THUMBPRINT> "x:\GitHub\PowerToys\x64\Debug\WinUI3Apps\NewPlusPackage.msix"

包文件名 NewPlusPackage.msix 对应 constants.h 中的 msix_package_name 常量,包名 Microsoft.PowerToys.NewPlusContextMenu 对应 context_menu_package_name

6. 检查旧包并卸载(如已存在):

Get-AppxPackage -Name Microsoft.PowerToys.NewPlusContextMenu
Remove-AppxPackage Microsoft.PowerToys.NewPlusContextMenu_<VERSION>_neutral__8wekyb3d8bbwe

7. 安装新签名的 MSIX 包(如果会先启动 PowerToys 设置,此步可省略):

Add-AppxPackage -Path "x:\GitHub\PowerToys\x64\Debug\WinUI3Apps\NewPlusPackage.msix" -ExternalLocation "x:\GitHub\PowerToys\x64\Debug\WinUI3Apps"

-ExternalLocation 正是 sparse 包的体现:包内容仍留在外部目录,避免复制。文档同时说明,也可以直接启动 PowerToys 设置并启用 New+ 模块,由 Runner 替你完成 MSIX 安装(对应 2.1 节的 register_msix_package())。

8. 重启资源管理器

taskkill /f /im explorer.exe && start explorer.exe

9. 以管理员身份运行 Visual Studio(可选); 10. 在代码中设置断点,例如 shell_context_menu.cpp#L45GetState 的入口); 11. 在资源管理器中右键,此时会拉起一个标题带 NewPlus 字样的 DllHost.exe 进程,把调试器附加到它上面:

调试时附加到 DllHost.exe 进程(任务管理器中可见标题含 NewPlus 的 DllHost 进程)

12. 附加完成后立刻再次右键,快速触发上下文菜单以命中断点。

一个必须理解的行为(文档原话,源码也印证了):DllHost 进程只在触发上下文菜单时加载 DLL,菜单关闭后即卸载,所以附加后必须马上再次触发。文档给出的务实建议是:调试此类 Shell 扩展时,优先使用日志或消息框,而不是依赖断点——New+ 自身也大量使用 Logger::error(如 EnumSubCommands 捕获异常时记录 "New+ create submenu error")支撑这种开发模式。

五、常见问题与内建 New 菜单恢复

文档的 "Common Issues" 列出的 Win11 菜单项不显示场景与排查方向:

  • 包未正确注册——检查 Get-AppxPackage -Name Microsoft.PowerToys.NewPlusContextMenu 是否返回记录,确认 MSIX 是否真的安装成功;
  • 注册后未重启 Explorer——资源管理器缓存了扩展列表,任何注册变更(含上文的注册表修改)后都要执行 taskkill /f /im explorer.exe && start explorer.exe
  • MSIX 包签名问题——签名证书必须已导入 Cert:\LocalMachine\Root,且签名算法/指纹与 AppxManifest.xml 中声明的 Publisher 一致。

以及一条开发经验:开发和测试阶段优先使用 Win10 处理器,省去签名环节。

如果卸载 PowerToys 后 Windows 11 内建的 New 菜单没有回来(通常与 BuiltInNewHidePreference 的注册表痕迹或设置异常有关),文档给出恢复步骤:

  1. 打开注册表编辑器;
  2. 定位到 Computer\HKEY_CURRENT_USER\Software\Classes\Directory\Background\ShellEx\ContextMenuHandlers
  3. 删除其中的 New 子键(完整路径 Computer\HKEY_CURRENT_USER\Software\Classes\Directory\Background\ShellEx\ContextMenuHandlers\New)。

六、小结:从 New+ 看 Shell 扩展的平台分叉设计

New+ 是 PowerToys 中处理 Windows 10/11 上下文菜单机制差异的一个典型样本:

  • 用两个独立子项目分别实现 IContextMenu(Win10,shell_context_menu_win10.cpp)与 IExplorerCommand(Win11,shell_context_menu.cpp),共享模板扫描、设置解析与图标工具代码;
  • QueryContextMenu 里的 IsWin11OrGreater() 早退条件实现"Win11 上只保留新式处理器"的选择性注册,换取不重复的菜单体验;
  • Win11 侧以 sparse MSIX(AppxManifest.xml)声明 Directory/Directory\Background 菜单与 STA COM 类,由 Runner 在启用模块时自动注册;
  • 设置项(HideFileExtensionHideStartingDigitsReplaceVariablesTemplateLocationBuiltInNewHidePreference)与 GPO 策略在 settings.cpp 中统一解析,模块开关能实时反映为菜单的 ECS_HIDDEN 状态。

掌握上述链路后,你就能独立构建、签名、注册并断点调试 New+ 在两个 Windows 版本上的全部上下文菜单路径。

登录后查看全文
热门项目推荐
相关项目推荐