第 ② 章 · 现代命令模型(IExplorerCommand)
用 IExplorerCommand 重写右键命令——标题/图标/状态/执行拆成独立方法,选中项走 IShellItemArray,注册改用 ExplorerCommandHandler;它也是进入 Win11 主菜单的实现基础。
第 ① 章的 IContextMenu 是 1996 年的接口:一个方法里既画菜单又管命令 ID,还要自己解析 verb 字符串。Windows 7 引入的 IExplorerCommand 是更现代的模型——一个 IExplorerCommand 对象就是“一条命令”,标题、图标、状态、执行各是一个方法。更重要的是:它是把命令送进 Windows 11 主右键菜单的实现基础(第 ③ 章打包时会直接复用本章的类)。
配套代码:02-explorer-command。
先抽出公共地基
从本章起,第 ① 章手写的那套 COM 样板(类工厂、DLL 引用计数、注册表工具)抽进了仓库根的 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。
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;
}GetTitle / GetIcon / GetToolTip 返回的字符串必须用 CoTaskMemAlloc 分配(SHStrDupW 就是),由 Shell 负责释放;用 malloc/new 会内存出错。同理 GetDisplayName 拿到的 path 要用 CoTaskMemFree 释放。
IExplorerCommand 的所有方法都在 UI 线程调用,且只支持进程内激活。绝不能在里面做网络请求或其它阻塞操作——会直接卡住资源管理器界面。
级联子菜单
想做“一个父项展开出多条子命令”,只需两步: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}RegWriteString(HKEY_CLASSES_ROOT, kVerbKey, nullptr, L"显示文件信息 (IExplorerCommand)");
RegWriteString(HKEY_CLASSES_ROOT, kVerbKey, L"ExplorerCommandHandler", clsidStr);记事本、Notepad++ 等都用这套 ExplorerCommandHandler 注册法接入右键菜单。它比经典 ContextMenuHandlers 更省事——不用自己实现 QueryContextMenu/InvokeCommand。
构建与验证
cmake -B build -A x64
cmake --build build --config Release.\install.ps1 # 右键任意文件(Win11 在"显示更多选项"里)
.\install.ps1 -Uninstall到这里,命令仍然落在 Win11 的「显示更多选项」旧菜单里——因为它还是用注册表注册的。下一章我们给它套上 MSIX / 稀疏包,把同一个 IExplorerCommand 类送进 Win11 的主右键菜单。