You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确显示并调用Shell扩展的「新建」上下文菜单?

如何正确显示Shell扩展的「新建」菜单并正常执行命令

我使用TrackPopupMenuEx函数显示从Shell扩展获取的菜单,目标是文件夹右键背景菜单中的「新建」项(对应注册表项HKEY_CLASSES_ROOT\Directory\Background\shellex\ContextMenuHandlers\New)。目前已获取菜单,但存在以下问题:

  • 直接将QueryContextMenu得到的HMENU(hNewMenu)传入TrackPopupMenuEx时,「新建」项不显示,仅直接展示其子元素;
  • 显示的菜单项无法通过IContextMenu::InvokeCommand执行,但传入GetSubMenu(hNewMenu, 0)时可执行,但此时「新建」项不可见。

我尝试创建新的根菜单hRootMenu,添加「新建」根项并将GetSubMenu(hNewMenu, 0)的子项作为其子菜单,但仍失败,手动添加的「新建」项不显示,仍直接展示子菜单。

请问如何既能显示包含子项的「新建」菜单,又能让菜单项通过IContextMenu::InvokeCommand正常执行?

代码运行说明

  1. 将path变量替换为任意文件夹路径;
  2. 点击「Context menu」按钮显示上下文菜单。

问题原因与解决方案

核心问题

  1. NewMenu Shell扩展的QueryContextMenu方法会直接将「新建」的子项填充到传入的菜单中,而非生成包含「新建」父项的完整菜单;
  2. 手动添加父项后,命令ID的偏移计算错误,导致InvokeCommand无法匹配正确的命令;
  3. 菜单消息未正确传递给IContextMenu2/IContextMenu3接口,影响菜单的正常交互。

修改要点

  1. 创建顶层菜单,添加「新建」父项,并将QueryContextMenu生成的菜单作为其子菜单;
  2. 调整InvokeCommand的命令ID偏移,确保与QueryContextMenu的ID范围对应;
  3. 在窗口过程中,确保子菜单的消息能被IContextMenu2/IContextMenu3正确处理。

修改后的完整代码

#include <Windows.h>
#include <ShlObj.h>
#include <sstream>

HINSTANCE hInst;

IContextMenu2* cm2 = nullptr;
IContextMenu3* cm3 = nullptr;
const wchar_t* path = L"C:\\Users\\Username\\Desktop\\folder";

bool UpdateContextMenu(LPCONTEXTMENU icm1, void** ppContextMenu)
{
    *ppContextMenu = nullptr;

    if (icm1)
    {
        // 尝试获取更高版本的IContextMenu接口
        if (SUCCEEDED(icm1->QueryInterface(IID_IContextMenu3, ppContextMenu))) {
            cm3 = static_cast<IContextMenu3*>(*ppContextMenu);
        }
        else if (SUCCEEDED(icm1->QueryInterface(IID_IContextMenu2, ppContextMenu))) {
            cm2 = static_cast<IContextMenu2*>(*ppContextMenu);
        }

        if (*ppContextMenu) {
            icm1->Release(); // 释放旧接口
        }
        else {
            *ppContextMenu = icm1; // 没有更高版本则保留原接口
        }
    }
    else
        return false;

    return true;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
    // 优先让IContextMenu3/IContextMenu2处理菜单消息
    if (cm3) {
        LRESULT res;
        if (SUCCEEDED(cm3->HandleMenuMsg2(message, wParam, lParam, &res))) {
            return res;
        }
    }
    else if (cm2) {
        if (SUCCEEDED(cm2->HandleMenuMsg(message, wParam, lParam))) {
            return 0;
        }
    }

    switch (message) {
    case WM_CREATE: {
        CreateWindowA(
            "BUTTON", "Context menu",
            WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON,
            100, 100, 100, 30,
            hWnd, reinterpret_cast<HMENU>(1), hInst, nullptr);
        break;
    }
    case WM_COMMAND: {
        if (LOWORD(wParam) == 1) {
            LPITEMIDLIST pidl = nullptr;
            SFGAOF sfgao;
            if (SUCCEEDED(SHParseDisplayName(path, NULL, &pidl, 0, &sfgao))) {
                // 保留pidl供Shell扩展初始化使用,不能提前释放
                IContextMenu* outPcm = nullptr;
                const CLSID clsidNewMenu = {0xD969A300, 0xE7FF, 0x11d0, {0xA9, 0x3B, 0x00, 0xA0, 0xC9, 0x0F, 0x27, 0x19}};

                const UINT SCRATCH_QCM_FIRST = 1000; // 使用较大的起始ID避免冲突
                const UINT SCRATCH_QCM_LAST = 0x7FFF;

                IShellExtInit* shExtInit = nullptr;
                if (SUCCEEDED(CoCreateInstance(clsidNewMenu, NULL, CLSCTX_INPROC_SERVER, IID_IShellExtInit, reinterpret_cast<LPVOID*>(&shExtInit)))) {
                    if (SUCCEEDED(shExtInit->Initialize(pidl, NULL, NULL))) {
                        IContextMenu* cm = nullptr;
                        if (SUCCEEDED(shExtInit->QueryInterface(IID_IContextMenu, reinterpret_cast<void**>(&cm)))) {
                            UpdateContextMenu(cm, reinterpret_cast<void**>(&outPcm));

                            // 创建顶层菜单和「新建」子菜单容器
                            HMENU hRootMenu = CreatePopupMenu();
                            HMENU hNewSubMenu = CreatePopupMenu();

                            if (SUCCEEDED(outPcm->QueryContextMenu(hNewSubMenu, 0, SCRATCH_QCM_FIRST, SCRATCH_QCM_LAST, CMF_NORMAL))) {
                                // 将「新建」项添加到顶层菜单,并关联子菜单
                                AppendMenuW(hRootMenu, MF_POPUP | MF_STRING, reinterpret_cast<UINT_PTR>(hNewSubMenu), L"新建");

                                // 弹出菜单并获取选中的命令ID
                                int idCmd = TrackPopupMenuEx(hRootMenu, TPM_RETURNCMD | TPM_RIGHTBUTTON, 100, 100, hWnd, NULL);
                                if (idCmd >= SCRATCH_QCM_FIRST && idCmd <= SCRATCH_QCM_LAST) {
                                    CMINVOKECOMMANDINFO cmi = {0};
                                    cmi.cbSize = sizeof(CMINVOKECOMMANDINFO);
                                    cmi.lpVerb = reinterpret_cast<LPSTR>(MAKEINTRESOURCE(idCmd - SCRATCH_QCM_FIRST));
                                    cmi.nShow = SW_SHOWNORMAL;
                                    cmi.hwnd = hWnd;

                                    HRESULT hr = outPcm->InvokeCommand(&cmi);
                                    if (!SUCCEEDED(hr)) {
                                        std::stringstream ss;
                                        ss << "InvokeCommand失败: GetLastError() = " << GetLastError() << ", HRESULT = " << hr;
                                        MessageBoxA(NULL, ss.str().c_str(), "错误", MB_ICONERROR);
                                    }
                                }

                                DestroyMenu(hNewSubMenu);
                            }
                            DestroyMenu(hRootMenu);
                            outPcm->Release();
                        }
                    }
                    shExtInit->Release();
                }
                CoTaskMemFree(pidl); // 释放pidl
            }
            cm2 = nullptr;
            cm3 = nullptr;
        }
        break;
    }
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    if (FAILED(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED))) {
        return 1;
    }
    hInst = hInstance;

    WNDCLASSA wc = {};
    wc.lpfnWndProc = WndProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = "MYWINDOWCLASS";
    wc.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1);

    if (!RegisterClassA(&wc)) {
        MessageBoxA(NULL, "窗口类注册失败", "错误", MB_ICONERROR);
        CoUninitialize();
        return 1;
    }

    HWND hWnd = CreateWindowExA(
        0, "MYWINDOWCLASS", "Shell菜单测试", WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 400, 300,
        nullptr, nullptr, hInstance, nullptr
    );

    if (!hWnd) {
        MessageBoxA(NULL, "窗口创建失败", "错误", MB_ICONERROR);
        CoUninitialize();
        return 1;
    }

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    MSG msg;
    while (GetMessage(&msg, nullptr, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    CoUninitialize();
    return static_cast<int>(msg.wParam);
}

内容的提问来源于stack exchange,提问作者Joe J

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 04:52:05