GetSystemMenu() 可能损坏其他进程的窗口菜单
通过 Windows API GetSystemMenu 获取窗口系统菜单(即窗口标题栏右键菜单)句柄,可以实现自定义窗口菜单的功能。但若获取的窗口菜单句柄来自其他进程的窗口,便会引发问题。
下面的代码展示了自定义 Windows 画图程序的窗口菜单:
c++#include <iostream>
#include <windows.h>
int main()
{
std::cout << "按任意键自定义画图窗口菜单..." << std::endl;
std::cin.get();
auto hWnd = FindWindowW(L"MSPaintApp", NULL);
if (hWnd == NULL) {
std::cerr << "未找到画图窗口!请先打开画图程序。" << std::endl;
return 1;
}
auto hMenu = GetSystemMenu(hWnd, FALSE);
if (hMenu == NULL) {
std::cerr << "获取窗口菜单失败!" << std::endl;
return 1;
}
std::cout << "菜单句柄值: " << hMenu << std::endl;
if (!IsMenu(hMenu)) {
std::cerr << "菜单句柄无效" << std::endl;
return 1;
}
AppendMenuW(hMenu, MF_STRING, 0x1001, L"自定义菜单项");
std::cout << "按任意键移除菜单项..." << std::endl;
std::cin.get();
RemoveMenu(hMenu, 0x1001, MF_BYCOMMAND);
std::cout << "按任意键退出程序..." << std::endl;
std::cin.get();
}
首先运行 Windows 自带的画图程序,然后编译并运行上面的示例程序。第一次运行时,该程序工作正常。待该程序退出再次运行它,便会得到错误提示:菜单句柄无效。另外,通过观察画图程序的窗口菜单,也可以发现在示例程序退出后,菜单样式发生了改变。

图1:示例程序运行前的画图窗口菜单

图2:示例程序退出后的画图窗口菜单
微软官方文档中对此现象并没有特别说明。不过我猜测原因如下:虽然文档中明确说 GetSystemMenu()
获取的是窗口菜单的副本,但是实际上该窗口菜单的所有权转移到了调用此 API 的进程,也就是示例程序进程。当示例程序退出后,该菜单对象便被销毁了。第二次运行示例程序,GetSystemMenu()
获取到的仍是之前的窗口句柄(两次运行输出的菜单句柄值一致),因此 IsMenu()
检测判断该句柄无效。图 1 中的窗口菜单其实是窗口管理器创建的应用了主题样式的菜单,而图 2 才是真正的窗口默认菜单,只是 GetSystemMenu()
无法获取到它的句柄。
解决方案有两种:
- 在示例程序退出前调用
GetSystemMenu(hWnd, TRUE)
重置窗口菜单。不过某些程序会对窗口菜单进行自定义,比如 Edge 浏览器,该方法会让自定义菜单项失效。 - 使用
SetWindowsHookEx()
创建钩子,将 DLL 注入到目标进程中,在目标进程中调用GetSystemMenu()
,避免发生菜单所有权转移。
在本篇发布后不久,我发现了一篇 Blog 文章,其作者是微软官方的开发人员 Raymond Chen。该文章部分证实了我之前的猜测,因此不再对原文做修改,而是将新内容补充在文末。
Every window with the WS_SYSMENU style has a system menu, but it’s not there until it needs to be
I mentioned last time that there’s an optimization in the treatment of the system menu which significantly reduces the number of menus in the system. When a window has the
WS_SYSMENU
window style, it has a system menu, but until somebody callsGetSystemMenu
on that window, nobody knows what its menu handle is. Until that point, the window manager doesn’t actually have to commit to creating a menu for the window; it can just pretend that the window has one. (This technique goes by the fancy name lazy initialization.) The window manager creates a global default system menu which contains the standard system menu items. If somebody presses Alt+Space or otherwise calls up the system menu for a window that has never hadGetSystemMenu
called on it, the window manager just uses the global default system menu, since it knows that nobody has customized the menu. (You can’t customize a menu you don’t have the handle to!) Since most people never customize their system menu, this optimization avoids cluttering the desktop heap with identical copies of the same menu. This was a particularly important optimization back in the 16-bit days, when all window manager objects had to fit into a single 64KB heap (known as System Resources). If you are really sneaky, you can catch a glimpse of the elusive global default system menu as it whizzes by: As with any other popup menu, the handle to the menu being displayed is passed to your window’sWM_INITMENUPOPUP
, and if your program has never calledGetSystemMenu
, the handle that you will see is the global default system menu. Mind you, you can’t do much to this menu, since the window manager blocks any attempt to modify it. (Otherwise, your program’s menu modification would have an unintended effect on the menus of other programs!)Therefore, if your program is in the habit of modifying its system menu in its
WM_INITMENUPOPUP
handler, you should stick a dummy call toGetSystemMenu
in yourWM_CREATE
handler to force your system menu to change from a pretend system menu to a real one.
Windows 窗口管理器不会立即为每个具有 WS_SYSMENU
风格的窗口创建窗口菜单,而是共用一个全局的默认窗口菜单,也就是上文中图 2 显示的那个菜单,由于某种原因,uxtheme 视觉样式没有被应用在该菜单上,所以看上去样式不一致。直到对该窗口调用 GetSystemMenu()
,这表示该窗口可能需要有一个自定义窗口菜单,此时窗口管理器才会为该窗口创建一个独立的窗口菜单。
新建窗口菜单的所有权属于第一次对该窗口调用 GetSystemMenu()
的进程。这也解释了,为什么 Edge 浏览器(或其他拥有自定义系统菜单的窗口)的系统菜单不受示例程序的影响——因为这些窗口的所属进程已经调用过 GetSystemMenu()
了。
根据上述原理,我们可以用通过判断窗口菜单是否为全局默认窗口菜单来决定是否重置窗口菜单——如果是默认窗口菜单,则重置;如果是自定义窗口菜单,则无需处理。具体实现代码如下:
c++bool IsDefaultSystemMenu(HMENU hMenu)
{
UINT defaultIDs[] = {
SC_RESTORE,
SC_MOVE,
SC_SIZE,
SC_MINIMIZE,
SC_MAXIMIZE,
0,
SC_CLOSE
};
auto itemCount = GetMenuItemCount(hMenu);
if (itemCount != sizeof(defaultIDs)) return false;
for (auto i = 0; i < itemCount; i++) {
auto itemID = GetMenuItemID(hMenu, i);
if (itemID != defaultIDs[i]) return false;
}
return true;
}
/**
* 进程退出前调用下面的代码
* 如果是默认窗口菜单则重置
*/
if (IsDefaultSystemMenu(hMenu)) {
GetSystemMenu(hWnd, TRUE);
}