在讲MFC消息机制之前,先来介绍一下Win32程序的消息处理。
Win32程序的编写流程一般就分为:注册窗口,创建窗口,显示窗口,更新窗口,
消息循环,消息处理。
Win32程序执行起来后,会在如下代码进行消息循环:
while(GetMessage(&msg...)) {//获取消息 TranslateMessage(...);//翻译消息 DispatchMessage(...);//派发消息 }
GetMessage获取的消息来自两种途径:
1. 系统消息队列:鼠标键盘等消息进入系统消息队列。
2. 用户消息队列:用户程序触发的事件消息
DispatchMessage会把消息派发到消息处理函数WndProc由我们处理
其中,我们可以通过PostMessage函数将消息投送至用户消息队列,通过SendMessage将消息直接投递至WndProc函数处理。
(下面我们用模拟的方式讲解MFC的消息映射,相关的宏与函数与MFC中不完全一样,我去掉了与消息映射无关的东西,这里我们只关注消息映射)
Windows 程序靠消息的流动而维护生命。我们已经在上面看到了消息的一般处理方式,
也就是在窗口函数中借着一个大大的switch/case 比对动作,判别消息再调用对应的处理
例程。为了让大大的switch/case 比对动作简化,也让程序代码更模块化一些,MFC
提供了一个消息映射表作法,把消息和其处理例程关联起来。
当我们的类别库成立,如果其中与消息有关的类别(姑且叫作「消息标的类别」好了,
在MFC 之中就是CCmdTarget)都是一条鞭式地继承,我们应该为每一个「消息标的类
别」准备一个消息映射表,并且将基础类别与衍生类别之消息映射表串接起来。然后,
当窗口函数做消息的比对时,我们就可以想办法导引它沿着这条路走过去,
但是,MFC 之中用来处理消息的C++ 类别,并不呈单鞭发展。作为application framework
的重要架构之一的document/view,也具有处理消息的能力(你现在可能还不清楚什么是
document/view,没有关系)。因此,消息藉以攀爬的路线应该有横流的机会。
消息如何流动,我们暂时先不管。是直线前进,或是中途换跑道,我们都暂时不管,我们先
把这个攀爬路线网讲明白。这整个攀爬路线网就是所谓的消息映射表(Message Map);说它是一张地图,当然也没有错。将消息与表格中的元素比对,然后调用对应的处理例程,这种动作我们也称之为消息映射(Message Mapping)。
消息映射表的建立用到了两个重要的数据结构:
struct AFX_MSGMAP { AFX_MSGMAP* pBaseMessageMap; AFX_MSGMAP_ENTRY* lpEntries; };
AFX_MSGMAP可以理解为消息映射表的一个节点。其中pBaseMessageMap成员变量保存上一个节点的地址,lpEntries为消息映射表节点的数据,它将指向一个数组首地址,数组中每一个元素为AFX_MSGMAP_ENTRY,这时大家可能已经猜到了,AFX_MSGMAP_ENTRY这个结构体肯定有成员变量为消息ID和消息处理函数,这样就实现了消息和处理函数的映射。
struct AFX_MSGMAP_ENTRY // MFC 4.0 format { UINT nMessage; // windows message UINT nCode; // control code or WM_NOTIFYcode UINT nID; // control ID (or 0 for windowsmessages) UINT nLastID; // used for entriesspecifying a range of control id's UINT nSig; // signature type (action) orpointer to message # AFX_PMSG pfn; // routine to call (orspecial value) };
AFX_MSGMAP_ENTRY结构包含了
一个消息的所有相关信息,其中
nMessage为Windows消息的ID号
nCode为控制消息的通知码
nID为Windows控制消息的ID
nLastID表示如果是一个指定范围的消息被映射的话,nLastID用来表示它的范围。
nSig表示消息的动作标识
AFX_PMSG pfn 它实际上是一个指向和该消息相应的执行函数的指针。
AFX_PMSG定义为:
typedef void (CCmdTarget::*AFX_PMSG)(void);
有了上面两个结构体,我们可以在每个类中声明一个AFX_MSGMAP_ENTRY数组,用来存储该类要接收的消息和该类中的处理函数的映射关系,然后再声明一个AFX_MSGMAP成员变量,用来指向这个数组和该类的父类的AFX_MSGMAP成员变量,这样就把每个类串起来了。
上面说的这些工作,都由这几个宏来完成:
DECLARE_MESSAGE_MAP和BEGIN_MESSAGE_MAP、END_MESSAGE_MAP
我们来这样定义宏:
#defineDECLARE_MESSAGE_MAP() \ staticAFX_MSGMAP_ENTRY _messageEntries[]; \ static AFX_MSGMAPmessageMap; \ virtual AFX_MSGMAP*GetMessageMap() const;
于是,DECLARE_MESSAGE_MAP就相当于声明了这样一个数据结构:
这个数据结构的内容填塞工作由三个宏完成:
#define BEGIN_MESSAGE_MAP(theClass,baseClass) \ AFX_MSGMAP* theClass::GetMessageMap() const\ { return &theClass::messageMap; } \ AFX_MSGMAP theClass::messageMap = \ { &(baseClass::messageMap), \ (AFX_MSGMAP_ENTRY*)&(theClass::_messageEntries) }; \ AFX_MSGMAP_ENTRYtheClass::_messageEntries[] = \ { #define ON_COMMAND(id, memberFxn) \ { WM_COMMAND, 0, (WORD)id, (WORD)id,AfxSig_vv, (AFX_PMSG)memberFxn }, #define END_MESSAGE_MAP() \ { 0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \ };
其中的AfxSig_end 定义为:
enum AfxSig { AfxSig_end = 0, // [marks end of messagemap] AfxSig_vv, };
注:AfxSig_xx 用来描述消息处理例程memberFxn 的类型(参数与回返值)。
END_MESSAGE_MAP中有一个{ 0, 0,0, 0, AfxSig_end, (AFX_PMSG)0 },它的作用是在消息进行匹配的时候,作为匹配结束标识。
我们以CView 为例来演示一下这几个宏的用法:
// in header file class CView : public CWnd { public: ... DECLARE_MESSAGE_MAP() }; // in implementation file #define CViewid 122 ... BEGIN_MESSAGE_MAP(CView, CWnd) ON_COMMAND(CViewid, 0) END_MESSAGE_MAP()<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
将宏展开我们就清晰了:
// in header file class CView : public CWnd { public: ... static AFX_MSGMAP_ENTRY _messageEntries[]; static AFX_MSGMAP messageMap; virtual AFX_MSGMAP* GetMessageMap() const; }; // in implementation file AFX_MSGMAP* CView::GetMessageMap() const { return &CView::messageMap; } AFX_MSGMAP CView::messageMap = { &(CWnd::messageMap), (AFX_MSGMAP_ENTRY*)&(CView::_messageEntries) }; AFX_MSGMAP_ENTRY CView::_messageEntries[] = { { WM_COMMAND, 0, (WORD)122, (WORD)122, 1,(AFX_PMSG)0 }, { 0, 0, 0, 0, 0, (AFX_PMSG)0 } };
用图表示为:
只要在每个我们想处理消息的类中增加这几个宏,我们的消息就可以很方便的顺着基类向上传递了。
普通消息(COMMAND消息会有横向传递)的传递的过程就是从子类找寻匹配的处理函数,没有则向上传递去父类找寻,我们可以写一个这样的函数(实际这个函数在CCmdTarget类中的OnCmdMsg函数中有,用来找寻匹配COMMAND消息;在CWnd::WindowProc中也有,用来找寻匹配非普通消息)
//传递一个AFX_MSGMAP,它会纵向传递 void MsgMapPrinting(AFX_MSGMAP*pMessageMap) { //纵向传递,每到达一层,调用printlpEntries去找寻匹配 for(; pMessageMap != NULL; pMessageMap =pMessageMap->pBaseMessageMap) { AFX_MSGMAP_ENTRY* lpEntry =pMessageMap->lpEntries; printlpEntries(lpEntry);//这里我们不模拟找寻匹配,只是输出一个值,表示我们来过 } } void printlpEntries(AFX_MSGMAP_ENTRY*lpEntry) { struct { int classid; char* classname; } classinfo[] = { CCmdTargetid, "CCmdTarget", CWinThreadid, "CWinThread", CWinAppid, "CWinApp", CMyWinAppid, "CMyWinApp", CWndid, "CWnd", CFrameWndid, "CFrameWnd", CMyFrameWndid, "CMyFrameWnd", CViewid, "CView", CMyViewid, "CMyView", CDocumentid, "CDocument", CMyDocid, "CMyDoc", 0, " " }; for (int i=0; classinfo[i].classid != 0;i++) { if (classinfo[i].classid ==lpEntry->nID) { cout << lpEntry->nID <<" "; cout << classinfo[i].classname<< endl; break; } } }