網路上許多 MFC 自繪選單教學都要求派生新類別,其實大材小用。利用 MFC 內建的 Owner-Draw 機制,只需在 CMainFrame 中處理三個核心訊息,貼上三個函數,即可立刻擁有漂亮的自繪選單。本文提供開箱即用的完整代碼,並解析關鍵結構體用法。
若選單項擁有 MF_OWNERDRAW 標誌,MFC 框架會自動呼叫:
OnMeasureItem:決定選單項寬高OnDrawItem:繪製選單項外觀 (背景、文字、圖示、分隔線)我們只需在 OnInitMenu 中為所有選單項加上此標誌即可啟動自繪流程。
OnMeasureItem:精確控制尺寸void CMainFrame::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{
LPMEASUREITEMSTRUCT& lpM = lpMeasureItemStruct;
if (lpM->CtlType == ODT_MENU) // 確認為選單自繪
{
if (lpM->itemID != ID_SEPARATOR) // 一般選單項
{
lpM->itemHeight = 20;
lpM->itemWidth = 150;
}
else // 分隔線
{
lpM->itemHeight = 1; // 極細分隔線
lpM->itemWidth = 150;
}
}
}
OnDrawItem:繪製視覺風格 (漸層背景、選中態、失效態、文字置中)void CMainFrame::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct)
{
LPDRAWITEMSTRUCT& lpD = lpDrawItemStruct;
if (nIDCtl == 0) // 0 代表選單 (按鈕自繪也會進來,需區分)
{
CDC* pDC = CDC::FromHandle(lpD->hDC);
pDC->SetBkMode(TRANSPARENT);
CMenu menu;
menu.Attach((HMENU)lpD->hwndItem); // 取得選單物件
CRect rcItem(lpD->rcItem); // 當前項目矩形
CRect rcLeft(rcItem.left, rcItem.top, 20, rcItem.bottom); // 左側圖示區 (寬 20px)
// 定義三種狀態筆刷與文字顏色
CBrush brNormal(RGB(250, 250, 250)); // 一般背景
CBrush brLeft(RGB(230, 230, 230)); // 左側欄背景
CBrush brSel(RGB(148, 170, 214)); // 選中藍
CString strText;
menu.GetMenuString(lpD->itemID, strText, MF_BYCOMMAND);
if (lpD->itemID != ID_SEPARATOR) // 一般項目繪製
{
// --- 選中/高亮狀態 ---
if (lpD->itemAction & ODA_SELECT)
{
pDC->FillRect(rcItem, &brSel);
if (lpD->itemState & ODS_GRAYED) pDC->SetTextColor(RGB(194, 194, 194)); // 失效灰
else if (lpD->itemState & ODS_SELECTED) pDC->SetTextColor(RGB(250, 250, 250)); // 選中白
}
// --- 一般/非選中狀態 ---
if (!((lpD->itemAction & ODA_SELECT) && (lpD->itemState & ODS_SELECTED)))
{
pDC->FillRect(rcItem, &brNormal);
pDC->FillRect(rcLeft, &brLeft); // 畫左側灰欄
if (lpD->itemState & ODS_GRAYED) pDC->SetTextColor(RGB(194, 194, 194));
else pDC->SetTextColor(RGB(66, 110, 180)); // 一般深藍字
}
// 文字輸出 (左邊留 30px 給圖示區)
pDC->TextOut(rcItem.left + 30, rcItem.top + 5, strText);
}
else // 分隔線繪製
{
pDC->FillRect(rcItem, &brLeft);
}
menu.Detach(); // 必須分離,否則資源洩漏
}
}
OnInitMenu:一鍵開啟全選單 Owner-Draw 模式void CMainFrame::OnInitMenu(CMenu* pMenu)
{
CMenu* pSubMenu;
UINT nCount = pMenu->GetMenuItemCount();
for (UINT i = 0; i < nCount; ++i)
{
pSubMenu = pMenu->GetSubMenu(i);
if (pSubMenu)
{
UINT nSubCount = pSubMenu->GetMenuItemCount();
for (UINT j = 0; j < nSubCount; ++j)
{
UINT nID = pSubMenu->GetMenuItemID(j);
// 關鍵:修改風格加入 MF_OWNERDRAW
pSubMenu->ModifyMenu(j, MF_BYPOSITION | MF_OWNERDRAW, nID);
}
}
}
}
| 結構體 | 關鍵成員 | 說明 |
|---|---|---|
MEASUREITEMSTRUCT | CtlType, itemID, itemWidth, itemHeight | 用於 OnMeasureItem,告知項目類型與尺寸 |
DRAWITEMSTRUCT | CtlType, itemID, itemAction, itemState, hwndItem, hDC, rcItem, itemData | 用於 OnDrawItem,包含繪圖 DC、狀態、矩形、自訂資料指標 |
OnInitMenu 每次彈出選單都會觸發,建議加入 bool m_bMenuStyled 標誌,避免重複 ModifyMenu;或在 OnCreate 一次性完成。WM_INITMENUPOPUP 處理。這套「三函數走天下」方案,極適合快速原型開發或維護舊專案,讓單調的灰色選單瞬間煥然一新!