在 VC++ 開發中,自繪菜單一直是打造專業級界面的關鍵技術。早期開發者常參考《自繪菜單的實現》(作者 querw),但在實際專案中會發現該方案存在諸多缺陷:無法處理 MDI 架構下系統自動添加的菜單(如「最近使用檔案」列表)、對 WM_DRAWITEM 與 WM_MEASUREITEM 訊息映射的理解有誤、子菜單高度計算採用「欺騙 MFC」的 Hack 手段等。本文將深入剖析 MFC 菜單繪製的核心機制,並提供一個健壯的 CMenuEx 實現方案。
原類別未處理 CMainFrame::OnInitPopupMenu(),導致無法攔截系統動態插入的菜單項(如 MRU 列表)。BCMenu 雖支援此功能,但其原理常被誤解。
作者聲稱 BCMenu 不需映射 WM_DRAWITEM 和 WM_MEASUREITEM 即可自繪,這是錯誤的。實際上,MFC 框架會自動呼叫 CMenu 的虛擬函數 MeasureItem() 和 DrawItem()。當 CMenuEx 繼承 CMenu 並覆寫這兩個函數時,基類指標指向派生類物件,多型機制自然觸發自繪邏輯——這才是 C++ 虛擬函數的威力所在。
參考 WINCORE.CPP 中 CWnd::OnMeasureItem 的原始碼可見:框架先取得主選單指標(CMenu*),再透過 _AfxFindPopupMenuFromID 定位目標彈出選單,最後呼叫 pMenu->MeasureItem()。同理,DrawItem 亦同樣流程。
原程式將分隔符的 Command ID 從 0 改為 -1,企圖欺騙 MFC 呼叫 MeasureItem() 計算子菜單高度。但 ID 為 -1 的項目永遠不會被 OnMeasureItem 找到,導致無分隔符時子菜單高度不可調。這是促使重寫類別的主因之一。
CMenuEx 中實作 MeasureItem 與 DrawItem,完全依賴 MFC 多型機制,無需手動映射訊息。OnInitMenuPopup,對系統自動生成的子菜單(如 MRU)進行子類別化或重新繪製。DrawItem 中使用 CDC::DrawState 或 GDI+ 繪製 32 位圖示、漸變背景、懸停特效等。// CMenuEx.h
class CMenuEx : public CMenu {
public:
virtual void MeasureItem(LPMEASUREITEMSTRUCT lpMIS);
virtual void DrawItem(LPDRAWITEMSTRUCT lpDIS);
// 輔助:計算項目高度、寬度
int CalcItemHeight(UINT nID);
int CalcItemWidth(UINT nID, CDC* pDC);
};
// CMenuEx.cpp
void CMenuEx::MeasureItem(LPMEASUREITEMSTRUCT lpMIS) {
// 統一計算高度:文字、圖示、邊距
lpMIS->itemHeight = CalcItemHeight(lpMIS->itemID);
lpMIS->itemWidth = CalcItemWidth(lpMIS->itemID, CDC::FromHandle(lpMIS->hDC));
}
void CMenuEx::DrawItem(LPDRAWITEMSTRUCT lpDIS) {
CDC dc; dc.Attach(lpDIS->hDC);
CRect rc(lpDIS->rcItem);
// 繪製背景、圖示、文字、快捷鍵、子菜單箭頭
// 根據 lpDIS->itemState 处理选中、禁用、检查状态
dc.Detach();
}



真正的自繪菜單不應依賴 Hack,而應建立在對 MFC 訊息路由與虛擬函數機制的深刻理解之上。透過正確繼承 CMenu 並覆寫核心虛擬函數,配合主框架對動態菜單的攔截,即可實現穩定、高效、真彩色的專業級菜單系統。