【MFC】探索消息映射机制的实现原理
# 消息机制简述
我们知道,Windows 的窗口是由消息驱动的;
Windows 通过捕获鼠标、键盘等输入设备产生的动作,再生成对应的消息,并传递给相应的窗口所属的线程。
直接基于 Win32API
开发时,我们需要在注册窗口类时指定窗口过程函数,以告知 Windows 回调 (传递消息) 的入口。
# MFC 的消息映射
而 MFC 为我们提供了一种更为方便的机制,
消息映射示例
1 |
|
# 消息映射宏
MFC 的消息映射是通过几个宏函数来实现的,我们以上述代码为例,将宏展开,一探究竟。
-
声明宏
MFC 的声明宏,就是在窗口类中声明两个受保护的成员函数。ECLARE_MESSAGE_MAP()
1
2
3protected:
static const AFX_MSGMAP* PASCAL GetThisMessageMap();
virtual const AFX_MSGMAP* GetMessageMap() const; -
定义宏
定义则稍显复杂,由多个宏函数组成,实际上是在实现声明宏声明的成员函数。BEGIN_MESSAGE_MAP()
BEGIN_MESSAGE_MAP(CMyFrameWnd, CFrameWnd)
1
2
3
4
5
6
7
8
9
10
11
12
13PTM_WARNING_DISABLE
const AFX_MSGMAP* CMyFrameWnd::GetMessageMap() const
{
return GetThisMessageMap();
}
const AFX_MSGMAP* PASCAL CMyFrameWnd::GetThisMessageMap()
{
typedef CMyFrameWnd ThisClass;
typedef CFrameWnd TheBaseClass;
__pragma(warning(push))
__pragma(warning(disable: 4640))
static const AFX_MSGMAP_ENTRY _messageEntries[] =
{ON_MESSAGE()
ON_MESSAGE(WM_CREATE, OnCreate)
1
2// 实际上是在为_messageEntries变量的初始化列表添加结构体元素
{ WM_CREATE, 0, 0, 0, AfxSig_lwl, (AFX_PMSG)(AFX_PMSGW)(static_cast<LRESULT(AFX_MSG_CALL CWnd::*)(WPARAM, LPARAM)>(OnCreate))},END_MESSAGE_MAP()
1
2
3
4
5
6
7
8{ 0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 }
};
__pragma(warning(pop))
static const AFX_MSGMAP messageMap =
{ &TheBaseClass::GetThisMessageMap, &_messageEntries[0] };
return &messageMap;
}
PTM_WARNING_RESTORE -
整体展开
消息映射示例-宏展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class CMyFrameWnd :public CFrameWnd {
protected:
static const AFX_MSGMAP* PASCAL GetThisMessageMap();
virtual const AFX_MSGMAP* GetMessageMap() const;
public:
LRESULT OnCreate(WPARAM wParam, LPARAM lParam) {
AfxMessageBox(L"Window Create!");
return 0;
}
};
PTM_WARNING_DISABLE
const AFX_MSGMAP* CMyFrameWnd::GetMessageMap() const
{
return GetThisMessageMap();
}
const AFX_MSGMAP* PASCAL CMyFrameWnd::GetThisMessageMap()
{
typedef CMyFrameWnd ThisClass;
typedef CFrameWnd TheBaseClass;
__pragma(warning(push))
__pragma(warning(disable: 4640))
static const AFX_MSGMAP_ENTRY _messageEntries[] =
{
{ WM_CREATE, 0, 0, 0, AfxSig_lwl, (AFX_PMSG)(AFX_PMSGW)(static_cast<LRESULT(AFX_MSG_CALL CWnd::*)(WPARAM, LPARAM)>(OnCreate))},
{ 0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 }
};
__pragma(warning(pop))
static const AFX_MSGMAP messageMap =
{ &TheBaseClass::GetThisMessageMap, &_messageEntries[0] };
return &messageMap;
}
PTM_WARNING_RESTORE
class CMyWinApp :public CWinApp {
public:
CMyWinApp() {
}
virtual BOOL InitInstance() {
CMyFrameWnd* frame = new CMyFrameWnd;
frame->Create(nullptr, L"MFCBase");
m_pMainWnd = frame;
frame->ShowWindow(SW_SHOW);
frame->UpdateWindow();
return TRUE;
}
};
CMyWinApp g_theApp;
通过宏添加消息映射时,实际上是重写了 CFrameWnd
类的 GetMessageMap
成员虚函数;
而 GetMessageMap
函数初始化了一个名为 _messageEntries
和 messageMap
的静态局部变量,并将该变量的地址返回。
我们简单猜测,与上一节重写 WindowProc
类似,微软在窗口过程函数 AfxWndProc
的某处,得到窗口对象之后,以窗口对象的身份调用其成员虚函数 GetMessageMap
;
由于成员虚函数被重写的原因,得到我们所设定的映射关系表,我们这里就把它叫做消息映射表;
基于此消息映射表,就可以在窗口过程函数中,
# 探索实现原理
我们基本上能够理解消息映射机制的工作原理,这小节我们就扒一下微软的源码,以便更清晰的了解消息映射机制。
AfxWndProc
1 | LRESULT CALLBACK |
调试上面提供的代码,首先在 AfxWndProc
函数设置断点;
继续走到 AfxCallWndProc
;
AfxCallWndProc
1 | LRESULT AFXAPI AfxCallWndProc(CWnd* pWnd, HWND hWnd, UINT nMsg, |
到此处停下,此时你可能会有些疑惑,诶,我们并没有重写 Cwnd::WindowProc
,为什么要到此处停下?
因为微软将消息映射机制放到了此函数内实现。
CWnd::WindowProc
1 | LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) |
继续到进入 CWnd::OnWndMsg
。
CWnd::OnWndMsg
1 | BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult) |
上面的代码我们暂时不关心,只关心对 GetMessageMap
函数的调用;
还记得吗?我们将消息声明 / 定义宏展开后,就相当于我们为 CMyFrameWnd
类定义了两个函数:
1 | static const AFX_MSGMAP* PASCAL GetThisMessageMap(); |
此处是以 CMyFrameWnd
类的对象的身份调用的 GetMessageMap
,因此实际上是在调用我们类中由消息映射宏重写的成员虚函数。
CMyFrameWnd::GetMessageMap
1 | const AFX_MSGMAP* CMyFrameWnd::GetMessageMap() const |
这里只是简单的调用了静态成员函数 GetThisMessageMap
。
CMyFrameWnd::GetThisMessageMap
1 | const AFX_MSGMAP* PASCAL CMyFrameWnd::GetThisMessageMap() |
我这里对原先的代码做了一些简化,更方便看一点,实际上呢就是为当前函数的静态局部变量 _messageEntries
、 messageMap
赋值,并返回 messageMap
的地址。
又回到 CWnd::OnWndMsg
CWnd::OnWndMsg
1 | BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult) |
使 pMessageMap
指向静态局部变量 messageMap
。
记住上述代码的第 19
~ 20
行要进入循环;
剩下的我们暂时不关心,继续向下看。
CWnd::OnWndMsg
1 | for (/* pMessageMap already init'ed */; pMessageMap->pfnGetBaseMap != NULL; |
第 10
~ 11
行是关键函数, pMessageMap->lpEntries
即是访问静态局部变量 messageMap
的第二个成员,也就是 _messageEntries
的地址;
而 message
就是本次窗口过程处理的消息 ID 了;
返回值即是 _messageEntries
中对应条目的地址,如果返回 NULL
表示消息映射表中不存在此消息。
我们假定此消息已经被我们影射了,跟随 goto LDispatch;
继续。
CWnd::OnWndMsg
1 | LDispatch: |
最终会在此处以 CMyFrameWnd
类对象的身份调用我们指定的 OnCreate
成员函数。
# 消息映射表链表
整个流程基本上到这里就完成了,实际上我们留了一个小坑,即上面代码中我们暂时跳过的 for 循环。
实际上那是在遍历
实际上, CFrameWnd
类也使用了消息映射机制,而 CMyFrameWnd::GetMessageMap
函数中的静态局部变量 messageMap
,第一个成员指向的就是静态成员函数 CFrameWnd::GetThisMessageMap
。
1 | for (/* pMessageMap already init'ed */; pMessageMap->pfnGetBaseMap != NULL; |
而 pMessageMap->pfnGetBaseMap
,就是在访问 messageMap
的第一个成员。
此 for 循环的工作:
从链表头 ( CMyFrameWnd
) 开始,
# 消息映射分类
其实,除了 ON_MESSAGE
之外,微软还提供了更加方便的,直接映射指定消息的宏。
代码示例
1 |
|
我们将 ON_WM_CREATE
宏展开,看看与 ON_MESSAGE
有什么区别。
ON_WM_CREATE
1 |
ON_MESSAGE
1 |
首先就是, ON_WM_CREATE
不再要求我们提供参数消息 id 与回调函数名,而是直接指定为 WM_CREATE
与 OnCreate
;
其次, ON_MESSAGE
与 ON_WM_CREATE
添加的消息映射条目 AFX_MSGMAP_ENTRY
元素的第 5 个成员 nSig
,分别是 AfxSig_l_w_l
与 AfxSig_i_v_s
。
还记得吗,我们指定的消息映射函数,都是在 CWnd::OnWndMsg
函数内被调用的;
而先前通过 ON_MESSAGE
定义的消息映射,回调函数原型都是相同的。
而通过 ON_WM_CREATE
定义的消息映射,和 ON_MESSAGE
指定的消息映射函数的原型是不同的,那么 CWnd::OnWndMsg
是怎么区分不同的函数原型并进行回调的呢?
我们回到 CWnd::OnWndMsg
函数:
CWnd::OnWndMsg
1 | LDispatch: |
其中最关键的就是 switch (lpEntry->nSig)
:
- 通过
ON_MESSAGE
定义的消息映射时,nSig 成员是AfxSig_l_w_l
; - 通过
ON_WM_CREATE
定义的消息映射时,nSig 成员是AfxSig_i_v_s
这就是为什么,函数原型不同,回调也能正确进行的缘故。