MFC 訊息映射核心架構
MFC 應用程式無傳統 main() 或訊息迴圈,所有事件處理封裝於 CWinApp 內部。開發者需透過訊息映射機制,將感興趣的 Windows 訊息對應至自訂處理函數。
訊息映射三要素
- 宣告巨集:類別定義中
DECLARE_MESSAGE_MAP()
- 對應表:
BEGIN_MESSAGE_MAP(類別, 基底類別) ... END_MESSAGE_MAP()
- 處理函數:
afx_msg void 函數名(參數),必須為 void 回傳、符合特定參數規範
CButton 按鈕控制項實戰
建立按鈕基本步驟
// 標頭檔
class CButtonWindow : public CFrameWnd {
CButton* m_pButton;
public:
CButtonWindow();
afx_msg void OnButtonClick();
DECLARE_MESSAGE_MAP()
};
// 實作檔
CButtonWindow::CButtonWindow() {
Create(NULL, _T("Button Demo"), WS_OVERLAPPEDWINDOW, CRect(0,0,300,200));
CRect r; GetClientRect(&r); r.DeflateRect(20,20);
m_pButton = new CButton();
m_pButton->Create(_T("Click Me"), WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON, r, this, IDC_MYBUTTON);
}
BEGIN_MESSAGE_MAP(CButtonWindow, CFrameWnd)
ON_BN_CLICKED(IDC_MYBUTTON, OnButtonClick)
END_MESSAGE_MAP()
void CButtonWindow::OnButtonClick() {
MessageBeep(-1); // 系統預設音效
}
關鍵點解析
- IDC_MYBUTTON:資源 ID (>100 避開系統保留),按鈕傳送
WM_COMMAND 時攜帶此 ID
- ON_BN_CLICKED:等同
ON_COMMAND 但更語義化,自動解析通知碼 BN_CLICKED
- 按鈕樣式:
BS_PUSHBUTTON、BS_DEFPUSHBUTTON、BS_CHECKBOX、BS_RADIOBUTTON、BS_OWNERDRAW 等 11 種
- 自動反白:按鈕按下時 MFC 自動處理視覺回饋
視窗尺寸變更處理 (WM_SIZE)
動態調整子控制項佈局
class CButtonWindow : public CFrameWnd {
CButton* m_pButton;
public:
afx_msg void OnButtonClick();
afx_msg void OnSize(UINT nType, int cx, int cy); // 三參數固定簽名
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CButtonWindow, CFrameWnd)
ON_BN_CLICKED(IDC_MYBUTTON, OnButtonClick)
ON_WM_SIZE() // 無參數,自動對應 OnSize
END_MESSAGE_MAP()
void CButtonWindow::OnSize(UINT nType, int cx, int cy) {
CFrameWnd::OnSize(nType, cx, cy); // 呼叫基底類別
if (m_pButton && m_pButton->GetSafeHwnd()) {
CRect r; GetClientRect(&r); r.DeflateRect(20,20);
m_pButton->MoveWindow(r); // 一次完成移動+縮放
}
}
ON_WM_* 巨集對應表
| 巨集 | 對應函數 | 參數 | 觸發時機 |
| ON_WM_SIZE | OnSize | UINT, int, int | 視窗大小改變 |
| ON_WM_MOVE | OnMove | int, int | 視窗位置移動 |
| ON_WM_PAINT | OnPaint | 無 | 需重繪客戶區 |
| ON_WM_TIMER | OnTimer | UINT_PTR | 計時器觸發 |
| ON_WM_HSCROLL | OnHScroll | UINT, UINT, CScrollBar* | 水平捲軸操作 |
| ON_WM_VSCROLL | OnVScroll | UINT, UINT, CScrollBar* | 垂直捲軸操作 |
計時器應用 (WM_TIMER)
// 建構函數中啟動計時器
SetTimer(IDT_MYTIMER, 1000, NULL); // 1000ms = 1秒,NULL 表示由視窗處理
// 訊息映射
ON_WM_TIMER()
// 處理函數
void CButtonWindow::OnTimer(UINT_PTR nIDEvent) {
if (nIDEvent == IDT_MYTIMER) {
MessageBeep(-1); // 每秒嗶一聲
// 實際應用:更新狀態、輪詢資料、動畫幀...
}
CFrameWnd::OnTimer(nIDEvent);
}
// 記得銷毀時殺掉計時器
KillTimer(IDT_MYTIMER);
CScrollBar 捲軸控制項完整實作
建立與初始化
class CScrollBarWindow : public CFrameWnd {
CScrollBar* m_pScrollBar;
static const int MIN_POS = 0, MAX_POS = 100;
public:
CScrollBarWindow();
afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
DECLARE_MESSAGE_MAP()
};
CScrollBarWindow::CScrollBarWindow() {
Create(NULL, _T("ScrollBar Demo"), WS_OVERLAPPEDWINDOW, CRect(0,0,400,300));
CRect r; GetClientRect(&r);
m_pScrollBar = new CScrollBar();
m_pScrollBar->Create(WS_CHILD|WS_VISIBLE|SBS_HORZ,
CRect(10,10,r.Width()-10,30), this, IDC_MYSCROLL);
m_pScrollBar->SetScrollRange(MIN_POS, MAX_POS, TRUE);
}
BEGIN_MESSAGE_MAP(CScrollBarWindow, CFrameWnd)
ON_WM_HSCROLL()
END_MESSAGE_MAP()
捲軸事件完整處理
void CScrollBarWindow::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) {
int pos = m_pScrollBar->GetScrollPos();
switch (nSBCode) {
case SB_LINELEFT: pos -= 1; break; // 左箭頭
case SB_LINERIGHT: pos += 1; break; // 右箭頭
case SB_PAGELEFT: pos -= 10; break; // 滑桿左側點擊
case SB_PAGERIGHT: pos += 10; break; // 滑桿右側點擊
case SB_LEFT: pos = MIN_POS; break; // Home/最左
case SB_RIGHT: pos = MAX_POS; break; // End/最右
case SB_THUMBTRACK: pos = nPos; break; // 拖曳滑桿 (即時)
case SB_THUMBPOSITION: pos = nPos; break; // 拖曳釋放
default: return;
}
// 邊界檢查
pos = max(MIN_POS, min(MAX_POS, pos));
m_pScrollBar->SetScrollPos(pos, TRUE); // TRUE = 重繪
}
捲軸通知碼對照
| 通知碼 | 使用者動作 | nPos 含義 |
| SB_LINELEFT/UP | 點擊左/上箭頭 | 無意義 |
| SB_LINERIGHT/DOWN | 點擊右/下箭頭 | 無意義 |
| SB_PAGELEFT/UP | 點擊滑桿軌道 (頁面) | 無意義 |
| SB_PAGERIGHT/DOWN | 點擊滑桿軌道 (頁面) | 無意義 |
| SB_LEFT/TOP | Home/最頂 | 無意義 |
| SB_RIGHT/BOTTOM | End/最底 | 無意義 |
| SB_THUMBTRACK | 拖曳滑桿中 | 當前拖曳位置 |
| SB_THUMBPOSITION | 釋放滑桿 | 最終位置 |
訊息映射底層原理與優勢
為什麼不用虛擬函數?
CWnd 擁有 200+ 成員函數,若全為虛擬函數 → 每個衍生類別需 200+ 虛擬函數表項目
- 30+ 衍生控制項類別 × 200 虛擬函數 = 龐大記憶體開銷,多數從未覆寫
訊息映射運作機制
- 編譯期:巨集展開為靜態陣列
AFX_MSGMAP_ENTRY,含訊息碼、處理函數指標
- 執行期:
CWnd::WindowProc 攔截訊息 → 搜尋自身訊息映射 → 找到則呼叫處理函數
- 未找到 → 遞迴搜尋基底類別映射 (由
BEGIN_MESSAGE_MAP 第二參數指定)
- 仍無匹配 →
DefWindowProc 預設處理
記憶體效率對比
| 機制 | 記憶體開銷 | 彈性 |
| 虛擬函數表 | 固定:類別數 × 函數數 × 指標大小 | 編譯期綁定 |
| 訊息映射 | 動態:僅佔用實際宣告的訊息項目 | 執行期可動態修改 (罕用) |
ClassWizard 整合開發
實務開發中,使用 ClassWizard (VC6) / 屬性視窗 (VS2005+) 自動產生:
- 類別精靈添加訊息處理器 → 自動生成
afx_msg 宣告、映射項目、函數骨架
- 資源編輯器設計對話框/視窗 → 關聯控制項 ID 與變數
- 屬性頁面快速設定控制項樣式、事件對應
最佳實踐總結
- 所有訊息處理函數:
afx_msg void Func(標準參數),放在 public/protected
- 複雜佈局改用
CWnd::RepositionBars 或 CDialog::OnSize 統一管理
- 計時器、Socket、執行緒訊息皆走訊息映射,保持單執行緒 UI 安全
- 自訂訊息:
#define WM_MYMSG (WM_USER + 100),用 ON_MESSAGE(WM_MYMSG, OnMyMsg) 對應
- 記得
KillTimer、DeleteObject、解除掛鉤,避免資源洩漏
- 上一篇文章: TCP/IP 協定族基礎架構與 IP 協定核心機制完整指南
- 下一篇文章: Visual C++ MFC 控制項樣式與 CStatic 靜態標籤完整定制指南
Copyright ©
程式碼之家 All Rights Reserved