Win32开发如何干净避免上下文菜单内二次触发新菜单弹出
解决方案
方案1:模仿系统编辑控件实现(最稳妥合规)
这是系统标准编辑控件已经大规模验证的实现逻辑,不需要全局变量,也不需要忽略API错误,完全符合Win32设计规范。
实现逻辑:
- 为每个窗口实例绑定一个布尔标记,记录是否收到过
WM_RBUTTONDOWN消息,标记可以存储在窗口额外内存(通过WNDCLASSEX的cbWndExtra字段预留),或者通过SetProp/GetProp动态绑定到窗口句柄,仅对当前窗口实例生效,无全局污染。 - 消息处理规则:
- 收到
WM_RBUTTONDOWN时,将标记设为TRUE,调用DefWindowProc正常处理 - 收到
WM_RBUTTONUP时,仅当标记为TRUE时才调用DefWindowProc(会触发后续的WM_CONTEXTMENU消息),调用完成后将标记重置为FALSE;如果标记为FALSE直接返回,不会触发上下文菜单 - 弹出菜单显示时,所有鼠标消息会被菜单系统内部捕获处理,不会分发到所属窗口,因此菜单打开时右键点击不会触发所属窗口的
WM_RBUTTONDOWN,标记始终为FALSE,自然不会触发重复的菜单打开请求。
- 收到
修改后的可运行代码如下(同时修复了原示例的菜单资源泄漏问题):
#include <Windows.h> #include <iostream> static LRESULT CALLBACK window_proc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { // 用窗口属性存储右键按下标记,避免全局变量 const wchar_t* PROP_RBUTTON_DOWN = L"RButtonDownFlag"; 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"; SetProp(hwnd, PROP_RBUTTON_DOWN, (HANDLE)TRUE); return DefWindowProc(hwnd, msg, wParam, lParam); case WM_RBUTTONUP: std::cout << "got WM_RBUTTONUP\n"; if (GetProp(hwnd, PROP_RBUTTON_DOWN) == (HANDLE)TRUE) { RemoveProp(hwnd, PROP_RBUTTON_DOWN); return DefWindowProc(hwnd, msg, wParam, lParam); } // 没有收到过对应的右键按下,直接返回,不触发上下文菜单 return 0; 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; DestroyMenu(hm); SetLastError(0); } return 0; case WM_DESTROY: PostQuitMessage(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; }
方案2:极简实现(无需额外标记)
如果接受对合法错误做特殊处理,实际生产环境中包括任务管理器在内的很多微软官方组件都采用该方案:在调用TrackPopupMenuEx后判断返回值为0且错误码为ERROR_POPUP_ALREADY_ACTIVE(1813)时直接忽略即可,代码改动最小,逻辑简单易维护。
内容的提问来源于stack exchange,提问作者Stack Exchange Broke The Law
相关产品推荐
相关产品推荐

