在上一节中,我们借助MFC框架中的两个类,编写了一个Windows窗口应用程序。本节我们来探究MFC的消息映射机制,在上一节程序的基础上增加响应鼠标WM_LBUTTONDOWN的消息。
增加消息处理能力
Win32原始消息机制
在此之前,我们先来回忆一下Win32SDK程序中与消息处理相关的部分:
- WinMain函数中,注册窗口类时,传入了窗口过程WndProc的地址。然后创建窗口、显示窗口、更新窗口。
- 完成上述步骤后,进入消息循环。调用user32!GetMessage或user32!PeekMessage、user32!TranslateMessage完成消息的获取和整理后,调用user32!DispatchMessage,将控制权交还给系统,由系统回调第一步注册窗口类时传入的WndProc函数。
- WndProc被系统调用,根据传入的消息不同,分别加以处理,形式上是一个switch-case或多级的if-else。对于我们不关心的消息,再交调用DefWindowProc交给系统处理。(另外,在创建窗口、显示窗口、更新窗口的时候,虽然尚未进入消息循环,但系统会直接调用我们的窗口过程处理一些与窗口创建相关的消息)
- 当关闭窗口时,调用DestroyWindow销毁窗口,然后调用PostQuitMessage退出消息循环,程序结束。
MFC消息映射机制初探
而在MFC中,处理消息的机制被称为消息映射机制,通过一组宏来完成,下面热烈欢迎它们登场:
DECLARE_MESSAGE_MAP()
BEGIN_MESSAGE_MAP(CChild, CBase)
END_MESSAGE_MAP()
ON_COMMAND()
ON_WM_XXX()
其中:
1. DECLARE_MESSAGE_MAP用于声明该类具有处理消息的资格和能力,放在该类的类声明中。
2. BEGIN_MESSAGE_MAP和END_MESSAGE_MAP成对出现,自成一体。BEGIN_MESSAGE_MAP有两个参数,分别是本类名称和父类名称。
在二者中间用ON_WM_XXX或ON_COMMAND将要处理的消息和消息处理函数绑定。
3. 这些消息映射的宏和函数的名字是固定的,因为这些是常用的系统消息。有这样的名字的函数和宏会自己关联。
4. 任何CCmdTarget及其派生类都可以通过添加上述的宏从而进行消息处理。但CCmdTarget类能处理的消息类型有限,大多数消息都需要由CWnd及其派生类来处理。
增补代码
在上节代码基础上,增加以下部分:
class CMyWnd : public CFrameWnd
{
public:
//类原有内容
void OnLButtonDown(UINT nFlags, CPoint point)
{
MessageBox(TEXT("In CMyWnd::OnLButtonDown"), TEXT("In CMyWnd::OnLButtonDown"), MB_OK);
}
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CMyWnd, CFrameWnd)
ON_WM_LBUTTONDOWN()
END_MESSAGE_MAP()
需要注意的是上述所有宏都要紧跟一对括号。
一探究竟
DECLARE_MESSAGE_MAP()宏
下面看看DECLARE_MESSAGE_MAP()宏的定义:
#define DECLARE_MESSAGE_MAP() \
protected: \
static const AFX_MSGMAP* PASCAL GetThisMessageMap(); \
virtual const AFX_MSGMAP* GetMessageMap() const; \
这个宏的作用就是在类声明中加入了两个函数,其中一个名为GetThisMessageMap的函数是静态函数,另一个名为GetMessageMap的函数为虚函数,两个函数都没有参数。注意到其返回值都是AFX_MSGMAP的指针,那这又是什么鬼呢?
struct AFX_MSGMAP
{
const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();
const AFX_MSGMAP_ENTRY* lpEntries;
};
该结构包含两个成员,第一个成员是一个函数指针,由名称猜出应该是得到基类中该结构的指针。
第二个成员又是一个结构指针,看名称中lpEntries用了复数,估计是指向了一个结构数组。先看看该结构:
#ifndef AFX_MSG_CALL
#define AFX_MSG_CALL
#endif
typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);
struct AFX_MSGMAP_ENTRY
{
UINT nMessage; // windows message
UINT nCode; // control code or WM_NOTIFY code
UINT nID; // control ID (or 0 for windows messages)
UINT nLastID; // used for entries specifying a range of control id's
UINT_PTR nSig; // signature type (action) or pointer to message #
AFX_PMSG pfn; // routine to call (or special value)
};
在其中,我们发现了nMessage和pfn,这应该对应的是一种映射关系,将消息与处理消息函数的入口地址建立一个映射关系。且pfn是CCmdTarget的成员函数。这也从侧面应证了CCmdTarget及其子类具有消息处理的能力。
在该宏中,我们没有找到GetThisMessageMap以及GetMessageMap的定义,预知后事如何,且看BEGIN_MESSAGE_MAP()宏的解析。
BEGIN_MESSAGE_MAP()宏与END_MESSAGE_MAP()宏
#define BEGIN_MESSAGE_MAP(theClass, baseClass) \
PTM_WARNING_DISABLE \
const AFX_MSGMAP* theClass::GetMessageMap() const \
{ return GetThisMessageMap(); } \
const AFX_MSGMAP* PASCAL theClass::GetThisMessageMap() \
{ \
typedef theClass ThisClass; \
typedef baseClass TheBaseClass; \
static const AFX_MSGMAP_ENTRY _messageEntries[] = \
{
#define END_MESSAGE_MAP() \
{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \
}; \
static const AFX_MSGMAP messageMap = \
{ &TheBaseClass::GetThisMessageMap, &_messageEntries[0] }; \
return &messageMap; \
} \
PTM_WARNING_RESTORE
哈,这两个宏真有意识。原来在两个宏中间定义的代码全部在GetThisMessageMap函数体内,准确的说是在GetThisMessageMap函数体内定义的AFX_MSGMAP_ENTRY类型的结构数组_messageEntries的条目中。那么ON_WM_LBUTTONDOWN应该是在这个数组中插入了一个新条目喽,查看其宏定义加以验证:
enum AfxSig
{
//省略
AfxSig_v_u_p, // void (UINT, CPoint)
AfxSig_vwp = AfxSig_v_u_p, // void (UINT, CPoint)
//省略
}
#define ON_WM_LBUTTONDOWN() \
{ WM_LBUTTONDOWN, 0, 0, 0, AfxSig_vwp, \
(AFX_PMSG)(AFX_PMSGW) \
(static_cast< void (AFX_MSG_CALL CWnd::*)(UINT, CPoint) > ( &ThisClass :: OnLButtonDown)) },
到这里我们终于知道了为什么添加了ON_WM_LBUTTONDOWN()宏后就能与CMyWnd类中的OnLButtonDown函数建立起联系了。原来这名字是MFC框架的编写者们早就定好的了。
并且我们还注意到,WM_LBUTTONDOWN消息对应的处理函数必须在CWnd类中加以定义(或其子类中),也就是说,只有CWnd及其子类才能处理WM_LBUTTONDOWN消息。翻看afxmsg_.h中的定义,绝大多数Windows消息都只能由CWnd类或其子类来处理。
只有WM_COMMAND消息和WM_NOTIFY消息可由CCmdTarget或其子类处理。此外,我们还看到了MFC定义的WM_REFLECT_BASE消息,即所谓的反射类消息。关于这些内容我们将在后续章节加以介绍。
在END_MESSAGE_MAP中,将AFX_MSGMAP结构中的pfnGetBaseMap函数指针设定为了父类的GetThisMessageMap函数。
因此,虚函数GetThisMessageMap中定了该类的消息-处理函数映射表并返回其指针,而GetMessageMap就是该类的虚函数(意味着可以用其基类的指针调用),在其中调用了该类的静态函数GetThisMessageMap。
小结
总结一下:
* DECLARE_MSG_MAP宏在类声明中添加了两个成员函数:GetThisMessageMap(静态函数)与GetMessageMap(虚函数)的声明
* BEGIN_MSG_MAP宏中实现了GetMessageMap,就是调用GetThisMessageMap(静态函数);部分实现了GetThisMessageMap函数:创建了一个AFX_MSGMAP_ENTRY的结构数组,该数组用于将消息和处理消息的函数一一对应。
* ON_WM_XXX宏和ON_COMMAND等宏其实就是在AFX_MSGMAP_ENTRY结构数组中增加了一个条目,将目标消息和已经定义好名称和函数原型的函数(该函数需要在类中实现)对应起来
* END_MSG_MAP宏在AFX_MSGMAP结构数组的末尾增加了一个全0的条目作为结尾,然后构造了一个静态的AFX_MSGMAP结构,将其中的pfnGetBaseMap字段填入父类的GetThisMessageMap函数地址,在lpEntries字段中填入刚构建的AFX_MSGMAP_ENTRY结构数组的地址并返回。