首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> 關於VC++

MFC 消息映射機制深度解析(四之二):命令路由、擴展映射、通知反射與 UI 狀態更新

     編輯:關於VC++

承接上篇標準 Windows 消息處理流程,本文深入 MFC 消息映射的核心難點:**命令消息路由機制**、**擴展映射宏實現多次處理**、**WM_NOTIFY/WM_COMMAND 通知消息處理與反射**、**UI 狀態更新 (ON_UPDATE_COMMAND_UI)** 的自動化機制、以及**消息預處理**。通過源碼級流程追蹤,揭示 OnCmdMsg、DispatchCmdMsg、CCmdUI、CFrameWnd/CMDIFrameWnd 路由邏輯的精妙設計。

一、 命令消息路由:標準分發路徑與動態約束

1. 從菜單點擊到 AfxWndProc

用戶點擊菜單 → 產生 WM_COMMAND (wParam 低字 = 命令 ID) → 發送至主框架窗口 HWND → AfxWndProc → AfxCallWndProc → CWnd::WindowProc → OnWndMsg → 判斷 WM_COMMAND → OnCommand。

2. CFrameWnd::OnCommand → OnCmdMsg

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
}

3. CFrameWnd::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 操作由視圖/框架處理,數據操作由文檔處理,全局命令由應用程序處理。

4. MDI 擴展路由:CMDIFrameWnd::OnCmdMsg

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 邏輯,但其「視圖」為子框架內的視圖,「文檔模板」為創建該子框架的模板。

二、 命令分發引擎:DispatchCmdMsg 與擴展映射宏

1. DispatchCmdMsg 實現

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; // 或返回函數返回值
}

2. 擴展映射宏:ON_COMMAND_EX 實現「多次處理」

標準 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 繼續路由

雙重功能:

  1. 多命令合一:多個 ON_COMMAND_EX 指向同一函數,nID 參數區分命令。
  2. 鏈式處理:處理函數返回 FALSE,命令繼續傳遞給路由鏈下一個目標。返回 TRUE 終止路由。

同理擴展至通知/反射/範圍宏:ON_NOTIFY_EX, ON_CONTROL_REFLECT_EX, ON_COMMAND_EX_RANGE 等。

三、 通知消息處理與反射機制

1. WM_COMMAND 通知 vs WM_NOTIFY

  • WM_COMMAND 通知:控件 ID 在 wParam 低字,通知碼在高字 (如 BN_CLICKED=0),lParam = 控件 HWND。BN_CLICKED 通知碼為 0,故 ON_BN_CLICKED 與 ON_COMMAND 等效。
  • WM_NOTIFY:用於複雜控件 (ListView, TreeView 等)。lParam 指向 NMHDR 或更大結構 (如 NM_LISTVIEW),首字段必為 NMHDR (hwndFrom, idFrom, code)。

2. OnNotify 處理流程

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 區分具體通知。

3. 消息反射:讓控件自我處理

機制:父窗口收到通知 → 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)
  • UI 更新: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))
}

四、 UI 狀態自動更新機制:CCmdUI 與雙通道觸發

1. 核心類 CCmdUI

輕量級狀態承載對象,**非 CObject 派生**。關鍵成員:

  • m_nID:命令 ID
  • m_pMenu / m_pSubMenu / m_pParentMenu:菜單指針 (菜單通道)
  • m_pOther:工具欄/狀態欄窗口指針 (工具欄通道)
  • m_nIndex:工具欄按鈕索引

核心方法:Enable(BOOL), SetCheck(int), SetRadio(BOOL), SetText(LPCTSTR)。

2. DoUpdate 核心邏輯

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);
    }
}

3. 兩大自動觸發通道

通道觸發時機發送者接收者CCmdUI 構造差異
菜單用戶點擊菜單欄 → WM_INITMENUPOPUP框架窗口 OnInitMenuPopup框架窗口 (路由至視圖/文檔/應用程序)m_pMenu/m_pParentMenu 有效 → 內部調用 CMenu::EnableMenuItem
工具欄/狀態欄消息循環空閒 → WM_IDLEUPDATECMDUI框架窗口 (向所有子窗口廣播)工具欄/狀態欄控件 (自身處理)m_pOther 指向控件 → 內部調用 CWnd::EnableWindow

統一編程體驗:同一個 ON_UPDATE_COMMAND_UI(ID, OnUpdateXXX) 映射,**同時生效於菜單項與對應工具欄按鈕**,CCmdUI 內部分發具體調用,對開發者透明。

五、 消息預處理:PreTranslateMessage

在 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 表示消息已消費,**不再進入標準分發流程**。

六、 總結:MFC 消息映射架構全景

  1. 映射表實現:靜態數組 _messageEntries + 靜態 messageMap 結構 + 虛擬 GetMessageMap 實現類級隔離與繼承查找。
  2. 查找算法:緩存池 (Hash, 512 條) → 當前類映射表 → 基類映射表 ... → CCmdTarget。匯編優化 AfxFindMessageEntry。
  3. 分發四大虛擬函數:OnWndMsg(標準消息) → OnCommand/OnNotify(WM_COMMAND/NOTIFY) → OnCmdMsg(命令路由/分發)。
  4. 命令路由鏈:視圖 → 文檔 → 模板 → 框架 → 應用程序 (MDI 先活動子框架)。
  5. 擴展宏:返回 BOOL 實現鏈式處理與多命令合一。
  6. 反射機制:父窗口主動轉發 WM_REFLECT_BASE+Msg 給控件,控件用反射宏響應。
  7. UI 更新:CCmdUI 統一抽象,菜單/工具欄雙通道 (WM_INITMENUPOPUP/WM_IDLEUPDATECMDUI) 自動觸發。
  8. 預處理:PreTranslateMessage 搶先處理加速鍵/Tooltip/對話框導航。

這套機制以「映射表 + 虛擬函數分發 + 路由鏈」替代龐大的虛擬函數表,實現了**按需映射、靈活路由、可擴展處理**,是 MFC 精華所在。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved