解决注册表右键菜单失效(HKEY_CLASSES_ROOT)
2025-02-28 21:36:26
HKEY_CLASSES_ROOT\Directory\Background\shell
右键菜单项失效问题解决
在使用 IContextMenu::QueryContextMenu
获取文件夹空白处右键菜单(背景菜单)时, 发现一个问题:从注册表 HKEY_CLASSES_ROOT\Directory\Background\shell
注册的菜单项无法正常工作。 具体表现是,尝试选择这些菜单项时,InvokeCommand
调用失败,返回错误码 -2147023741
。 使用 "Error lookup" 工具查看这个错误码,会提示"没有应用程序与此操作的指定文件关联"。
问题原因分析
根本原因在于,HKEY_CLASSES_ROOT\Directory\Background\shell
下的菜单项通常依赖于 Explorer 的上下文环境来执行。 而直接通过 IContextMenu::QueryContextMenu
和 InvokeCommand
方式调用, 缺少了这种必要的上下文环境。 简单来说,就是 Explorer 并不知道该如何去处理这些菜单项。
例如,考虑注册表路径下的“新建”子菜单。 当你在资源管理器的背景中单击鼠标右键并选择“新建”时,Explorer.exe 会解析注册表,加载必要的组件并最终执行命令。如果你的代码中没有 Explorer 进程,那么即使运行了命令也执行不成功。
解决方案
以下是几种解决这个问题的可行方法。
方法一:使用 IContextMenu::GetCommandString
获取命令并手动执行
这种方法的核心思想是,绕过 InvokeCommand
,而是直接获取菜单项对应的命令字符串,然后自己来执行这个命令。
- 获取命令字符串: 使用
IContextMenu::GetCommandString
函数,并指定GCS_VERBW
或GCS_VERBA
标志,获取菜单项的谓词(verb)或命令字符串。 - 检查谓词/命令字符串: 将获取到的谓词/命令字符串与
HKEY_CLASSES_ROOT\Directory\Background\shell
下注册的命令进行比较。 - 手动执行命令: 如果谓词/命令字符串匹配,则从注册表读取对应的命令,并使用
ShellExecute
或CreateProcess
等函数手动执行。
代码示例(修改 WM_COMMAND
处理部分):
case WM_COMMAND:
if (LOWORD(wParam) == buttonId)
{
// ... (之前的代码保持不变) ...
if (cmd)
{
CMINVOKECOMMANDINFO cmi{};
cmi.cbSize = sizeof(CMINVOKECOMMANDINFO);
cmi.lpVerb = (LPSTR)MAKEINTRESOURCE(cmd - firstId);
cmi.nShow = SW_SHOWNORMAL;
CComPtr<IShellFolder> desktop;
SHGetDesktopFolder(&desktop); //Get desktop folder
//尝试用 IContextMenu 接口 InvokeCommand.
HRESULT hRes = cm->InvokeCommand(&cmi);
if (FAILED(hRes))
{
// 获取命令字符串。
wchar_t verbW[256] = { 0 };
hRes = cm->GetCommandString(cmd - firstId, GCS_VERBW, NULL, (LPSTR)verbW, 256);
if (SUCCEEDED(hRes))
{
HKEY hKey;
//open HKEY_CLASSES_ROOT\Directory\Background\shell
if (RegOpenKeyEx(HKEY_CLASSES_ROOT, L"Directory\\Background\\shell", 0, KEY_READ, &hKey) == ERROR_SUCCESS)
{
wchar_t subKey[256];
DWORD index = 0;
//遍历所有的key.
while (RegEnumKeyEx(hKey, index++, subKey, &(_countof(subKey)), nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS)
{
HKEY hSubKey;
// Open the 'command' subkey
if (RegOpenKeyEx(hKey, (std::wstring{ subKey } + L"\\command").c_str(), 0, KEY_READ, &hSubKey) == ERROR_SUCCESS)
{
wchar_t command[1024];
DWORD commandSize = sizeof(command);
// 读取 key.
if (RegQueryValueEx(hSubKey, nullptr, nullptr, nullptr, (LPBYTE)command, &commandSize) == ERROR_SUCCESS)
{
HKEY hSubKeyHKey;
//尝试读取 shell\key\
if (RegOpenKeyEx(hKey, subKey, 0, KEY_READ, &hSubKeyHKey) == ERROR_SUCCESS)
{
wchar_t verb[256];
DWORD verbSize = sizeof(verb);
// 查看有没有配置 verb,注意如果获取到的数据为 REG_SZ,末尾会包含空结束符 '\0',如果类型为 REG_EXPAND_SZ 则不包含。
if (RegQueryValueEx(hSubKeyHKey, L"MUIVerb", nullptr, nullptr, (LPBYTE)verb, &verbSize) == ERROR_SUCCESS || RegQueryValueEx(hSubKeyHKey, nullptr, nullptr, nullptr, (LPBYTE)verb, &verbSize) == ERROR_SUCCESS) {
if (wcscmp(verbW, verb) == 0) {
// 执行command.
ShellExecute(hwnd, nullptr, command, nullptr, nullptr, SW_SHOWNORMAL);
}
}
RegCloseKey(hSubKeyHKey);
}
//没有, 那么尝试执行 command.
else if (wcscmp(verbW, subKey) == 0 ) {
ShellExecute(hwnd, nullptr, command, nullptr, path, SW_SHOWNORMAL); // 假设需要传入路径。
// 已经执行,跳出循环。
break;
}
}
RegCloseKey(hSubKey);
}
}
RegCloseKey(hKey);
}
}
}
}
// ... (之后的代码保持不变) ...
}
break;
注意: 此方法增加了手动解析注册表的复杂性,且可能有兼容问题, 代码相对来说较多且复杂,需要注意指针和 key 的释放,确保代码的稳定性。
方法二:通过 IShellBrowser
和 IOleCommandTarget
模拟 Explorer 上下文
这种方法更加“正统”,它通过获取 IShellBrowser
接口,并进一步获取 IOleCommandTarget
接口,来模拟 Explorer 的上下文环境,然后通过 IOleCommandTarget::Exec
来执行命令。
- 获取
IShellBrowser
: 通过IUnknown::QueryService
接口 (在Site
类中) 获取服务SID_STopLevelBrowser
, IID_IShellBrowser, 即可拿到IShellBrowser
。 - 获取
IOleCommandTarget
: 通过IShellBrowser::QueryInterface
获取IOleCommandTarget
接口。 - 构建
CMINVOKECOMMANDINFOEX
: 创建CMINVOKECOMMANDINFOEX
结构体,并设置必要的参数,包括lpVerb
(菜单项的命令ID)和ptInvoke
(点击坐标)。注意cbSize
要设置成CMINVOKECOMMANDINFOEX
的大小。 - 执行命令: 调用
IOleCommandTarget::Exec
函数,使用CGID_ShellServiceObject
作为命令组,SH ELLVERB_INVOKECOMMAND
作为命令ID,传入CMINVOKECOMMANDINFOEX
结构体。
代码示例(添加并修改Site类和 WM_COMMAND
处理部分):
在Site
类的QueryInterface
添加:
if (riid == IID_IShellBrowser) // QueryInterface 方法添加以下分支.
{
*ppvObject = static_cast<IShellBrowser*>(this);
return S_OK;
}
然后在Site
实现IShellBrowser
:
//IShellBrowser
virtual HRESULT STDMETHODCALLTYPE BrowseObject(
/* [in] */ PCUIDLIST_RELATIVE pidl,
/* [in] */ UINT wFlags) {
return E_NOTIMPL;
};
virtual HRESULT STDMETHODCALLTYPE GetWindow(
/* [out] */ HWND* phwnd) {
return E_NOTIMPL;
};
virtual HRESULT STDMETHODCALLTYPE ContextSensitiveHelp(
/* [in] */ BOOL fEnterMode) {
return E_NOTIMPL;
};
virtual HRESULT STDMETHODCALLTYPE InsertMenusSB(
/* [in] */ HMENU hmenuShared,
/* [out][in] */ LPOLEMENUGROUPWIDTHS pmgwReserved) {
return E_NOTIMPL;
}
virtual HRESULT STDMETHODCALLTYPE SetMenuSB(
/* [in] */ HMENU hmenuShared,
/* [in] */ HOLEMENU holemenuRes,
/* [in] */ HWND hwndActiveObject) {
return E_NOTIMPL;
}
virtual HRESULT STDMETHODCALLTYPE RemoveMenusSB(
/* [in] */ HMENU hmenuShared) {
return E_NOTIMPL;
};
virtual HRESULT STDMETHODCALLTYPE SetStatusTextSB(
/* [in] */ LPCWSTR pszStatusText) {
return E_NOTIMPL;
};
virtual HRESULT STDMETHODCALLTYPE EnableModelessSB(
/* [in] */ BOOL fEnable) {
return E_NOTIMPL;
};
virtual HRESULT STDMETHODCALLTYPE TranslateAcceleratorSB(
/* [in] */ MSG* pmsg,
/* [in] */ WORD wID) {
return E_NOTIMPL;
};
virtual HRESULT STDMETHODCALLTYPE OnViewWindowActive(
/* [in] */ IShellView* pshv) {
return E_NOTIMPL;
};
virtual HRESULT STDMETHODCALLTYPE GetViewStateStream(
/* [in] */ DWORD grfMode,
/* [out] */ IStream** ppStrm) {
return E_NOTIMPL;
}
修改 WM_COMMAND
:
case WM_COMMAND:
if (LOWORD(wParam) == buttonId)
{
// ... 之前的代码
if (cmd)
{
CComPtr<IShellBrowser> br;
if (SUCCEEDED(site.QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(&br)))) {
CComPtr<IOleCommandTarget> pct;
if (SUCCEEDED(br->QueryInterface(IID_PPV_ARGS(&pct)))) {
CMINVOKECOMMANDINFOEX cmi{};
cmi.cbSize = sizeof(CMINVOKECOMMANDINFOEX);
cmi.lpVerb = (LPSTR)MAKEINTRESOURCE(cmd - firstId);
cmi.nShow = SW_SHOWNORMAL;
cmi.ptInvoke = pt; //注意,这里设置 ptInvoke 。
cmi.fMask = CMIC_MASK_PTINVOKE;
pct->Exec(&CGID_ShellServiceObject, SHELLVERB_INVOKECOMMAND, 0, &cmi, nullptr); // 通过 IOleCommandTarget::Exec 执行。
}
}
else {
// 错误提示框。
MessageBox(hwnd, L"QueryService fail", L"error", MB_OK);
}
}
//... 后面的代码。
}
break;
注意: 如果此操作失败,并不能很清晰地知道失败的原因,可以通过返回值和 GetLastError 来检查错误, 或者进一步检查事件日志。
总结与建议
第二种方式(使用 IShellBrowser
和 IOleCommandTarget
)更推荐,因为它更接近 Explorer 的原生行为,兼容性和可靠性更高,且可以自动处理更多细节,无需手动解析注册表。不过,需要注意 IOleCommandTarget
的使用细节,例如上下文,参数的传递, 和可能产生的错误.
对于实际的开发,最好根据自身需求选择合适的方式,也可以将两者结合使用, 提供一定的容错性和扩展能力.