如何让Windows自绘菜单匹配标准外观?WM_MEASUREITEM宽度设置难题
我之前在做高DPI适配的自绘菜单时,也卡在这里好久!完全懂你想复刻标准菜单分栏式布局的需求——毕竟混合DPI下系统菜单的表现实在拉胯。下面是我踩了一堆坑后总结的靠谱解法:
1. 先提前拿到系统菜单的核心度量参数
要对齐标准菜单,不能自己瞎猜间距,得直接拿系统的预设值,而且必须考虑DPI!窗口初始化时先跑一遍这些:
- 菜单字体:用
SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof(NONCLIENTMETRICSW), &ncm, 0)获取,里面的lfMenuFont就是系统菜单用的字体,别用默认GUI字体,高DPI下会不准。 - 主文本与快捷键的间距:调用
GetSystemMetricsForDpi(SM_CXMENUSHORZSPACING, dpi),这个值就是系统用来分隔主文本和快捷键的固定分栏间距。 - 菜单边距:左右各有一个
SM_CXMENUEDGE的边距,所以总边距是这个值的两倍,同样用GetSystemMetricsForDpi取对应DPI下的值。 - 复选框/单选框尺寸:如果菜单里有勾选项,用
GetMenuCheckMarkDimensions()拿勾选框的宽度,再加上SM_CXMENUCHECK的间距(勾选框和文本之间的空隙)。
2. 先遍历所有菜单项,算出主文本的最大宽度
标准菜单的“分栏感”核心是所有主文本的右边缘对齐,所以必须先遍历当前菜单的所有项,用菜单字体算出每个主文本的宽度,记录最大值:
int CalcMaxItemTextWidth(HMENU hMenu, HFONT hMenuFont, int dpi) { int maxWidth = 0; HDC hdc = GetDC(NULL); HFONT oldFont = SelectObject(hdc, hMenuFont); // 给DC做DPI缩放,不然文本尺寸会和系统菜单不匹配 SetGraphicsMode(hdc, GM_ADVANCED); XFORM xform = {0}; xform.eM11 = static_cast<float>(dpi) / 96.0f; SetWorldTransform(hdc, &xform); int itemCount = GetMenuItemCount(hMenu); for (int i = 0; i < itemCount; i++) { wchar_t text[256] = {0}; GetMenuStringW(hMenu, i, text, ARRAYSIZE(text), MF_BYPOSITION); // 标准菜单用\t分隔主文本和快捷键,把它们拆分开 wchar_t* tabPos = wcschr(text, L'\t'); if (tabPos) *tabPos = L'\0'; SIZE textSize = {0}; GetTextExtentPoint32W(hdc, text, wcslen(text), &textSize); if (textSize.cx > maxWidth) maxWidth = textSize.cx; } SelectObject(hdc, oldFont); ReleaseDC(NULL, hdc); return maxWidth; }
这里一定要处理\t分隔符——系统菜单里的快捷键都是用这个标记和主文本分开的,拆出来才能单独计算宽度。
3. 在WM_MEASUREITEM里精确计算单个菜单项的宽度
拿到最大主文本宽度后,处理WM_MEASUREITEM时就可以精准算出每个项的总宽度了:
case WM_MEASUREITEM: { MEASUREITEMSTRUCT* mis = reinterpret_cast<MEASUREITEMSTRUCT*>(lParam); if (mis->CtlType == ODT_MENU) { HMENU hMenu = mis->hItem; int itemPos = mis->itemPosition; // 这里用之前提前算好的maxTextWidth、menuFont、spacing、dpi等值 wchar_t text[256] = {0}; GetMenuStringW(hMenu, itemPos, text, ARRAYSIZE(text), MF_BYPOSITION); wchar_t* tabPos = wcschr(text, L'\t'); wchar_t shortcutText[128] = {0}; if (tabPos) { wcscpy_s(shortcutText, tabPos + 1); *tabPos = L'\0'; } // 计算快捷键文本的宽度 HDC hdc = GetDC(NULL); HFONT oldFont = SelectObject(hdc, menuFont); SIZE shortcutSize = {0}; GetTextExtentPoint32W(hdc, shortcutText, wcslen(shortcutText), &shortcutSize); SelectObject(hdc, oldFont); ReleaseDC(NULL, hdc); // 总宽度公式:左右边距 + 勾选框宽度(如果有) + 最大主文本宽度 + 分栏间距 + 快捷键宽度 int edgeWidth = GetSystemMetricsForDpi(SM_CXMENUEDGE, dpi) * 2; int checkWidth = (mis->itemState & MFS_CHECKED) ? (GetMenuCheckMarkDimensions().cx + GetSystemMetricsForDpi(SM_CXMENUCHECK, dpi)) : 0; mis->itemWidth = edgeWidth + checkWidth + maxTextWidth + spacing + shortcutSize.cx; // 高度直接用系统菜单的默认高度 mis->itemHeight = GetSystemMetricsForDpi(SM_CYMENU, dpi); } return TRUE; }
最后提个高DPI的关键细节
如果是弹出菜单,一定要拿弹出菜单所在屏幕的DPI,而不是主窗口的!比如用GetDpiForWindow(GetParent(hMenu))或者MonitorFromWindow结合GetDpiForMonitor来获取,不然跨显示器显示时布局会歪。
这么处理完,你的自绘菜单宽度布局就会和标准菜单完全一致,快捷键的分栏效果也完美复刻,混合DPI的问题也解决了。
内容的提问来源于stack exchange,提问作者Uwe Raabe
相关产品推荐
相关产品推荐

