返回

解决注册表右键菜单失效(HKEY_CLASSES_ROOT)

windows

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::QueryContextMenuInvokeCommand 方式调用, 缺少了这种必要的上下文环境。 简单来说,就是 Explorer 并不知道该如何去处理这些菜单项。

例如,考虑注册表路径下的“新建”子菜单。 当你在资源管理器的背景中单击鼠标右键并选择“新建”时,Explorer.exe 会解析注册表,加载必要的组件并最终执行命令。如果你的代码中没有 Explorer 进程,那么即使运行了命令也执行不成功。

解决方案

以下是几种解决这个问题的可行方法。

方法一:使用 IContextMenu::GetCommandString 获取命令并手动执行

这种方法的核心思想是,绕过 InvokeCommand,而是直接获取菜单项对应的命令字符串,然后自己来执行这个命令。

  1. 获取命令字符串: 使用 IContextMenu::GetCommandString 函数,并指定 GCS_VERBWGCS_VERBA 标志,获取菜单项的谓词(verb)或命令字符串。
  2. 检查谓词/命令字符串: 将获取到的谓词/命令字符串与 HKEY_CLASSES_ROOT\Directory\Background\shell 下注册的命令进行比较。
  3. 手动执行命令: 如果谓词/命令字符串匹配,则从注册表读取对应的命令,并使用 ShellExecuteCreateProcess 等函数手动执行。

代码示例(修改 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 的释放,确保代码的稳定性。

方法二:通过 IShellBrowserIOleCommandTarget 模拟 Explorer 上下文

这种方法更加“正统”,它通过获取 IShellBrowser 接口,并进一步获取 IOleCommandTarget 接口,来模拟 Explorer 的上下文环境,然后通过 IOleCommandTarget::Exec 来执行命令。

  1. 获取 IShellBrowser 通过 IUnknown::QueryService 接口 (在 Site 类中) 获取服务 SID_STopLevelBrowser, IID_IShellBrowser, 即可拿到 IShellBrowser
  2. 获取 IOleCommandTarget 通过 IShellBrowser::QueryInterface 获取 IOleCommandTarget 接口。
  3. 构建 CMINVOKECOMMANDINFOEX 创建 CMINVOKECOMMANDINFOEX 结构体,并设置必要的参数,包括 lpVerb(菜单项的命令ID)和 ptInvoke(点击坐标)。注意 cbSize 要设置成 CMINVOKECOMMANDINFOEX 的大小。
  4. 执行命令: 调用 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 来检查错误, 或者进一步检查事件日志。

总结与建议

第二种方式(使用 IShellBrowserIOleCommandTarget)更推荐,因为它更接近 Explorer 的原生行为,兼容性和可靠性更高,且可以自动处理更多细节,无需手动解析注册表。不过,需要注意 IOleCommandTarget 的使用细节,例如上下文,参数的传递, 和可能产生的错误.

对于实际的开发,最好根据自身需求选择合适的方式,也可以将两者结合使用, 提供一定的容错性和扩展能力.