Windows 11 一级右键菜单添加自定义应用(以 Alacritty 为例)
目标:在 Windows 11 文件资源管理器的一级右键菜单(不是"显示更多选项")中添加自定义应用入口,例如在文件夹空白处右键直接出现 "Open Alacritty here",点击即以该目录为工作目录打开终端。
本教程完整复现了 VS Code "通过 Code 打开" 所用的官方机制:稀疏包(Sparse Package)+ PackagedCom + IExplorerCommand。已在本机(Windows 11 25H2, build 26200)实测通过。
一、背景:为什么普通注册表项进不了一级菜单
Windows 11 对右键菜单做了重新设计,一级菜单只显示:
- 内置命令(查看、排序、新建...)
- 通过 IExplorerCommand + 应用身份 注册的扩展(应用分组区)
- 部分系统命令
而经典注册表方式全部降级到"显示更多选项"(即旧版完整菜单):
HKEY_CLASSES_ROOT\Directory\Background\shell\<verb>\command这类静态 verbIContextMenuCOM 动态处理(TortoiseGit 等老式扩展)
曾流行的"白名单键名"技巧已失效
2022--2023 年间流传的做法是把自定义项的键名 命名为系统白名单 ID(如 SetDesktopWallpaper、PinToHome、Extract),例如 PeaZip 8.5 的 add PeaZip...reg 脚本。该方法在 Windows 11 24H2 及以后已被微软移除,不再生效。
正路:稀疏包 + PackagedCom
微软官方博客(Extending the Context Menu and Share Dialog in Windows 11)给出的未打包 Win32 应用方案:
- 应用获得包身份(sparse package,无 payload 需求,可注册任意文件夹)
- 通过清单声明一个 IExplorerCommand COM 服务器(PackagedCom)
- Explorer 把该命令渲染到一级菜单
VS Code(code_explorer_command_x64.dll)、Zed 都采用此机制。下面按此实现。
二、原理
AppxManifest.xml(稀疏包清单,放在任意目录)
├── desktop4:Extension Category="windows.fileExplorerContextMenus"
│ └── ItemType Type="Directory\Background" → Verb Clsid={你的CLSID}
└── com:Extension Category="windows.comServer"
└── SurrogateServer → com:Class Path="xxx.dll" ThreadingModel="STA"
xxx.dll(in-proc COM 服务器,由 dllhost 代理加载)
└── 实现 IExplorerCommand
├── GetTitle() → 菜单文字,如 "Open Alacritty here"
├── GetIcon() → 图标(返回 exe 路径即可)
├── GetState() → ECS_ENABLED
└── Invoke() → 执行动作(启动程序、传参)
注册:Add-AppxPackage -Register(需要开发者模式)
效果:包身份写入 HKLM\Software\Classes\PackagedCom,一级菜单出现该项
三、实施步骤
1. 准备
- C++ 编译器。本教程使用 TDM-GCC 64(MinGW-w64);MSVC 同样适用,代码无 ATL 依赖。
- 生成一个 GUID 作为 COM 类 CLSID(后面代码和清单都要用到):
powershell
[guid]::NewGuid().ToString()
2. 编写 IExplorerCommand DLL
保存为 alacritty_context.cpp(替换点 :CLSID、kAlacrittyPath 程序路径、kMenuTitle 菜单文字、Invoke 中的启动命令):
cpp
// Windows 11 一级右键菜单命令(IExplorerCommand)
// 编译命令见文件头注释
#define WIN32_LEAN_AND_MEAN
#define UNICODE
#define _UNICODE
#include <new>
#include <windows.h>
#include <shellapi.h>
#include <shobjidl.h>
#include <shlwapi.h>
// COM 类 CLSID(与 AppxManifest.xml 中的 Clsid 一致,用第一步生成的 GUID 替换)
static const CLSID CLSID_AlacrittyCommand = {
0x4f241d8c, 0x15b1, 0x4104, {0x99, 0x3e, 0x33, 0x61, 0x23, 0x52, 0xd8, 0x57}
};
static const wchar_t kAlacrittyPath[] = L"C:\\Program Files\\Alacritty\\alacritty.exe";
static const wchar_t kMenuTitle[] = L"Open Alacritty here";
// shell 会用 CoTaskMemFree 释放 GetTitle/GetIcon 返回的字符串,
// 所以必须用 CoTaskMemAlloc 分配。
// 注意:不要用 SysAllocString------在某些被钩子注入的环境下,
// SysAllocString 的块与 CoTaskMemFree 不兼容,实测会堆损坏(0xC0000374)。
static LPWSTR alloc_string(const wchar_t *s) {
size_t len = wcslen(s);
LPWSTR p = static_cast<LPWSTR>(CoTaskMemAlloc((len + 1) * sizeof(wchar_t)));
if (p) wcscpy(p, s);
return p;
}
class AlacrittyCommand final : public IExplorerCommand {
public:
AlacrittyCommand() : m_ref(1) {}
// ---- IUnknown ----
STDMETHODIMP QueryInterface(REFIID riid, void **ppv) override {
if (!ppv) return E_POINTER;
*ppv = nullptr;
if (riid == __uuidof(IUnknown) || riid == __uuidof(IExplorerCommand)) {
*ppv = static_cast<IExplorerCommand *>(this);
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
STDMETHODIMP_(ULONG) AddRef() override { return InterlockedIncrement(&m_ref); }
STDMETHODIMP_(ULONG) Release() override {
ULONG refs = InterlockedDecrement(&m_ref);
if (refs == 0) delete this;
return refs;
}
// ---- IExplorerCommand ----
STDMETHODIMP GetTitle(IShellItemArray *, LPWSTR *ppszName) override {
*ppszName = alloc_string(kMenuTitle);
return *ppszName ? S_OK : E_OUTOFMEMORY;
}
STDMETHODIMP GetIcon(IShellItemArray *, LPWSTR *ppszIcon) override {
*ppszIcon = alloc_string(kAlacrittyPath);
return *ppszIcon ? S_OK : E_OUTOFMEMORY;
}
STDMETHODIMP GetToolTip(IShellItemArray *, LPWSTR *ppszInfotip) override {
*ppszInfotip = nullptr;
return E_NOTIMPL;
}
STDMETHODIMP GetCanonicalName(GUID *pguidCommandName) override {
*pguidCommandName = CLSID_AlacrittyCommand;
return S_OK;
}
STDMETHODIMP GetState(IShellItemArray *, BOOL, EXPCMDSTATE *pCmdState) override {
*pCmdState = ECS_ENABLED;
return S_OK;
}
STDMETHODIMP GetFlags(EXPCMDFLAGS *pFlags) override {
*pFlags = ECF_DEFAULT;
return S_OK;
}
STDMETHODIMP EnumSubCommands(IEnumExplorerCommand **ppEnum) override {
*ppEnum = nullptr;
return E_NOTIMPL;
}
STDMETHODIMP Invoke(IShellItemArray *psiItemArray, IBindCtx *) override {
WCHAR dir[MAX_PATH];
bool haveDir = false;
// Directory\Background 场景下,数组第一项就是当前文件夹;
// 若是文件则取其父目录。
if (psiItemArray) {
DWORD count = 0;
if (SUCCEEDED(psiItemArray->GetCount(&count)) && count > 0) {
IShellItem *item = nullptr;
if (SUCCEEDED(psiItemArray->GetItemAt(0, &item)) && item) {
LPWSTR path = nullptr;
if (SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &path)) && path) {
DWORD attrs = GetFileAttributesW(path);
if (attrs != INVALID_FILE_ATTRIBUTES) {
lstrcpynW(dir, path, MAX_PATH);
if (!(attrs & FILE_ATTRIBUTE_DIRECTORY)) {
WCHAR *slash = wcsrchr(dir, L'\\');
if (slash) *slash = L'\0';
}
haveDir = true;
}
CoTaskMemFree(path);
}
item->Release();
}
}
}
if (!haveDir) {
if (!GetCurrentDirectoryW(MAX_PATH, dir)) {
lstrcpynW(dir, L"C:\\", MAX_PATH);
}
}
WCHAR cmdline[MAX_PATH + 64];
wcscpy(cmdline, L"\"C:\\Program Files\\Alacritty\\alacritty.exe\" --working-directory \"");
wcscat(cmdline, dir);
wcscat(cmdline, L"\"");
// 用 CreateProcessW 启动,立即返回。
// 不要用 ShellExecuteExW + SEE_MASK_NOASYNC:它会阻塞到子进程退出。
STARTUPINFOW si = { sizeof(si) };
PROCESS_INFORMATION pi{};
if (CreateProcessW(kAlacrittyPath, cmdline, nullptr, nullptr, FALSE,
CREATE_UNICODE_ENVIRONMENT, nullptr, nullptr, &si, &pi)) {
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}
return S_OK;
}
private:
LONG m_ref;
};
class AlacrittyFactory final : public IClassFactory {
public:
AlacrittyFactory() : m_ref(1) {}
// ---- IUnknown ----
STDMETHODIMP QueryInterface(REFIID riid, void **ppv) override {
if (!ppv) return E_POINTER;
*ppv = nullptr;
if (riid == __uuidof(IUnknown) || riid == __uuidof(IClassFactory)) {
*ppv = static_cast<IClassFactory *>(this);
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
STDMETHODIMP_(ULONG) AddRef() override { return InterlockedIncrement(&m_ref); }
STDMETHODIMP_(ULONG) Release() override {
ULONG refs = InterlockedDecrement(&m_ref);
if (refs == 0) delete this;
return refs;
}
// ---- IClassFactory ----
STDMETHODIMP CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppv) override {
if (pUnkOuter) return CLASS_E_NOAGGREGATION;
AlacrittyCommand *cmd = new (std::nothrow) AlacrittyCommand();
if (!cmd) return E_OUTOFMEMORY;
HRESULT hr = cmd->QueryInterface(riid, ppv);
cmd->Release();
return hr;
}
STDMETHODIMP LockServer(BOOL) override { return S_OK; }
private:
LONG m_ref;
};
extern "C" {
__declspec(dllexport) HRESULT WINAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void **ppv) {
if (!IsEqualCLSID(rclsid, CLSID_AlacrittyCommand)) return CLASS_E_CLASSNOTAVAILABLE;
AlacrittyFactory *factory = new (std::nothrow) AlacrittyFactory();
if (!factory) return E_OUTOFMEMORY;
HRESULT hr = factory->QueryInterface(riid, ppv);
factory->Release();
return hr;
}
__declspec(dllexport) HRESULT WINAPI DllCanUnloadNow(void) {
return S_OK;
}
__declspec(dllexport) HRESULT WINAPI DllRegisterServer(void) {
return S_OK; // 打包注册即可,regsvr32 非必需
}
__declspec(dllexport) HRESULT WINAPI DllUnregisterServer(void) {
return S_OK;
}
} // extern "C"
接口布局注意 :
IExplorerCommand的 vtable 是 8 个方法(含 GetToolTip) 的现行布局。MinGW-w64 的旧版shobjidl.h与此一致可直接用;若使用老工具链缺失该接口定义,请按 Windows SDKshobjidl_core.h中的定义自行声明,不要按旧博客里的 7 方法布局手写(Explorer 按现行布局调用,错位会崩溃)。
3. 编译
bash
g++ -O2 -shared -static-libgcc -static-libstdc++ -DUNICODE -D_UNICODE \
-o alacritty_context.dll alacritty_context.cpp \
-lole32 -loleaut32 -lshell32 -lshlwapi
4. 生成清单引用的 logo(两个纯色 PNG 即可)
python
import os, struct, zlib
def png(w, h, rgba):
def chunk(t, d):
c = struct.pack(">I", len(d)) + t + d
return c + struct.pack(">I", zlib.crc32(t + d) & 0xFFFFFFFF)
ihdr = struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0)
raw = b"".join(b"\x00" + bytes(rgba) * w for _ in range(h))
return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr)
+ chunk(b"IDAT", zlib.compress(raw)) + chunk(b"IEND", b""))
open("logo150.png", "wb").write(png(150, 150, (38, 38, 38, 255)))
open("logo44.png", "wb").write(png(44, 44, (38, 38, 38, 255)))
5. 编写稀疏包清单 AppxManifest.xml
与 DLL、logo 放在同一目录(替换点 :Name、Publisher、DisplayName、两处 Clsid、Path、Executable):
xml
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4"
xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5"
xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
IgnorableNamespaces="uap rescap desktop desktop4 desktop5 desktop6 uap10 com">
<Identity
Name="AlacrittyShellExt"
Publisher="CN=Alacritty"
Version="1.0.0.0"
ProcessorArchitecture="neutral" />
<Properties>
<DisplayName>Alacritty Shell Extension</DisplayName>
<PublisherDisplayName>Alacritty</PublisherDisplayName>
<Logo>logo150.png</Logo>
<desktop6:RegistryWriteVirtualization>disabled</desktop6:RegistryWriteVirtualization>
<desktop6:FileSystemWriteVirtualization>disabled</desktop6:FileSystemWriteVirtualization>
</Properties>
<Resources>
<Resource Language="en-us" />
</Resources>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
</Dependencies>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
<rescap:Capability Name="unvirtualizedResources" />
</Capabilities>
<Applications>
<Application Id="Alacritty"
Executable="alacritty.exe"
uap10:TrustLevel="mediumIL"
uap10:RuntimeBehavior="win32App">
<uap:VisualElements
AppListEntry="none"
DisplayName="Alacritty Shell Extension"
Description="Open Alacritty here"
BackgroundColor="transparent"
Square150x150Logo="logo150.png"
Square44x44Logo="logo44.png">
</uap:VisualElements>
<Extensions>
<desktop4:Extension Category="windows.fileExplorerContextMenus">
<desktop4:FileExplorerContextMenus>
<desktop5:ItemType Type="Directory">
<desktop5:Verb Id="OpenInAlacritty" Clsid="4f241d8c-15b1-4104-993e-33612352d857" />
</desktop5:ItemType>
<desktop5:ItemType Type="Directory\Background">
<desktop5:Verb Id="OpenInAlacritty" Clsid="4f241d8c-15b1-4104-993e-33612352d857" />
</desktop5:ItemType>
</desktop4:FileExplorerContextMenus>
</desktop4:Extension>
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:SurrogateServer DisplayName="Alacritty Shell Extension">
<com:Class Id="4f241d8c-15b1-4104-993e-33612352d857" Path="alacritty_context.dll" ThreadingModel="STA"/>
</com:SurrogateServer>
</com:ComServer>
</com:Extension>
</Extensions>
</Application>
</Applications>
</Package>
说明:
ItemType Type="Directory"→ 右键文件夹本身 时显示;Directory\Background→ 右键文件夹空白处时显示(桌面也属于 Background)。只想要一种就删掉另一项。- 不要 加
uap10:AllowExternalContent="true":Add-AppxPackage -Register模式下会报 0x80073CF9(外部位置安装)。 Executable指向包外的程序名即可(VS Code 同款写法,清单里不会被实际启动)。
6. 开启开发者模式并注册
注册表开启开发者模式(需要管理员权限的命令行):
bat
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /v AllowDevelopmentWithoutDevLicense /t REG_DWORD /d 1 /f
注册稀疏包:
powershell
Add-AppxPackage -Register "C:\你的目录\AppxManifest.xml" -ForceApplicationShutdown
未签名的
-Register安装必须开启开发者模式,否则报 0x80073CFF。注册成功后包的身份进入HKLM\Software\Classes\PackagedCom。
7. 重启资源管理器
新菜单对包列表有缓存,重启 Explorer 生效:
bat
taskkill /f /im explorer.exe
start explorer.exe
(或任务管理器重启 Explorer / 注销重登)
8. 验证
powershell
Get-AppxPackage -Name AlacrittyShellExt # 能看到包及 InstallLocation
bat
reg query "HKLM\Software\Classes\PackagedCom\Package\AlacrittyShellExt_1.0.0.0_neutral__nbmxatwdvsmxj" /s
应看到 Class\{你的CLSID}(DllPath)与 Server\0(SurrogateAppId)两条------结构与 VS Code 的 Microsoft.VisualStudioCode_* 完全同构。
最后:文件夹空白处右键,一级菜单应出现带图标的 "Open Alacritty here"。
四、卸载
powershell
Get-AppxPackage -Name AlacrittyShellExt | Remove-AppxPackage
然后删除包目录(DLL、清单、logo)。如需关闭开发者模式:
bat
reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /v AllowDevelopmentWithoutDevLicense /f
(或 设置 → 系统 → 开发者选项 关闭;已安装的包不受影响)
更新 DLL 后重新执行 Add-AppxPackage -Register 即生效,无需卸载。
五、常见错误排查
| 现象 | 原因 | 解决 |
|---|---|---|
| 0x80073CFF,要求证书验证/回退策略 | 未签名 -Register 需要开发者模式 |
开启 AllowDevelopmentWithoutDevLicense |
| 0x80073CF9,"使用外部位置安装" | 清单含 uap10:AllowExternalContent |
删除该属性后重试 |
| 0xC0000374 堆损坏 | SysAllocString 与 CoTaskMemFree 混用(被钩子环境) |
改用 CoTaskMemAlloc + wcscpy |
| 点击菜单项后无反应/阻塞 | ShellExecuteExW + SEE_MASK_NOASYNC 等待子进程退出 |
改用 CreateProcessW |
| 注册成功但菜单不出现 | Explorer 缓存包列表 | 重启 Explorer 或注销 |
| 一级菜单出现但在旧版"显示更多选项"也有 | 正常,IExplorerCommand 项同时出现在两个菜单 | --- |
| 白名单键名(SetDesktopWallpaper 等)无效 | 24H2+ 已移除该机制 | 使用本文的稀疏包方案 |
六、参考
- 微软官方博客:Extending the Context Menu and Share Dialog in Windows 11
- Microsoft 示例:AppModelSamples(SparsePackages / PackageWithExternalLocation)
- 本机参考实现(含全部源码):
%LOCALAPPDATA%\AlacrittyShellExt\