承接上篇標準 Windows 消息處理流程,本文深入 MFC 消息映射的核心難點:**命令消息路由機制**、**擴展映射宏實現多次處理**、**WM_NOTIFY/WM_COMMAND 通知消息處理與反射**、**UI 狀態更新 (ON_UPDATE_COMMAND_UI)** 的自動化機制、以及**消息預處理**。通過源碼級流程追蹤,揭示 OnCmdMsg、DispatchCmdMsg、CCmdUI、CFrameWnd/CMDIFrameWnd 路由邏輯的精妙設計。
用戶點擊菜單 → 產生 WM_COMMAND (wParam 低字 = 命令 ID) → 發送至主框架窗口 HWND → AfxWndProc → AfxCallWndProc → CWnd::WindowProc → OnWndMsg → 判斷 WM_COMMAND → OnCommand。
BOOL CFrameWnd::OnCommand(WPARAM wParam, LPARAM lParam) {
// 檢查 Help 模式 (Shift+F1)
if (bHelpMode) return SendCommand(ID_CONTEXT_HELP);
return CWnd::OnCommand(wParam, lParam); // 基類執行標準路由
}
BOOL CWnd::OnCommand(WPARAM wParam, LPARAM lParam) {
UINT nID = LOWORD(wParam);
// 1. 更新 UI 狀態測試:OnCmdMsg(nID, CN_UPDATE_COMMAND_UI, &state)
// 2. 發送命令:OnCmdMsg(nID, CN_COMMAND, NULL, NULL)
// 3. 通知消息:反射給子控件 + OnCmdMsg
}
BOOL CFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo) {
// 1. 活動視圖
if (pView = GetActiveView())
if (pView->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) return TRUE;
// 2. 視圖關聯文檔
if (pDoc = pView->GetDocument())
if (pDoc->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) return TRUE;
// 3. 文檔模板
if (pTemplate = pDoc->GetDocTemplate())
if (pTemplate->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) return TRUE;
// 4. 框架窗口自身 (CFrameWnd 基類鏈最終到 CCmdTarget)
if (CWnd::OnCmdMsg(...)) return TRUE; // 實際調用 CCmdTarget::OnCmdMsg
// 5. 應用程序對象
if (AfxGetApp()->OnCmdMsg(...)) return TRUE;
return FALSE; // 所有目標均未處理
}
關鍵點:路由順序 **視圖 → 文檔 → 文檔模板 → 框架窗口 → 應用程序**。任一環節返回 TRUE 即終止路由。這實現了「就近處理」原則:UI 操作由視圖/框架處理,數據操作由文檔處理,全局命令由應用程序處理。
BOOL CMDIFrameWnd::OnCmdMsg(...) {
// 1. 優先發送給活動 MDI 子框架
if (pChild = MDIGetActive())
if (pChild->OnCmdMsg(...)) return TRUE; // 子框架繼承 CFrameWnd 路由:視圖→文檔→模板→自身→應用程序
// 2. 主框架自身標準路由 (會再次嘗試活動子框架,屬冗餘但安全)
if (CFrameWnd::OnCmdMsg(...)) return TRUE;
return FALSE;
}
CMDIChildWnd 未覆蓋 OnCmdMsg,沿用 CFrameWnd 邏輯,但其「視圖」為子框架內的視圖,「文檔模板」為創建該子框架的模板。
BOOL CCmdTarget::DispatchCmdMsg(CCmdTarget* pTarget, UINT nID, int nCode,
AFX_PMSG pfn, void* pExtra, UINT nSig, AFX_CMDHANDLERINFO* pHandlerInfo) {
if (pHandlerInfo) { // 測試模式
pHandlerInfo->pTarget = pTarget;
pHandlerInfo->pmf = pfn;
return TRUE;
}
// 實際調用:根據 nSig 將 pfn 轉換為正確成員函數指針類型並調用
union MessageMapFunctions mmf;
mmf.pfn = pfn;
switch (nSig) {
case AfxSig_vv: (pTarget->*mmf.pfn_vv)(); break; // void ()
case AfxSig_vw: (pTarget->*mmf.pfn_vw)(wParam); break; // void (WPARAM)
// ... 其他簽名
}
return TRUE; // 或返回函數返回值
}
標準 ON_COMMAND 處理函數無返回值 (void),路由遇到即停止。擴展宏 定義返回 BOOL 的處理函數:
// 宏定義簡化
#define ON_COMMAND_EX(id, memberFxn) \
{ WM_COMMAND, CN_COMMAND, id, id, AfxSig_bv, (AFX_PMSG)&memberFxn }
// 處理函數原型
BOOL CMyClass::OnCmdEx(UINT nID) { ... return FALSE; } // 返回 FALSE 繼續路由
雙重功能:
ON_COMMAND_EX 指向同一函數,nID 參數區分命令。FALSE,命令繼續傳遞給路由鏈下一個目標。返回 TRUE 終止路由。同理擴展至通知/反射/範圍宏:ON_NOTIFY_EX, ON_CONTROL_REFLECT_EX, ON_COMMAND_EX_RANGE 等。
BN_CLICKED=0),lParam = 控件 HWND。BN_CLICKED 通知碼為 0,故 ON_BN_CLICKED 與 ON_COMMAND 等效。NMHDR 或更大結構 (如 NM_LISTVIEW),首字段必為 NMHDR (hwndFrom, idFrom, code)。BOOL CWnd::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult) {
// 1. 反射給控件
if (ReflectLastMsg()) return TRUE;
// 2. 轉發給 OnCmdMsg
AFX_NOTIFY notify; notify.pResult = pResult; notify.pNMHDR = (NMHDR*)lParam;
return OnCmdMsg(LOWORD(wParam), MAKELONG(WM_NOTIFY, HIWORD(wParam)), ¬ify, NULL);
}
DispatchCmdMsg 的 pExtra 參數傳遞 AFX_NOTIFY*,處理函數可通過 pNMHDR->code 區分具體通知。
機制:父窗口收到通知 → OnCommand/OnNotify → ReflectChildNotify → 發送 WM_REFLECT_BASE + 原消息 給控件 HWND → 控件 OnChildNotify → OnCmdMsg/OnWndMsg 匹配 反射映射宏。
反射映射宏命名規則:
ON_WM_HSCROLL_REFLECT, ON_WM_VSCROLL_REFLECT, ON_WM_CTLCOLOR_REFLECT, ON_WM_DRAWITEM_REFLECT...ON_CONTROL_REFLECT (WM_COMMAND), ON_NOTIFY_REFLECT (WM_NOTIFY)ON_UPDATE_COMMAND_UI_REFLECT實例:黃色背景編輯框
class CYellowEdit : public CEdit {
DECLARE_MESSAGE_MAP()
afx_msg HBRUSH CtlColor(CDC* pDC, UINT nCtlColor);
};
BEGIN_MESSAGE_MAP(CYellowEdit, CEdit)
ON_WM_CTLCOLOR_REFLECT()
END_MESSAGE_MAP()
HBRUSH CYellowEdit::CtlColor(CDC* pDC, UINT nCtlColor) {
pDC->SetTextColor(RGB(0,0,0));
pDC->SetBkColor(RGB(255,255,0));
return m_brBkgnd; // 成員變量:CreateSolidBrush(RGB(255,255,0))
}
輕量級狀態承載對象,**非 CObject 派生**。關鍵成員:
m_nID:命令 IDm_pMenu / m_pSubMenu / m_pParentMenu:菜單指針 (菜單通道)m_pOther:工具欄/狀態欄窗口指針 (工具欄通道)m_nIndex:工具欄按鈕索引核心方法:Enable(BOOL), SetCheck(int), SetRadio(BOOL), SetText(LPCTSTR)。
void CCmdUI::DoUpdate(CCmdTarget* pTarget, BOOL bDisableIfNoHandler) {
// 1. 發送更新命令:pTarget->OnCmdMsg(m_nID, CN_UPDATE_COMMAND_UI, this, NULL)
// → 路由鏈尋找 ON_UPDATE_COMMAND_UI 映射 → 調用處理函數 (如 OnUpdateEditCopy)
// → 處理函數調用 pCmdUI->Enable/SetCheck/SetText 設置狀態
// 2. 若 bDisableIfNoHandler 且無處理函數 → 自動 Disable
if (bDisableIfNoHandler) {
AFX_CMDHANDLERINFO info;
if (!pTarget->OnCmdMsg(m_nID, CN_COMMAND, this, &info)) // 僅測試
Enable(FALSE);
}
}
| 通道 | 觸發時機 | 發送者 | 接收者 | CCmdUI 構造差異 |
|---|---|---|---|---|
| 菜單 | 用戶點擊菜單欄 → WM_INITMENUPOPUP | 框架窗口 OnInitMenuPopup | 框架窗口 (路由至視圖/文檔/應用程序) | m_pMenu/m_pParentMenu 有效 → 內部調用 CMenu::EnableMenuItem |
| 工具欄/狀態欄 | 消息循環空閒 → WM_IDLEUPDATECMDUI | 框架窗口 (向所有子窗口廣播) | 工具欄/狀態欄控件 (自身處理) | m_pOther 指向控件 → 內部調用 CWnd::EnableWindow |
統一編程體驗:同一個 ON_UPDATE_COMMAND_UI(ID, OnUpdateXXX) 映射,**同時生效於菜單項與對應工具欄按鈕**,CCmdUI 內部分發具體調用,對開發者透明。
在 PumpMessage 中 TranslateMessage 前調用,由 WalkPreTranslateTree 從目標窗口向上遞歸調用祖先窗口的 PreTranslateMessage。典型用途:
CWnd:Tooltip 消息過濾 (RelayEvent)。CFrameWnd:加速鍵處理 TranslateAccelerator(m_hAccelTable)。CMDIChildWnd:加速鍵處理 TranslateAccelerator(GetMDIFrame()->m_hAccelTable) (需主框架加速鍵表)。CMDIFrameWnd:系統 MDI 加速鍵 (如 Ctrl+F4 關閉子窗口)。CDialog/CFormView:IsDialogMessage 處理 Tab/Enter 導航、快捷鍵。預處理返回 TRUE 表示消息已消費,**不再進入標準分發流程**。
_messageEntries + 靜態 messageMap 結構 + 虛擬 GetMessageMap 實現類級隔離與繼承查找。CCmdTarget。匯編優化 AfxFindMessageEntry。OnWndMsg(標準消息) → OnCommand/OnNotify(WM_COMMAND/NOTIFY) → OnCmdMsg(命令路由/分發)。BOOL 實現鏈式處理與多命令合一。WM_REFLECT_BASE+Msg 給控件,控件用反射宏響應。CCmdUI 統一抽象,菜單/工具欄雙通道 (WM_INITMENUPOPUP/WM_IDLEUPDATECMDUI) 自動觸發。PreTranslateMessage 搶先處理加速鍵/Tooltip/對話框導航。這套機制以「映射表 + 虛擬函數分發 + 路由鏈」替代龐大的虛擬函數表,實現了**按需映射、靈活路由、可擴展處理**,是 MFC 精華所在。