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

如何让Windows自绘菜单匹配标准外观?WM_MEASUREITEM宽度设置难题

解决Owner Draw Menu在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 11:42:57