设计模式第5讲 — MFC消息响应机制
主讲:王亚沙 · 北京大学软件研究所 课程:0C107 设计模式 来源:田浩然上传的资料
核心主题
本讲深入 MFC 框架的消息响应机制底层实现:从 SDK 下的原始消息循环,到 MFC 如何用消息映射宏封装消息分发,再到钩子函数拦截窗口过程、消息查找匹配算法,以及为什么 MFC 不用虚函数而用消息映射宏的设计权衡。
一、SDK 下的消息响应机制(基础)
1.1 Windows 消息模型
- 消息与线程对应:Windows 消息是和线程相关的,系统把消息发送到对应线程的消息队列
- MSG 结构(6 个字段):
| 字段 | 类型 | 含义 |
|---|---|---|
| hwnd | HWND | 目标窗口句柄 |
| message | UINT | 消息 ID 号 |
| wParam | WPARAM | 消息参数(高/低位) |
| lParam | LPARAM | 消息参数 |
| time | DWORD | 消息发送时间 |
| pt | POINT | 消息发送时鼠标位置 |
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 文件中)
展开后声明三件事:
_messageEntries[]—— 静态消息条目数组messageMap—— 静态消息映射结构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; // 本类消息条目数组首地址
};两大作用:
- 得到本类消息映射数组的首地址(lpEntries)
- 得到基类消息映射表的地址(pBaseMap)—— 用于本类不响应时向上传递
3.3 整体数据结构(链表式继承)
本类 AFX_MSGMAP ── lpEntries ──→ [消息条目0][消息条目1][消息条目2]...
│
└── pBaseMap ──→ 基类 AFX_MSGMAP ── lpEntries ──→ [...]
│
└── pBaseMap ──→ 更上层基类 ...
四、MFC 消息处理完整流程
4.1 初始化阶段(钩子拦截)
MFC 通过 CBT 钩子 _AfxCbtFilterHook 在窗口创建时拦截 WM_CREATE:
- HWND ↔ CWnd 映射*:调用
CWnd::Attach,在全局CHandleMap中登记 HWND 与 CWnd 对象指针的对应关系 - 替换窗口过程:把窗口过程设为
AfxWndProc,原始窗口过程保存到CWnd::m_pfnSuper
钩子(Hook):Windows 系统级接口,可截获并处理发往其他窗口的消息,是一种回调函数机制
4.2 消息分发五步走
操作系统消息
↓
① AfxWndProc(全局函数,接收消息)
↓
② AfxCallWndProc(全局函数,句柄→CWnd对象转换)
↓
③ CWnd::WindowProc(成员方法,统一入口)
↓
④ CWnd::OnWndMsg(正式消息分派:查表匹配)
↓
⑤ 找到 → 调用用户消息处理函数
没找到 → CWnd::DefWindowProc(调用原始窗口过程)
4.3 消息查找算法(AfxFindMessageEntry)
- 从本类的
AFX_MSGMAP_ENTRY[]数组中逐个比较 message ID - 找到匹配项 → 返回
AFX_MSGMAP_ENTRY* - 根据 nSig 标识将 pfn 强转为正确原型并调用
- 本类找不到 → 沿 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 消息机制,但隐含了多个设计模式的思想:
- 模板方法(Template Method):
WindowProc → OnWndMsg → 具体处理函数是固定骨架,子类填充具体消息处理 - 责任链(Chain of Responsibility):消息沿继承链向上查找基类消息映射表,直到找到处理者或到顶
- 策略(Strategy):nSig + pfn 的组合本质上是将函数签名作为策略标识,运行时选择不同调用方式
- 外观(Facade):ClassWizard 和消息映射宏隐藏了底层钩子、查表、强转等复杂细节
七、关键概念速查
| 概念 | 一句话解释 |
|---|---|
| CCmdTarget | MFC 所有可处理消息类的基类 |
| CHandleMap | 全局 HWND ↔ CWnd* 映射表 |
| _AfxCbtFilterHook | CBT 钩子函数,窗口创建时拦截并替换窗口过程 |
| AfxWndProc | MFC 统一的窗口过程入口 |
| AFX_MSGMAP_ENTRY | 单条消息映射条目(消息ID + 函数指针 + 签名) |
| AFX_MSGMAP | 消息映射表(本类条目数组 + 基类表指针) |
| nSig | 消息处理函数的签名标识,用于正确调用 |
| DefWindowProc | 消息未处理时的兜底:调用原始窗口过程 |
关联笔记
- 设计模式第3讲-五大设计原则 —— 前序课程,SRP/OCP/LSP/DIP/ISP
- 设计模式第4讲-外观适配器策略桥接抽象工厂 —— 前序课程,5种设计模式
- MFC消息映射机制 —— MFC 框架核心机制(如有)
原始信息
- 文件:
ss-05(消息机制).pdf - 大小:342.23 KB
- 页数:13 页
- 格式:文字版 PDF
- 来源路径:
/田浩然上传的资料/0C107设计模式/ss-05(消息机制).pdf - 处理方法:pdftotext 全文提取
- 处理日期:2026-08-28