Windows11一级右键菜单添加Alacritty教程

Windows 11 一级右键菜单添加自定义应用(以 Alacritty 为例)

目标:在 Windows 11 文件资源管理器的一级右键菜单(不是"显示更多选项")中添加自定义应用入口,例如在文件夹空白处右键直接出现 "Open Alacritty here",点击即以该目录为工作目录打开终端。

本教程完整复现了 VS Code "通过 Code 打开" 所用的官方机制:稀疏包(Sparse Package)+ PackagedCom + IExplorerCommand。已在本机(Windows 11 25H2, build 26200)实测通过。


一、背景:为什么普通注册表项进不了一级菜单

Windows 11 对右键菜单做了重新设计,一级菜单只显示:

  1. 内置命令(查看、排序、新建...)
  2. 通过 IExplorerCommand + 应用身份 注册的扩展(应用分组区)
  3. 部分系统命令

而经典注册表方式全部降级到"显示更多选项"(即旧版完整菜单):

  • HKEY_CLASSES_ROOT\Directory\Background\shell\<verb>\command 这类静态 verb
  • IContextMenu COM 动态处理(TortoiseGit 等老式扩展)

曾流行的"白名单键名"技巧已失效

2022--2023 年间流传的做法是把自定义项的键名 命名为系统白名单 ID(如 SetDesktopWallpaperPinToHomeExtract),例如 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 SDK shobjidl_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 放在同一目录(替换点NamePublisherDisplayName、两处 ClsidPathExecutable):

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 堆损坏 SysAllocStringCoTaskMemFree 混用(被钩子环境) 改用 CoTaskMemAlloc + wcscpy
点击菜单项后无反应/阻塞 ShellExecuteExW + SEE_MASK_NOASYNC 等待子进程退出 改用 CreateProcessW
注册成功但菜单不出现 Explorer 缓存包列表 重启 Explorer 或注销
一级菜单出现但在旧版"显示更多选项"也有 正常,IExplorerCommand 项同时出现在两个菜单 ---
白名单键名(SetDesktopWallpaper 等)无效 24H2+ 已移除该机制 使用本文的稀疏包方案

六、参考

相关推荐
智闲电子设计41 分钟前
STM32 DAC 实战:用 DMA 把定时器表变成波形,自制信号发生器
stm32·单片机·嵌入式硬件
zcmodeltech1 小时前
反应装置模型控制系统设计与实现:多设备协同联动方案
java·网络·数据库·stm32·嵌入式硬件·能源·制造
深圳市恒锐丰科技杨生1 小时前
CL4054HL 36V 耐压 600mA 单节锂电池线性充电芯片|SOT23‑5 小型便携充电方案
嵌入式硬件·硬件工程
ALINX技术博客1 小时前
【期刊共读】如何应对 FPGA 短缺常态?
嵌入式硬件·fpga开发·fpga
快秃头的码农1 小时前
esp32-s3 espidf实现st7789-tn+电阻屏
嵌入式硬件
电子工匠1 小时前
Altium + Quilter 搭建指南
嵌入式硬件·ai·pcb工艺
HUI-4741 小时前
EG1185|SOT23‑6 高压非隔离恒压恒流控制器,极简外围高压辅助与 LED 驱动方案
嵌入式硬件·硬件工程
louis.D.2 小时前
CMake + VS Code + J-Link
单片机
恒锐丰科技林技术员2 小时前
SS6952T 单通道 H 桥电机驱动芯片:大电流电机一体化解决方案
经验分享·嵌入式硬件·硬件工程