设计模式第5讲 — MFC消息响应机制

主讲:王亚沙 · 北京大学软件研究所 课程:0C107 设计模式 来源:田浩然上传的资料

核心主题

本讲深入 MFC 框架的消息响应机制底层实现:从 SDK 下的原始消息循环,到 MFC 如何用消息映射宏封装消息分发,再到钩子函数拦截窗口过程、消息查找匹配算法,以及为什么 MFC 不用虚函数而用消息映射宏的设计权衡。


一、SDK 下的消息响应机制(基础)

1.1 Windows 消息模型

  • 消息与线程对应:Windows 消息是和线程相关的,系统把消息发送到对应线程的消息队列
  • MSG 结构(6 个字段):
字段类型含义
hwndHWND目标窗口句柄
messageUINT消息 ID 号
wParamWPARAM消息参数(高/低位)
lParamLPARAM消息参数
timeDWORD消息发送时间
ptPOINT消息发送时鼠标位置

1.2 SDK 消息循环骨架

WinMain(...) {
    MSG msg;
    RegisterClass(...);      // 注册窗口类
    CreateWindow(...);       // 创建窗口
    ShowWindow(...);         // 显示窗口
    UpdateWindow(...);
    
    while (GetMessage(&msg, ...)) {  // 消息循环
        TranslateMessage(...);       // 翻译键盘消息
        DispatchMessage(...);        // 派发给窗口过程
    }
    return msg.wParam;
}
  • 窗口过程(Window Proc):根据消息类型 switch-case 分发处理

问题:用户需要在窗口过程中手动分析消息类型和参数,代码繁琐、维护成本高


二、MFC 消息响应机制

2.1 设计思想

  • MFC 把消息调用过程封装起来,用户通过 ClassWizard 可视化地添加消息处理
  • 所有能处理消息的类都继承自 CCmdTarget —— MFC 消息处理的基础和核心

2.2 消息映射宏(两大支柱)

支柱 1:DECLARE_MESSAGE_MAP(.h 文件中)

展开后声明三件事:

  1. _messageEntries[] —— 静态消息条目数组
  2. messageMap —— 静态消息映射结构
  3. GetMessageMap() —— 虚函数,返回消息映射表指针
#define DECLARE_MESSAGE_MAP() \
private: \
    static const AFX_MESSAGE_ENTRY _messageEntries[]; \
protected: \
    static AFX_DATA const AFX_MSGMAP messageMap; \
    virtual const AFX_MSGMAP* GetMessageMap() const;

支柱 2:BEGIN/END_MESSAGE_MAP(.cpp 文件中)

BEGIN_MESSAGE_MAP(CInheritClass, CBaseClass)
    //{{AFX_MSG_MAP(CInheritClass)
    // ... 具体消息映射条目
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()
  • 填充 _messageEntries[] 数组和 messageMap 结构
  • 第二个参数指定基类,建立继承链

三、关键数据结构

3.1 AFX_MSGMAP_ENTRY(单条消息映射)

struct AFX_MSGMAP_ENTRY {
    UINT nMessage;   // 消息 ID
    UINT nCode;      // 控件通知码 或 WM_NOTIFY 码
    UINT nID;        // 控件 ID(窗口消息为 0)
    UINT nLastID;    // 控件 ID 范围的上限
    UINT nSig;       // 函数签名标识(signature)
    AFX_PMSG pfn;    // 消息处理函数指针
};

核心字段解读:

  • pfn:指向 CCmdTarget 成员函数的指针,类型为 void (CCmdTarget::*)(void)
  • nSig:签名标识,不同原型的消息处理函数对应不同的 nSig 值
    • 解决”统一函数指针类型”与”不同消息函数原型不同”的矛盾
    • 运行时根据 nSig 决定如何强转 pfn 并调用

3.2 AFX_MSGMAP(消息映射表)

struct AFX_MSGMAP {
#ifdef _AFXDLL
    const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();  // DLL 版:函数指针
#else
    const AFX_MSGMAP* pBaseMap;                   // 静态版:直接指针
#endif
    const AFX_MSGMAP_ENTRY* lpEntries;             // 本类消息条目数组首地址
};

两大作用:

  1. 得到本类消息映射数组的首地址(lpEntries)
  2. 得到基类消息映射表的地址(pBaseMap)—— 用于本类不响应时向上传递

3.3 整体数据结构(链表式继承)

本类 AFX_MSGMAP ── lpEntries ──→ [消息条目0][消息条目1][消息条目2]...
        │
        └── pBaseMap ──→ 基类 AFX_MSGMAP ── lpEntries ──→ [...]
                                  │
                                  └── pBaseMap ──→ 更上层基类 ...

四、MFC 消息处理完整流程

4.1 初始化阶段(钩子拦截)

MFC 通过 CBT 钩子 _AfxCbtFilterHook 在窗口创建时拦截 WM_CREATE:

  1. HWND ↔ CWnd 映射*:调用 CWnd::Attach,在全局 CHandleMap 中登记 HWND 与 CWnd 对象指针的对应关系
  2. 替换窗口过程:把窗口过程设为 AfxWndProc,原始窗口过程保存到 CWnd::m_pfnSuper

钩子(Hook):Windows 系统级接口,可截获并处理发往其他窗口的消息,是一种回调函数机制

4.2 消息分发五步走

操作系统消息
    ↓
① AfxWndProc(全局函数,接收消息)
    ↓
② AfxCallWndProc(全局函数,句柄→CWnd对象转换)
    ↓
③ CWnd::WindowProc(成员方法,统一入口)
    ↓
④ CWnd::OnWndMsg(正式消息分派:查表匹配)
    ↓
⑤ 找到 → 调用用户消息处理函数
   没找到 → CWnd::DefWindowProc(调用原始窗口过程)

4.3 消息查找算法(AfxFindMessageEntry)

  1. 从本类的 AFX_MSGMAP_ENTRY[] 数组中逐个比较 message ID
  2. 找到匹配项 → 返回 AFX_MSGMAP_ENTRY*
  3. 根据 nSig 标识将 pfn 强转为正确原型并调用
  4. 本类找不到 → 沿 pBaseMap 到基类继续找(继承链向上传递)

4.4 DefWindowProc 兜底逻辑

LRESULT CWnd::DefWindowProc(UINT nMsg, WPARAM wParam, LPARAM lParam)
{
    if (m_pfnSuper != NULL)
        return ::CallWindowProc(m_pfnSuper, m_hWnd, nMsg, wParam, lParam);
    WNDPROC pfnWndProc;
    if ((pfnWndProc = *GetSuperWndProcAddr()) == NULL)
        return ::DefWindowProc(m_hWnd, nMsg, wParam, lParam);
    else
        return ::CallWindowProc(pfnWndProc, m_hWnd, nMsg, wParam, lParam);
}
  • 优先调用子类化前的超类窗口过程(m_pfnSuper)
  • 没有则调用系统默认的 DefWindowProc

五、消息映射 vs 虚函数:为什么 MFC 选宏?

维度虚函数方案MFC 消息映射宏
实现方式动态联编/动态绑定宏 + 结构体数组查表
内存开销每个类都需要完整虚表,即使只覆盖几个函数
100+ 消息 × 4 字节 ≈ 400 字节/类
只在数组中存放需要处理的消息条目
按需添加,内存占用少
自定义消息很难处理(虚表是编译期固定的)和系统消息一样,加一条宏即可
扩展性受限于类层次灵活,COMMAND 消息还能跨类路由(文档/视图/框架)

核心洞察:MFC 的消息映射本质上是一种手动实现的、更轻量的、可扩展的虚函数表替代方案。

  • 虚函数表是编译器生成的、定长的、顺序固定的
  • 消息映射表是宏生成的、变长的、只含需要的条目的
  • 在 Windows 消息数量多(100+)但具体类只处理少数几个的场景下,消息映射的空间效率远高于虚函数

六、设计模式视角

本讲虽然讲的是 MFC 消息机制,但隐含了多个设计模式的思想:

  1. 模板方法(Template Method):WindowProc → OnWndMsg → 具体处理函数 是固定骨架,子类填充具体消息处理
  2. 责任链(Chain of Responsibility):消息沿继承链向上查找基类消息映射表,直到找到处理者或到顶
  3. 策略(Strategy):nSig + pfn 的组合本质上是将函数签名作为策略标识,运行时选择不同调用方式
  4. 外观(Facade):ClassWizard 和消息映射宏隐藏了底层钩子、查表、强转等复杂细节

七、关键概念速查

概念一句话解释
CCmdTargetMFC 所有可处理消息类的基类
CHandleMap全局 HWND ↔ CWnd* 映射表
_AfxCbtFilterHookCBT 钩子函数,窗口创建时拦截并替换窗口过程
AfxWndProcMFC 统一的窗口过程入口
AFX_MSGMAP_ENTRY单条消息映射条目(消息ID + 函数指针 + 签名)
AFX_MSGMAP消息映射表(本类条目数组 + 基类表指针)
nSig消息处理函数的签名标识,用于正确调用
DefWindowProc消息未处理时的兜底:调用原始窗口过程

关联笔记

原始信息

  • 文件:ss-05(消息机制).pdf
  • 大小:342.23 KB
  • 页数:13 页
  • 格式:文字版 PDF
  • 来源路径:/田浩然上传的资料/0C107设计模式/ss-05(消息机制).pdf
  • 处理方法:pdftotext 全文提取
  • 处理日期:2026-08-28