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

IContextMenu::QueryContextMenu返回超出ID范围菜单项的原因咨询

为什么IContextMenu::QueryContextMenu传入ID范围后仍出现超出范围的菜单项?

调用IContextMenu::QueryContextMenu时,我们传入了idCmdFirst=1和idCmdLast=5参数,预期菜单项ID应处于[1,5]范围内,但测试文本文件的右键菜单时,却出现了超过5个且ID超出该范围的项,这是为什么?

代码运行步骤

  • 将path变量的值替换为任意文本文件的路径
  • 点击“Show Context menu”按钮以显示右键菜单

测试代码

#include <windows.h>
#include <shlobj.h>
#include <atlbase.h>
#include <strmif.h>

LPCWSTR path = L"C:\\Users\\Username\\file.txt";

IContextMenu2* _cm2 = nullptr;
IContextMenu3* _cm3 = nullptr;

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    if (_cm3)
    {
        LRESULT lr = 0;
        if (SUCCEEDED(_cm3->HandleMenuMsg2(message, wParam, lParam, &lr)))
            return lr;
    }
    else if (_cm2)
    {
        if (SUCCEEDED(_cm2->HandleMenuMsg(message, wParam, lParam)))
            return 0;
    }

    const int buttonId = 1;
    const int buttonX = 10;
    const int buttonY = 10;
    switch (message)
    {
    case WM_CREATE:
        CreateWindow(L"BUTTON", L"Show Context menu", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON, buttonX, buttonY, 200, 30, hwnd, (HMENU)(long)buttonId, nullptr, nullptr);
        break;

    case WM_COMMAND:
        if (LOWORD(wParam) == buttonId)
        {
            IShellItem* item;
            IBindCtx* ctx;
            CreateBindCtx(0, &ctx); // 使用绑定上下文避免Shell自动获取错误项
            SHCreateItemFromParsingName(path, ctx, IID_PPV_ARGS(&item));
            ctx->Release();
            if (item)
            {
                IContextMenu* cm;
                item->BindToHandler(nullptr, BHID_SFUIObject, IID_PPV_ARGS(&cm));
                if (cm)
                {
                    if (FAILED(cm->QueryInterface(&_cm3)))
                    {
                        cm->QueryInterface(&_cm2);
                    }


                    auto menu = CreatePopupMenu();
                    const int firstId = 1;
                    const int lastId = 5;
                    cm->QueryContextMenu(menu, 0, firstId, lastId, CMF_NORMAL); // 按需移除不必要的标志
                    POINT pt{ buttonX, buttonY };
                    ClientToScreen(hwnd, &pt);
                    auto cmd = TrackPopupMenu(menu, TPM_RETURNCMD, pt.x, pt.y, 0, hwnd, nullptr);
                    if (cmd)
                    {
                        CMINVOKECOMMANDINFO cmi{};
                        cmi.cbSize = sizeof(CMINVOKECOMMANDINFO);
                        cmi.lpVerb = (LPSTR)MAKEINTRESOURCE(cmd - firstId);
                        cmi.nShow = SW_SHOWNORMAL;
                        cm->InvokeCommand(&cmi);
                    }


                    cm->Release();
                    DestroyMenu(menu);

                    if (_cm2)
                    {
                        _cm2->Release();
                        _cm2 = nullptr;
                    }

                    if (_cm3)
                    {
                        _cm3->Release();
                        _cm3 = nullptr;
                    }
                }
                item->Release();
            }
        }
        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)
{
    auto hmod = LoadLibrary(L"shell32.dll");
    auto fileIconInit = (BOOL(WINAPI*)(BOOL))GetProcAddress(hmod, MAKEINTRESOURCEA(660));
    if (fileIconInit)
    {
        fileIconInit(TRUE);
    }

    WNDCLASS wc = {};
    wc.lpfnWndProc = WndProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = L"MyWindowClass";
    RegisterClass(&wc);

    auto hwnd = CreateWindow(wc.lpszClassName, L"Context Menu", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, hInstance, nullptr);
    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

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

问题原因及解决方案

核心原因

IContextMenu的实现(比如Shell系统组件或第三方扩展)不受idCmdFirst和idCmdLast范围的完全限制:

  1. 系统标准菜单项:Windows Shell会自动插入系统级默认菜单项(如“打开”“编辑”“属性”等),这些项使用小于idCmdFirst的负ID值,不属于你指定的[1,5]范围。
  2. CMF_NORMAL标志的影响:你使用的CMF_NORMAL标志会让Shell加载所有类型的菜单项,包括系统默认项、所有已注册的第三方Shell扩展项,这些扩展可能会在你指定的ID范围之外添加项,甚至忽略idCmdLast的限制。

关键细节补充

  • QueryContextMenu的返回值是实际使用的最大命令ID加1,你没有捕获这个值,所以无法确认扩展实际占用的ID范围。正确用法应该是:
    HRESULT hr = cm->QueryContextMenu(menu, 0, firstId, lastId, CMF_NORMAL);
    if (SUCCEEDED(hr)) {
        UINT actualMaxId = LOWORD(hr) - 1;
        // 实际扩展项的ID范围是 [firstId, actualMaxId]
    }
    
  • 你的InvokeCommand调用存在问题:对于系统默认的负ID项,不能用cmd - firstId来生成verb,需要单独判断——只有当cmd >= firstId时,才是你分配范围里的扩展项,负ID项需要直接调用系统对应的命令逻辑。

可选调整方案

  • 如果只想加载自定义扩展项,不想显示系统默认项,可以将CMF_NORMAL替换为CMF_EXCLUDEDEFAULT标志。
  • 若要严格限制扩展项的ID范围,需要在调用QueryContextMenu后,手动检查并移除超出[firstId, lastId]范围的菜单项(但不建议这么做,可能破坏Shell菜单的正常逻辑)。

内容的提问来源于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 01:53:14