# 第 ② 章 · 现代命令模型（IExplorerCommand）

> 用 IExplorerCommand 重写右键命令——标题/图标/状态/执行拆成独立方法，选中项走 IShellItemArray，注册改用 ExplorerCommandHandler；它也是进入 Win11 主菜单的实现基础。

- 作者：David（道雾轩）
- 专栏：Windows Shell 扩展指南（https://daiw.org/manual/windows-shell-ext.md）
- 最后更新：2026-07-12
- 原文：https://daiw.org/manual/windows-shell-ext/explorer-command
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

第 ① 章的 `IContextMenu` 是 1996 年的接口：一个方法里既画菜单又管命令 ID，还要自己解析 verb 字符串。Windows 7 引入的 [`IExplorerCommand`](https://learn.microsoft.com/zh-cn/windows/win32/api/shobjidl_core/nn-shobjidl_core-iexplorercommand) 是更现代的模型——**一个 `IExplorerCommand` 对象就是“一条命令”**，标题、图标、状态、执行各是一个方法。更重要的是：**它是把命令送进 Windows 11 主右键菜单的实现基础**（第 ③ 章打包时会直接复用本章的类）。

配套代码：[`02-explorer-command`](https://github.com/davidapp/shell-extension-demos/tree/main/02-explorer-command)。

## 先抽出公共地基

从本章起，第 ① 章手写的那套 COM 样板（类工厂、DLL 引用计数、注册表工具）抽进了仓库根的 [`common/ShellExtBase.h`](https://github.com/davidapp/shell-extension-demos/blob/main/common/ShellExtBase.h)。其中最有用的是一个**类工厂模板**：

```cpp title="common/ShellExtBase.h（节选）"
template <class THandler>
class ClassFactory : public IClassFactory {
    IFACEMETHODIMP CreateInstance(IUnknown* outer, REFIID riid, void** ppv) override {
        if (outer) return CLASS_E_NOAGGREGATION;
        THandler* p = new (std::nothrow) THandler();
        if (!p) return E_OUTOFMEMORY;
        HRESULT hr = p->QueryInterface(riid, ppv);
        p->Release();
        return hr;
    }
    // …AddRef/Release/LockServer/QueryInterface…
};
```

于是每个新示例的 `dllmain.cpp` 里，`DllGetClassObject` 只要 `new ClassFactory<你的处理器>()` 即可，不用再抄一遍工厂。`g_cDllRef` 用了 C++17 的 **inline 变量**，跨编译单元只有一份实例。

## IExplorerCommand 的八个方法

| 方法 | 作用 |
| --- | --- |
| `GetTitle` | 菜单显示的文字 |
| `GetIcon` | 图标（`"dll,-资源ID"` 字符串；不要就返回 `E_NOTIMPL`） |
| `GetToolTip` | 提示文字 |
| `GetState` | 该命令此刻是启用 / 禁用 / 隐藏（`EXPCMDSTATE`） |
| `GetFlags` | 命令特性（有无子菜单、是否分隔符……`EXPCMDFLAGS`） |
| `EnumSubCommands` | 级联子命令的枚举器 |
| `GetCanonicalName` | 命令的 GUID 标识（没有就返回 `GUID_NULL`） |
| `Invoke` | 用户点击时执行 |

和 `IContextMenu` 最大的不同：**选中的文件通过参数 `IShellItemArray` 传进来**，不再需要 `IShellExtInit`。

```cpp title="ExplorerCommand.cpp（节选）"
IFACEMETHODIMP FileInfoCommand::GetTitle(IShellItemArray*, LPWSTR* ppszName) {
    return SHStrDupW(L"显示文件信息 (IExplorerCommand)", ppszName);
}
IFACEMETHODIMP FileInfoCommand::GetState(IShellItemArray*, BOOL, EXPCMDSTATE* pState) {
    *pState = ECS_ENABLED;      // 也可返回 ECS_DISABLED / ECS_HIDDEN
    return S_OK;
}
IFACEMETHODIMP FileInfoCommand::Invoke(IShellItemArray* items, IBindCtx*) {
    IShellItem* psi = nullptr;
    items->GetItemAt(0, &psi);
    PWSTR path = nullptr;
    psi->GetDisplayName(SIGDN_FILESYSPATH, &path);   // 拿文件系统路径
    // …取大小、MessageBox…
    CoTaskMemFree(path);
    psi->Release();
    return S_OK;
}
```

<Callout type="warn">
  `GetTitle` / `GetIcon` / `GetToolTip` 返回的字符串必须用 **`CoTaskMemAlloc`** 分配（`SHStrDupW` 就是），由 Shell 负责释放；用 `malloc`/`new` 会内存出错。同理 `GetDisplayName` 拿到的 `path` 要用 `CoTaskMemFree` 释放。
</Callout>

<Callout type="info">
  `IExplorerCommand` 的所有方法都在 **UI 线程**调用，且**只支持进程内激活**。绝不能在里面做网络请求或其它阻塞操作——会直接卡住资源管理器界面。
</Callout>

## 级联子菜单

想做“一个父项展开出多条子命令”，只需两步：`GetFlags` 返回 **`ECF_HASSUBCOMMANDS`**，并在 `EnumSubCommands` 里返回一个 `IEnumExplorerCommand` 枚举器（每个元素又是一个 `IExplorerCommand`）。本例没用到，返回 `E_NOTIMPL`。这也是 Win7+ 做级联菜单的推荐方式（比注册表的 `SubCommands` 更灵活）。

## 注册：ExplorerCommandHandler

这是和第 ① 章的另一处关键区别。`IExplorerCommand` 作为 verb 注册在 **`shell\<verb>`** 下，用一个名为 **`ExplorerCommandHandler`** 的值指向 CLSID（而不是 `ShellEx\ContextMenuHandlers`）：

```
CLSID\{GUID}\InProcServer32\  (默认) = DLL 路径, ThreadingModel = Apartment
*\shell\FileInfoExplorerCmd\  (默认) = 显示文件信息 (IExplorerCommand)
                             ExplorerCommandHandler = {GUID}
```

```cpp title="dllmain.cpp（节选）"
RegWriteString(HKEY_CLASSES_ROOT, kVerbKey, nullptr, L"显示文件信息 (IExplorerCommand)");
RegWriteString(HKEY_CLASSES_ROOT, kVerbKey, L"ExplorerCommandHandler", clsidStr);
```

<Callout type="info">
  记事本、Notepad++ 等都用这套 `ExplorerCommandHandler` 注册法接入右键菜单。它比经典 `ContextMenuHandlers` 更省事——不用自己实现 `QueryContextMenu`/`InvokeCommand`。
</Callout>

## 构建与验证

```bat
cmake -B build -A x64
cmake --build build --config Release
```
```powershell
.\install.ps1              # 右键任意文件（Win11 在"显示更多选项"里）
.\install.ps1 -Uninstall
```

到这里，命令仍然落在 Win11 的「显示更多选项」旧菜单里——因为它还是**用注册表注册的**。下一章我们给它套上 MSIX / 稀疏包，把**同一个 `IExplorerCommand` 类**送进 Win11 的**主**右键菜单。

## 参考

- [IExplorerCommand](https://learn.microsoft.com/zh-cn/windows/win32/api/shobjidl_core/nn-shobjidl_core-iexplorercommand) · [EXPCMDSTATE](https://learn.microsoft.com/zh-cn/windows/win32/api/shobjidl_core/ne-shobjidl_core-expcmdstate) · [EXPCMDFLAGS](https://learn.microsoft.com/zh-cn/windows/win32/api/shobjidl_core/ne-shobjidl_core-expcmdflags)
- [ExplorerCommandVerb 官方示例](https://github.com/microsoft/Windows-classic-samples/tree/main/Samples/Win7Samples/winui/shell/appshellintegration/ExplorerCommandVerb)
- [在 Windows 11 扩展右键菜单与共享对话框](https://blogs.windows.com/windowsdeveloper/2021/07/19/extending-the-context-menu-and-share-dialog-in-windows-11/)
