自绘托盘子菜单时GetMenuString无法返回菜单项字符串的问题
问题
为应用程序托盘图标创建菜单时,给包含子菜单的菜单项设置OwnerDraw属性后,自绘菜单背景正常,但调用GetMenuString始终返回0,无法获取菜单项文本,导致所有项显示为硬编码的"Item"。
原绘制代码:
procedure DrawOpaMenuItem(Menu: HMENU; DC: HDC; itemID: Cardinal; itemRect: TRect; State: Cardinal); var Text: PWideChar; TxtLen: Integer; FNT: HFONT; BkBrush: HBRUSH; begin SetTextColor(DC, GetSysColor(COLOR_MENUTEXT)); SetBkMode(DC, TRANSPARENT); FNT := GetMenuFont; //A function that uses SystemParametersInfo with SPI_GETNONCLIENTMETRICS to create a font based on lfMenuFont SelectObject(DC, FNT); BkBrush := CreateSolidBrush(RGB($40,$F0,$FF)); if State and ODS_SELECTED = 1 then FillRect(DC, itemRect, GetSysColorBrush(COLOR_HIGHLIGHT)) else FillRect(DC, itemRect, BkBrush); itemRect.Left := itemRect.Left + 10; TxtLen := GetMenuString(Menu, itemID, nil, 0, MF_BYCOMMAND); //returns only 0!!! if TxtLen <> 0 then begin GetMenuString(Menu, itemID, Text, TxtLen + 1, MF_BYCOMMAND); DrawTextEx(DC, Text, -1, itemRect, DT_SINGLELINE or DT_VCENTER, nil); end else DrawTextEx(DC, PWideChar('Item'), -1, itemRect, DT_SINGLELINE or DT_VCENTER, nil); DeleteObject(BkBrush); DeleteObject(FNT); SetBkMode(DC, BKMODE_LAST); end;
WinProc调用代码:
function WinProc(hWnd: HWND; Msg: Cardinal; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall; begin case Msg of {...} WM_MEASUREITEM: begin with PMEASUREITEMSTRUCT(lParam)^ do begin itemWidth := 100; itemHeight := GetSystemMetrics(SM_CYMENU); end; end; WM_DRAWITEM: begin with PDRAWITEMSTRUCT(lParam)^ do begin DrawOpaMenuItem(hwndItem, hDC, itemID, rcItem, itemState); end; end; {...} end; {...} Result := DefWindowProc(hWnd, Msg, wParam, lParam); end;
解决方案
问题根源
- 子菜单项的
itemID是位置索引而非命令ID:在WM_DRAWITEM中,子菜单的itemID字段实际代表菜单项在子菜单中的位置索引(从0开始),但调用GetMenuString时使用了MF_BYCOMMAND标志(按命令ID查找),导致找不到对应项,返回0。 - 未分配文本缓冲区:
Text变量未分配内存,直接传递给GetMenuString会导致未定义行为。 - 错误的背景模式恢复:
BKMODE_LAST不是合法的SetBkMode参数,无需手动恢复DC的背景模式,系统会在绘制完成后处理。
修正后的代码
procedure DrawOpaMenuItem(Menu: HMENU; DC: HDC; ItemPos: Cardinal; itemRect: TRect; State: Cardinal); var Text: array[0..255] of WideChar; // 预分配足够的缓冲区 TxtLen: Integer; FNT: HFONT; OldFont: HFONT; BkBrush: HBRUSH; begin SetTextColor(DC, GetSysColor(COLOR_MENUTEXT)); SetBkMode(DC, TRANSPARENT); FNT := GetMenuFont; OldFont := SelectObject(DC, FNT); // 保存旧字体,避免资源泄漏 BkBrush := CreateSolidBrush(RGB($40,$F0,$FF)); if (State and ODS_SELECTED) <> 0 then FillRect(DC, itemRect, GetSysColorBrush(COLOR_HIGHLIGHT)) else FillRect(DC, itemRect, BkBrush); DeleteObject(BkBrush); // 及时释放画笔 itemRect.Left := itemRect.Left + 10; // 使用MF_BYPOSITION按位置索引获取菜单文本 TxtLen := GetMenuString(Menu, ItemPos, @Text[0], Length(Text), MF_BYPOSITION or MF_UNICODE); if TxtLen > 0 then DrawTextEx(DC, @Text[0], TxtLen, itemRect, DT_SINGLELINE or DT_VCENTER, nil) else DrawTextEx(DC, 'Item', -1, itemRect, DT_SINGLELINE or DT_VCENTER, nil); SelectObject(DC, OldFont); // 恢复旧字体 DeleteObject(FNT); // 释放自定义字体 end;
WinProc调用调整
在WM_DRAWITEM中,直接传递itemID作为位置索引即可(子菜单的itemID就是位置):
WM_DRAWITEM: begin with PDRAWITEMSTRUCT(lParam)^ do begin DrawOpaMenuItem(hwndItem, hDC, itemID, rcItem, itemState); end; end;
额外说明
- 顶层菜单项的
itemID是命令ID,此时需要用MF_BYCOMMAND,但子菜单必须用MF_BYPOSITION。 - 预分配缓冲区时,可根据实际需求调整大小,或者先调用
GetMenuString获取长度后动态分配内存。 - 操作GDI对象(如字体)时,必须保存旧对象并恢复,避免资源泄漏。
内容的提问来源于stack exchange,提问作者yonni
相关产品推荐
相关产品推荐

