【问题标题】:How do I cleanly avoid creating a context menu inside a context menu?如何完全避免在上下文菜单中创建上下文菜单?
【发布时间】:2022-01-03 21:03:18
【问题描述】:

我正在向我的窗口添加一个上下文菜单,我似乎发现了 Win32 的一个怪癖:因为弹出菜单将鼠标消息转发到它的所有者窗口,而 WM_RBUTTONUP 的默认行为是发送 WM_CONTEXTMENU,如果你打开WM_CONTEXTMENU 处理程序中的弹出菜单,您可以右键单击该菜单以打开另一个菜单。这会失败,因为菜单已经打开。

这似乎是一个非常明显的常见错误,但even the official WM_CONTEXTMENU guide from Microsoft 甚至没有提及它。其他教程也没有。

那么,尝试在另一个菜单中打开弹出菜单的最干净的方法是什么?

当然,可以简单地添加一个全局标志来指示菜单已打开,但这并不干净。也可以忽略来自TrackPopupMenuEx 的错误,但这也不干净。我无法想象 Win32(尽管如此可怕)会鼓励程序员做这样的事情。


我研究了一些常见控件的行为方式:

  • 编辑控件不会将 WM_RBUTTONUP 传递给 DefWindowProc,除非它收到以前的 WM_RBUTTONDOWN。因为菜单系统在内部处理 WM_RBUTTONDOWN 而不会调度它,这意味着它不会在打开菜单时将 WM_RBUTTONUP 传递给 DefWindowProc。
  • Microsoft 管理控制台中的树形视图会创建一个临时窗口来关联菜单。
  • 任务管理器中的详细信息列表视图确实接收到递归 WM_CONTEXTMENU;我假设它尝试创建菜单并忽略错误。

这个测试程序演示了这个现象:

#include <Windows.h>
#include <iostream>

static LRESULT CALLBACK window_proc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
    switch(msg) {
    case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hwnd, &ps);
            EndPaint(hwnd, &ps);
        }
        return 0;
    case WM_RBUTTONDOWN:
        std::cout << "got WM_RBUTTONDOWN\n";
        return DefWindowProc(hwnd, msg, wParam, lParam);
    case WM_RBUTTONUP:
        std::cout << "got WM_RBUTTONUP\n";
        return DefWindowProc(hwnd, msg, wParam, lParam);
    case WM_CONTEXTMENU:
        std::cout << "got WM_CONTEXTMENU\n";
        {
            HMENU hm = CreatePopupMenu();
            AppendMenu(hm, MF_STRING, 1234, L"Hello");
            AppendMenu(hm, MF_STRING, 4321, L"world");
            POINT pt;
            GetCursorPos(&pt);
            SetLastError(0);
            int res = TrackPopupMenuEx(hm, TPM_LEFTALIGN | TPM_TOPALIGN | TPM_NONOTIFY | TPM_RETURNCMD, pt.x, pt.y, hwnd, NULL);
            int err = GetLastError();
            std::cout << "TrackPopupMenuEx returned " << res << ", error " << err << std::endl;
            SetLastError(0);
        }
        return 0;
    default:
        return DefWindowProc(hwnd, msg, wParam, lParam);
    }
}

int main()
{
    WNDCLASSEX wcx = {0};
    wcx.cbSize = sizeof wcx;
    wcx.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
    wcx.hCursor = LoadCursor(NULL, IDC_ARROW);
    wcx.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wcx.hInstance = (HINSTANCE)GetModuleHandle(NULL);
    wcx.lpfnWndProc = window_proc;
    wcx.lpszClassName = L"TestWindowClass";
    RegisterClassEx(&wcx);
    CreateWindowEx(0, wcx.lpszClassName, L"Popup menu message test", WS_VISIBLE | WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 300, 300, NULL, NULL, wcx.hInstance, NULL);
    
    MSG msg;
    while(GetMessage(&msg, NULL, 0, 0) > 0) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return 0;
}

【问题讨论】:

  • 所以当你右键单击时没有任何反应。正如预期的那样,失败是一种特征。
  • 如果您提供标志TPM_RIGHTBUTTON,那么右键将用于选择菜单项,而不是启动嵌套的WM_CONTEXTMENU 消息。
  • @HansPassant 当然,在显示第二个上下文菜单时可以忽略该错误。那不是很干净。有没有替代方法?
  • 菜单不应该按照您描述的方式递归,除非您通过 TPM_RECURSE 标志明确要求该行为并处理 WM_MENURBUTTONUP 消息
  • 与问题无关但GetCursorPos 错误,LPARAM 告诉您正确的位置。 (鼠标可以在点击后但在收到消息之前移动)

标签: c++ winapi


【解决方案1】:

来自documentation

TrackPopupMenuEx

... 如果在 fuFlags 参数中指定 TPM_RETURNCMD,则返回 value 是用户选择的项目的菜单项标识符。 如果用户在没有选择的情况下取消了菜单,或者 如果 发生错误,返回值为零。

如果弹出菜单已经打开并且用户右键单击它,或者用户取消了菜单,函数返回FALSE。无论哪种情况,您都不想处理结果,因此只需立即返回。如果res 不为零,则处理它。

BOOL res = TrackPopupMenuEx(hm, 
    TPM_LEFTALIGN | TPM_TOPALIGN | TPM_NONOTIFY | TPM_RETURNCMD,
    pt.x, pt.y, hwnd, NULL);
if (!res) return 0;
std::cout << "Menu selection: " << res << "\n";

或者您可以使用GetLastError 查找失败的原因。是因为用户取消了,右键菜单,还是其他原因

if (!res)
{
    int err = GetLastError();
    if (err != 0 && err != ERROR_POPUP_ALREADY_ACTIVE)
        std::cout << "Unexpected error " << err << "\n";
    return 0;
}

【讨论】:

  • 问题是用一种简洁的方式来编写它,而不会忽略错误。如果真的没有比忽略错误更好的方法,我可能会接受这个答案。
【解决方案2】:

我想TrackPopupMenu 是没有人检查错误的函数之一。毕竟,只要输入参数有效,只有在资源不足(内存或 gdi/用户句柄)时才会失败(在正常的非递归情况下)。如果它失败了,MessageBox 可能也会失败,所以你甚至无法通知用户。

正常的编码模式是只检查您的菜单命令 ID,而忽略其他所有内容。

在 IE 4/5 时代添加的 TPM_RECURSE 标志允许您将弹出菜单放在弹出菜单上,但从来没有人这样做过。

【讨论】:

    猜你喜欢
    • 2014-07-27
    • 2016-08-19
    • 1970-01-01
    • 2011-09-30
    • 2017-05-13
    • 1970-01-01
    • 2011-06-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多