使用 MFC 自繪選單通常需在框架類手動映射 WM_DRAWITEM、WM_MEASUREITEM,對話框專案尤覺麻煩。發現 BCMenu 庫無需映射即可自繪,研究發現關鍵在於 **將所有子選單建立為彈出式選單 (CreatePopupMenu)**,框架會自動對彈出選單觸發 MeasureItem/DrawItem 虛函數。本文展示 CMenuEx::LoadMenu 重構標準選單資源為全彈出式結構的完整代碼。
標準選單資源中,子選單為 MF_POPUP 旗標的項目。若在運行時將整個選單樹重建為 **每一層皆為獨立 HMENU (由 CreatePopupMenu 建立) 並關聯 CMenuEx 實例**,MFC 內部機制會自動對這些彈出選單調用虛函數,無需外部訊息映射轉發。
CMenuEx::LoadMenu 完整重構代碼BOOL CMenuEx::LoadMenu(UINT uMenu)
{
HMENU hMenu = ::CreateMenu(); // 建立頂層選單
Attach(hMenu); // 關聯至當前 CMenuEx
CMenu menuTemp; // 臨時載入原始資源
menuTemp.LoadMenu(uMenu);
for (int i = 0; i < (int)menuTemp.GetMenuItemCount(); ++i)
{
UINT uID = menuTemp.GetMenuItemID(i);
if (uID == 0) // 分隔符
{
::AppendMenu(hMenu, MF_SEPARATOR, 0, NULL);
}
else if ((int)uID == -1) // 子選單 (MF_POPUP)
{
CMenu* pSub = menuTemp.GetSubMenu(i);
HMENU hSub = ::CreatePopupMenu(); // 關鍵:建立獨立彈出選單句柄
CString strText; menuTemp.GetMenuString(i, strText, MF_BYPOSITION);
::InsertMenu(hMenu, i, MF_BYPOSITION | MF_POPUP | MF_STRING, (UINT)hSub, strText);
ChangeMenuStyle(pSub, hSub); // 遞迴處理子選單
}
else // 一般命令項
{
CString strText; menuTemp.GetMenuString(uID, strText, MF_BYCOMMAND);
AppendMenu(MF_STRING, uID, strText);
}
}
menuTemp.DestroyMenu();
return TRUE;
}
ChangeMenuStylevoid CMenuEx::ChangeMenuStyle(CMenu* pSrcMenu, HMENU hDestMenu)
{
// 關聯新 CMenuEx 實例以接收虛函數回調
CMenuEx* pNewMenu = new CMenuEx;
pNewMenu->Attach(hDestMenu);
m_SubMenuArr.Add(pNewMenu); // 成員陣列持有指標,防止過早釋放
for (int i = 0; i < pSrcMenu->GetMenuItemCount(); ++i)
{
UINT uID = pSrcMenu->GetMenuItemID(i);
MENUITEM* pItem = new MENUITEM; // 自定義結構體保存繪製所需資料
pItem->pImageList = &m_ImageList; // 共享圖像列表
if (uID == 0) // 分隔符
{
::AppendMenu(hDestMenu, MF_SEPARATOR, 0, NULL);
pItem->uID = 0; pItem->uIndex = -1;
::ModifyMenu(hDestMenu, i, MF_BYPOSITION | MF_OWNERDRAW, -1, (LPCTSTR)pItem);
}
else if (uID == (UINT)-1) // 子選單
{
CMenu* pSub = pSrcMenu->GetSubMenu(i);
HMENU hPop = ::CreatePopupMenu();
CString str; pSrcMenu->GetMenuString(i, str, MF_BYPOSITION);
::InsertMenu(hDestMenu, i, MF_BYPOSITION | MF_POPUP, (UINT)hPop, str);
pItem->uID = -1; pItem->strText = str; pItem->uIndex = -1;
::ModifyMenu(hDestMenu, i, MF_BYPOSITION | MF_OWNERDRAW, -1, (LPCTSTR)pItem);
ChangeMenuStyle(pSub, hPop); // 遞迴
}
else // 命令項
{
CString str; pSrcMenu->GetMenuString(uID, str, MF_BYCOMMAND);
pItem->uID = uID; pItem->strText = str; pItem->uIndex = -1;
UINT uState = pSrcMenu->GetMenuState(i, MF_BYPOSITION);
::AppendMenu(hDestMenu, MF_OWNERDRAW | MF_BYCOMMAND | uState, uID, (LPCTSTR)pItem);
}
m_MenuItemArr.Add(pItem); // 統一管理所有項目資料
}
}
| 技術點 | 說明 |
|---|---|
CreatePopupMenu vs CreateMenu | 頂層用 CreateMenu (水平列),所有子層用 CreatePopupMenu (垂直彈出),這是框架自動回調虛函數的前提。 |
MF_OWNERDRAW 旗標 | 每個項目 (含分隔符、彈出選單標題) 必須加上此旗標,框架才會嘗試呼叫 MeasureItem/DrawItem。 |
自定義 MENUITEM 結構 | 利用 AppendMenu/ModifyMenu 最後一個參數 LPCTSTR 傳遞 MENUITEM* 指標,在 DrawItem 中取回 (lpDIS->itemData),攜帶圖示索引、文字、圖像列表指標等繪製資料。 |
| 子選單實例管理 | m_SubMenuArr 陣列持有所有層級 CMenuEx*,確保生命週期覆蓋選單顯示期間,DestroyMenu 時統一清理。 |