这个函数把下一步的工作交给OnWndMsg函数来处理。如果OnWndMsg没有处理,则交给DefWindowProc来处理。
OnWndMsg和DefWindowProc都是CWnd类的虚拟函数。
OnWndMsg的原型如下:
BOOL CWnd::OnWndMsg( UINT message,
WPARAM wParam, LPARAM lParam,RESULT*pResult );
该函数是虚拟函数。
和WindowProc一样,由于当前对象的类CTview没有覆盖该函数,所以CWnd的OnWndMsg被调用。
在CWnd中,MFC使用OnWndMsg来分别处理各类消息:
如果是WM_COMMAND消息,交给OnCommand处理;然后返回。
如果是WM_NOTIFY消息,交给OnNotify处理;然后返回。
如果是WM_ACTIVATE消息,先交给_AfxHandleActivate处理(后面5.3.3.7节会解释它的处理),再继续下面的处理。
如果是WM_SETCURSOR消息,先交给_AfxHandleSetCursor处理;然后返回。
如果是其他的Windows消息(包括WM_ACTIVATE),则
首先在消息缓冲池进行消息匹配,
若匹配 成功 ,则调用相应的消息处理函数;
若不成功,则在消息目标的消息映射数组中进行查找匹配,看它是否处理当前消息。这里,消息目标即CTview对象。
如果消息目标处理了该消息,则会匹配到消息处理函数,调用它进行处理;
否则,该消息没有被应用程序处理,OnWndMsg返回FALSE。
关于Windows消息和消息处理函数的匹配,见下一节。
缺省处理函数DefWindowProc将在讨论对话框等的实现时具体 分析 。
Windows消息的查找和匹配
CWnd或者派生类的对象调用OnWndMsg搜索本对象或者基类的消息映射数组,寻找当前消息的消息处理函数。如果当前对象或者基类处理了当前消息,则必定在其中一个类的消息映射数组中匹配到当前消息的处理函数。
消息匹配是一个比较耗时的任务,为了提高效率,MFC设计了一个消息缓冲池,把要处理的消息和匹配到的消息映射条目(条目包含了消息处理函数的地址)以及进行消息处理的当前类等 信息 构成一条缓冲信息,放到缓冲池中。如果以后又有同样的消息需要同一个类处理,则直接从缓冲池查找到对应的消息映射条目就可以了。
MFC用哈希查找来查询消息映射缓冲池。消息缓冲池相当于一个哈希表,它是应用程序的一个全局变量,可以放512条 最新 用到的消息映射条目的缓冲信息,每一条缓冲信息是哈希表的一个入口。
采用AFX_MSG_CACHE结构描述每条缓冲信息,其定义如下:
struct AFX_MSG_CACHE
{
UINT nMsg;
const AFX_MSGMAP_ENTRY* lpEntry;
const AFX_MSGMAP* pMessageMap;
};
nMsg存放消息ID,每个哈希表入口有不同的nMsg。
lpEnty存放和消息ID匹配的消息映射条目的地址,它可能是this所指对象的类的映射条目,也可能是这个类的某个基类的映射条目,也可能是空。
pMessageMap存放消息处理函数匹配成功时进行消息处理的当前类(this所指对象的类)的静态成员变量messageMap的地址,它唯一的标识了一个类(每个类的messageMap变量都不一样)。
this所指对象是一个CWnd或其派生类的实例,是正在处理消息的MFC窗口对象。
哈希查找:使用消息ID的值作为关键值进行哈希查找,如果成功,即可从lpEntry获得消息映射条目的地址,从而得到消息处理函数及其原型。
如何判断是否成功匹配呢?有两条标准:
第一,当前要处理的消息message在哈希表(缓冲池)中有入口;第二,当前窗口对象(this所指对象)的类的静态变量messageMap的地址应该等于本条缓冲信息的pMessagMap。MFC通过虚拟函数GetMessagMap得到messageMap的地址。
如果在消息缓冲池中没有找到匹配,则搜索当前对象的消息映射数组,看是否有合适的消息处理函数。
如果匹配到一个消息处理函数,则把匹配结果加入到消息缓冲池中,即填写该条消息对应的哈希表入口:
nMsg=message;
pMessageMap=this->GetMessageMap;
lpEntry=查找结果
然后,调用匹配到的消息处理函数。否则(没有找到),使用_GetBaseMessageMap得到基类的消息映射数组,查找和匹配;直到匹配成功或搜寻了所有的基类(到CCmdTarget)为止。
如果最后没有找到,则也把该条消息的匹配结果加入到缓冲池中。和匹配成功不同的是:指定lpEntry为空。这样OnWndMsg返回,把控制权返还给AfxCallWndProc函数,AfxCallWndProc将继续调用DefWndProc进行缺省处理。
消息映射数组的搜索在CCmdTarget::OnCmdMsg函数中也用到了,而且算法相同。为了提高速度,MFC把和消息映射数组条目逐一比较、匹配的函数AfxFindMessageEntry用汇编书写。
const AFX_MSGMAP_ENTRY* AFXAPI
AfxFindMessageEntry(const AFX_MSGMAP_ENTRY* lpEntry,
UINT nMsg, UINT nCode, UINT nID)
第一个参数是要搜索的映射数组的入口;第二个参数是Windows消息标识;第三个参数是控制通知消息标识;第四个参数是命令消息标识。
对Windows消息来说,nMsg是每条消息不同的,nID和nCode为0。
对命令消息来说,nMsg固定为WM_COMMAND,nID是每条消息不同,nCode都是CN_COMMAND(定义为0)。
对控制通知消息来说,nMsg固定为WM_COMMAND或者WM_NOTIFY,nID和nCode是每条消息不同。
对于Register消息,nMsg指定为0XC000,nID和nCode为0。在使用函数AfxFindMessageEntry得到匹配结果之后,还必须判断nSig是否等于message,只有相等才调用对应的消息处理函数。
Windows消息处理函数的调用
对一个Windows消息,匹配到了一个消息映射条目之后,将调用映射条目所指示的消息处理函数。
调用处理函数的过程就是转换映射条目的pfn指针为适当的函数类型并执行它:MFC定义了一个成员函数指针mmf,首先把消息处理函数的地址赋值给该函数指针,然后根据消息映射条目的nSig值转换指针的类型。但是,要给函数指针mmf赋值,必须使该指针可以指向所有的消息处理函数,为此则该指针的类型是所有类型的消息处理函数指针的联合体。
对上述过程,MFC的实现大略如下:
union MessageMapFunctions mmf;
mmf.pfn = lpEntry->pfn;
swithc (value_of_nsig){
…
case AfxSig_is: //OnCreate就是该类型
lResult = (this->*mmf.pfn_is)((LPTSTR)lParam);
break;
…
default:
ASSERT(FALSE); break;
}
…
LDispatchRegistered: // 处理registered windows messages
ASSERT(message >= 0xC000);
mmf.pfn = lpEntry->pfn;
lResult = (this->*mmf.pfn_lwl)(wParam, lParam);
…
如果消息处理函数有返回值,则返回该结果,否则,返回TRUE。
对于图4-1所示的例子,nSig等于AfxSig_is,所以将执行语句
(this->*mmf.pfn_is)((LPTSTR)lParam)
也就是对CTview::OnCreate的调用。
顺便指出,对于Registered窗口消息,消息处理函数都是同一原型,所以都被转换成lwl型(关于Registered窗口消息的映射,见4.4.2节)。
综上所述,标准Windwos消息和应用程序消息中的Registered消息,由窗口过程直接调用相应的处理函数处理:
如果某个类型的窗口(C++类)处理了某条消息(覆盖了CWnd或直接基类的处理函数),则对应的HWND窗口(Winodws window)收到该消息时就调用该覆盖函数来处理;如果该类窗口没有处理该消息,则调用实现该处理函数最直接的基类(在C++的类层次上接近该类)来处理,上述例子中如果CTview不处理WM_CREATE消息,则调用上一层的CWnd::OnCreate处理;
如果基类都不处理该消息,则调用DefWndProc来处理。
消息映射机制完成虚拟函数功能的原理
综合对Windows消息的处理来看,MFC使用消息映射机制完成了C++虚拟函数的功能。这主要基于以下几点:
所有处理消息的类从CCmdTarget派生。
使用静态成员变量_messageEntries数组存放消息映射条目,使用静态成员变量messageMap来唯一地区别和得到类的消息映射。
通过GetMessage虚拟函数来获取当前对象的类的messageMap变量,进而得到消息映射入口。
按照先底层,后基层的顺序在类的消息映射数组中搜索消息处理函数。基于这样的机制,一般在覆盖基类的消息处理函数时,应该调用基类的同名函数。
以上论断适合于MFC其他消息处理机制,如对命令消息的处理等。不同的是其他消息处理有一个命令派发/分发的过程。
下一节,讨论命令消息的接受和处理。
对命令消息的接收和处理
MFC标准命令消息的发送
在SDI或者MDI应用程序中,命令消息由用户界面对象(如菜单、工具条等)产生,然后送给主边框窗口。主边框窗口使用标准MFC窗口过程处理命令消息。窗口过程把命令传递给MFC主边框窗口对象,开始命令消息的分发。MFC边框窗口类CFrameWnd提供了消息分发的能力。
下面,还是通过一个例子来说明命令消息的处理过程。
使用AppWizard产生一个单文档应用程序t。从help菜单选择“About”,就会弹出一个ABOUT对话框。下面,讨论从命令消息的发出到对话框弹出的过程。
首先,选择“ About”菜单项的动作导致一个Windows命令消息ID_APP_ABOUT的产生。Windows系统发送该命令消息到边框窗口,导致它的窗口过程AfxWndProc被调用,参数1是边框窗口的句柄,参数2是消息ID(即WM_COMMAND),参数3、4是消息参数,参数3的值是ID_APP_ABOUT。接着的系列调用如图4-3所示。
下面分别讲述每一层所调用的函数。
前4步同对Windows消息的处理。这里接受消息的HWND窗口是主边框窗口,因此,AfxWndProc根据HWND句柄得到的MFC窗口对象是MFC边框窗口对象。
在4.2.2节谈到,如果CWnd::OnWndMsg判断要处理的消息是命令消息(WM_COMMAND),就调用OnCommand进一步处理。由于OnCommand是虚拟函数,当前MFC窗口对象是边框窗口对象,它的类从CFrameWnd类导出,没有覆盖CWnd的虚拟函数OnCommand,而CFrameWnd覆盖了CWnd的OnCommand,所以,CFrameWnd的OnCommand被调用。换句话说,CFrameWnd的OnCommand被调用是动态约束的结果。接着介绍的本例子的有关调用,也是通过动态约束而实际发生的函数调用。接着的有关调用,将不进行为什么调用某个类的虚拟或者消息处理函数的分析。
(1)CFrameWnd的OnCommand函数
BOOL CFrameWnd::OnCommand(WPARAM wParam, LPARAM lParam)
参数wParam的低阶word存放了菜单命令nID或控制子窗口ID;如果消息来自控制窗口,高阶word存放了控制通知消息;如果消息来自加速键,高阶word值为1;如果消息来自菜单,高阶word值为0。
如果是通知消息,参数lParam存放了控制窗口的句柄hWndCtrl,其他情况下lParam是0。
在这个例子里,低阶word是ID_APP_ABOUT,高阶word是1;lParam是0。
MFC对CFrameWnd的缺省实现主要是获得一个机会来检查程序是否运行在HELP状态,需要执行上下文帮助,如果不需要,则调用基类的CWnd::OnCommand实现正常的命令消息发送。
(2)CWnd的OnCommand函数
BOOL CWnd::OnCommand(WPARAM wParam, LPARAM lParam)
它按一定的顺序处理命令或者通知消息,如果发送成功,返回TRUE,否则,FALSE。处理顺序如下:
如果是命令消息,则调用OnCmdMsg(nID, CN_UPDATE_COMMAND_UI, &state, NULL)测试nID命令是否已经被禁止,如果这样,返回FALSE;否则,调用OnCmdMsg进行命令发送。关于CN_UPDATE_COMMAND_UI通知消息,见后面用户界面状态的更新处理。
如果是控制通知消息,则先用ReflectLastMsg反射通知消息到子窗口。如果子窗口处理了该消息,则返回TRUE;否则,调用OnCmdMsg进行命令发送。关于通知消息的反射见后面4.4.4.3节。OnCommand给OnCmdMsg传递四个参数:nID,即命令消息ID;nCode,如果是通知消息则为通知代码,如果是命令消息则为NC_COMMAND(即0);其余两个参数为空。
(3)CFrameWnd的OnCmdMsg函数
BOOL CFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
参数1是命令ID;如果是通知消息(WM_COMMAND或者WM_NOTIFY),则参数2表示通知代码,如果是命令消息,参数2是0;如果是WM_NOTIFY,参数3包含了一些额外的信息;参数4在正常消息处理中应该是空。
在这个例子里,参数1是命令ID,参数2为0,参数3空。
OnCmdMsg是虚拟函数,CFrameWnd覆盖了该函数,当前对象(this所指)是MFC单文档的边框窗口对象。故CFrameWnd的OnCmdMsg被调用。CFrameWnd::OnCmdMsg在MFC消息发送中占有非常重要的地位,MFC对该函数的缺省实现确定了MFC的标准命令发送路径:
送给活动(Active)视处理,调用活动视的OnCmdMsg。由于当前对象是MFC视对象,所以,OnCmdMsg将搜索CTview及其基类的消息映射数组,试图得到相应的处理函数。
如果视对象自己不处理,则视得到和它关联的文档,调用关联文档的OnCmdMsg。由于当前对象是MFC视对象,所以,OnCmdMsg将搜索CTdoc及其基类的消息映射数组,试图得到相应的处理函数。
如果文档对象不处理,则它得到管理文档的文档模板对象,调用文档模板的OnCmdMsg。由于当前对象是MFC文档模板对象,所以,OnCmdMsg将搜索文档模板类及其基类的消息映射数组,试图得到相应的处理函数。
如果文档模板不处理,则把没有处理的信息逐级返回:文档模板告诉文档对象,文档对象告诉视对象,视对象告诉边框窗口对象。最后,边框窗口得知,视、文档、文档模板都没有处理消息。
CFrameWnd的OnCmdMsg继续调用CWnd::OnCmdMsg(斜体表示有类属限制)来处理消息。由于CWnd没有覆盖OnCmdMsg,故实际上调用了函数CCmdTarget::OnCmdMsg。由于当前对象是MFC边框窗口对象,所以OnCmdMsg函数将搜索CMainFrame类及其所有基类的消息映射数组,试图得到相应的处理函数。CWnd没有实现OnCmdMsg却指定要执行其OnCmdMsg函数,可能是为了以后MFC给CWnd实现了OnCmdMsg之后其他代码不用改变。
这一步是边框窗口自己尝试处理消息。
如果边框窗口对象不处理,则送给应用程序对象处理。调用CTApp的OnCmdMsg,由于实际上CTApp及其基类CWinApp没有覆盖OnCmdMsg,故实际上调用了函数CCmdTarget::OnCmdMsg。由于当前对象是MFC应用程序对象,所以OnCmdMsg函数将搜索CTApp类及其所有基类的的消息映射入口数组,试图得到相应的处理函数
如果应用程序对象不处理,则返回FALSE,表明没有命令目标处理当前的命令消息。这样,函数逐级别返回,OnCmdMsg告诉OnCommand消息没有被处理,OnCommand告诉OnWndMsg消息没有被处理,OnWndMsg告诉WindowProc消息没有被处理,于是WindowProc调用DefWindowProc进行缺省处理。
本例子在第六步中,应用程序对ID_APP_ABOUT消息作了处理。它找到处理函数CTApp::OnAbout,使用DispatchCmdMsg派发消息给该函数处理。
如果是MDI边框窗口,标准发送路径还有一个环节,该环节和第二、三、四步所涉及的OnCmdMsg函数,将在下两节再次具体分析。
命令消息的派发和消息的多次处理
命令消息的派发
如前3.1所述,CCmdTarget的静态成员函数DispatchCmdMsg用来派发命令消息给指定的命令目标的消息处理函数。
static BOOL DispatchCmdMsg(CCmdTarget* pTarget,
UINT nID, int nCode,
AFX_PMSG pfn, void* pExtra, UINT nSig,
AFX_CMDHANDLERINFO* pHandlerInfo)
前面在讲CCmdTarget时,提到了该函数。这里讲述它的实现:
第一个参数指向处理消息的对象;第二个参数是命令ID;第三个是通知消息等;第四个是消息处理函数地址;第五个参数用于存放一些有用的信息,根据nCode的值表示不同的意义,例如当消息是WM_NOFITY,指向一个NMHDR结构(关于WM_NOTIFY,参见4.4.4.2节通知消息的处理);第六个参数标识消息处理函数原型;第七个参数是一个指针,指向AFX_CMDHANDLERINFO结构。前六个参数(除了第五个外)都是向函数传递信息,第五个和第七个参数是双向的,既向函数传递信息,也可以向调用者返回信息。
关于AFX_CMDHANDLERINFO结构:
struct AFX_CMDHANDLERINFO
{
CCmdTarget* pTarget;
void (AFX_MSG_CALL CCmdTarget::*pmf)(void);
};
第一个成员是一个指向命令目标对象的指针,第二个成员是一个指向CCmdTarget成员函数的指针。
该函数的实现流程可以如下描述:
首先,它检查参数pHandlerInfo是否空,如果不空,则用pTarget和pfn填写其指向的结构,返回TRUE;通常消息处理时传递来的pHandlerInfo空,而在使用OnCmdMsg来测试某个对象是否处理某条命令时,传递一个非空的pHandlerInfo指针。若返回TRUE,则表示可以处理那条消息。
如果pHandlerInfo空,则进行消息处理函数的调用。它根据参数nSig的值,把参数pfn的类型转换为要调用的消息处理函数的类型。这种指针转换技术和前面讲述的Windows消息的处理是一样的。
消息的多次处理
如果消息处理函数不返回值,则DispatchCmdMsg返回TRUE;否则,DispatchCmdMsg返回消息处理函数的返回值。这个返回值沿着消息发送相反的路径逐级向上传递,使得各个环节的OnCmdMsg和OnCommand得到返回的处理结果:TRUE或者FALSE,即成功或者失败。
这样就产生了一个问题,如果消息处理函数有意返回一个FALSE,那么不就传递了一个错误的信息?例如,OnCmdMsg函数得到FALSE返回值,就认为消息没有被处理,它将继续发送消息到下一环节。的确是这样的,但是这不是MFC的漏洞,而是有意这么设计的,用来处理一些特别的消息映射宏,实现同一个消息的多次处理。
通常的命令或者通知消息是没有返回值的(见4.4.2节的消息映射宏),仅仅一些特殊的消息处理函数具有返回值,这类消息的消息处理函数是使用扩展消息映射宏映射的,例如:
ON_COMMAND对应的ON_COMMAND_EX
扩展映射宏和对应的普通映射宏的参数个数相同,含义一样。但是扩展映射宏的消息处理函数的原型和对应的普通映射宏相比,有两个不同之处:一是多了一个UINT类型的参数,另外就是有返回值(返回BOOL类型)。回顾4.4.2章节,范围映射宏ON_COMMAND_RANGE的消息处理函数也有一个这样的参数,该参数在两处的含义是一样的,例如:命令消息扩展映射宏ON_COMMAND_EX定义的消息处理函数解释该参数是当前要处理的命令消息ID。有返回值的意义在于:如果扩展映射宏的消息处理函数返回FALSE,则导致当前消息被发送给消息路径上的下一个消息目标处理。
综合来看,ON_COMMAND_EX宏有两个功能:
一是可以把多个命令消息指定给一个消息处理函数处理。这类似于ON_COMMAND_RANGE宏的作用。不过,这里的多条消息的命令ID或者控制子窗口ID可以不连续,每条消息都需要一个ON_COMMAND_EX宏。
二是可以让几个消息目标处理同一个命令或者通知或者反射消息。如果消息发送路径上较前的命令目标不处理消息或者处理消息后返回FALSE,则下一个命令目标将继续处理该消息。
对于通知消息、反射消息,它们也有扩展映射宏,而且上述论断也适合于它们。例如:
ON_NOTIFY对应的ON_NOTIFY_EX
ON_CONTROL对应的ON_CONTROL_EX
ON_CONTROL_REFLECT对应的ON_CONTROL_REFLECT_EX
等等。
范围消息映射宏也有对应的扩展映射宏,例如:
ON_NOTIFY_RANGE对应的ON_NOTIFY_EX_RANGE
ON_COMMAND_RANGE对应的ON_COMMAND_EX_RANGE
使用这些宏的目的在于利用扩展宏的第二个功能:实现消息的多次处理。
关于扩展消息映射宏的例子,参见13.2..4.4节和13.2.4.6节。
一些消息处理类的OnCmdMsg的实现
从以上论述知道,OnCmdMsg虚拟函数在MFC命令消息的发送中扮演了重要的角色,CFrameWnd的OnCmdMsg实现了MFC的标准命令消息发送路径。
那么,就产生一个问题:如果命令消息不送给边框窗口对象,那么就不会有按标准命令发送路径发送消息的过程?答案是肯定的。例如一个菜单被一个对话框窗口所拥有,那么,菜单命令将送给MFC对话框窗口对象处理,而不是MFC边框窗口处理,当然不会和CFrameWnd的处理流程相同。
但是,有一点需要指出,一般标准的SDI和MDI应用程序,只有主边框窗口拥有菜单和工具条等用户接口对象,只有在用户与用户接口对象进行交互时,才产生命令,产生的命令必然是送给SDI或者MDI程序的主边框窗口对象处理。
下面,讨论几个MFC类覆盖OnCmdMsg虚拟函数时的实现。这些类的OnCmdMsg或者可能是标准MFC命令消息路径的一个环节,或者可能是一个独立的处理过程(对于其中的MFC窗口类)。
从分析CView的OnCmdMsg实现开始。
CView的OnCmdMsg
CView::OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
首先,调用CWnd::OnCmdMsg,结果是搜索当前视的类和基类的消息映射数组,搜索顺序是从下层到上层。若某一层实现了对命令消息nID的处理,则调用它的实现函数;否则,调用m_pDocument->OnCmdMsg,把命令消息送给文档类处理。m_pDocument是和当前视关联的文档对象指针。如果文档对象类实现了OnCmdMsg,则调用它的覆盖函数;否则,调用基类(例如CDocument)的OnCmdMsg。
接着,讨论CDocument的实现。
CDocument的 OnCmdMsg
BOOL CDocument::OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
首先,调用CCmdTarget::OnCmdMsg,导致当前对象(this)的类和基类的消息映射数组被搜索,看是否有对应的消息处理函数可用。如果有,就调用它;如果没有,则调用文档模板的OnCmdMsg函数(m_pTemplate->OnCmdMsg)把消息送给文档模板处理。
MFC文档模板没有覆盖OnCmdMsg,导致基类CCmdTarget的OnCmdMsg被调用,看是否有文档模板类或基类实现了对消息的处理。是的话,调用对应的消息处理函数,否则,返回FALSE。从前面的分析知道,CCmdTarget类的消息映射数组是空的,所以这里返回FALSE。
CDialog的OnCmdMsg
BOOL CDialog::OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
调用CWnd::OnCmdMsg,让对话框或其基类处理消息。
如果还没有处理,而且是控制消息或系统命令或非命令按钮,则返回FALSE,不作进一步处理。否则,调用父窗口的OnCmdmsg(GetParent()->OnCmdmsg)把消息送给父窗口处理。
如果仍然没有处理,则调用当前线程的OnCmdMsg(GetThread()->OnCmdMsg)把消息送给线程对象处理。
如果最后没有处理,返回FALSE。
CMDIFrameWnd的OnCmdMsg
对于MDI应用程序,MDI主边框窗口首先是把命令消息发送给活动的MDI文档边框窗口进行处理。MDI主边框窗口对OnCmdMsg的实现函数的原型如下:
BOOL CMDIFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
如果有激活的文档边框窗口,则调用它的OnCmdMsg(MDIGetActive()->OnCmdMsg)把消息交给它进行处理。MFC的文档边框窗口类并没有覆盖OnCmdMsg函数,所以基类CFrameWnd的函数被调用,导致文档边框窗口的活动视、文档边框窗口本身、应用程序对象依次来进行消息处理。
如果文档边框窗口没有处理,调用CFrameWnd::OnCmdMsg把消息按标准路径发送,重复第一次的步骤,不过对于MDI边框窗口来说不存在活动视,所以省却了让视处理消息的必要;接着让MDI边框窗口本身来处理消息,如果它还没有处理,则让应用程序对象进行消息处理──虽然这是一个无用的重复。
除了CView、CDocument和CMDIFrameWnd类,还有几个OLE相关的类覆盖了OnCmdMsg函数。OLE的处理本书暂不涉及,CDialog::OnCmdMsg将在对话框章节专项讨论其具体实现。
一些消息处理类的OnCommand的实现
除了虚拟函数OnCmdMsg,还有一个虚拟函数OnCommand在命令消息的发送中占有重要地位。在处理命令或者通知消息时,OnCommand被MFC窗口过程调用,然后它调用OnCmdMsg按一定路径传送消息。除了CWnd类和一些OLE相关类外,MFC里主要还有MDI边框窗口实现了OnCommand。
BOOL CMDIFrameWnd::OnCommand(WPARAM wParam, LPARAM lParam)
第一,如果存在活动的文档边框窗口,则使用AfxCallWndProc调用它的窗口过程,把消息送给文档边框窗口来处理。这将导致文档边框窗口的OnCmdMsg作如下的处理:
活动视处理消息→与视关联的文档处理消息→本文档边框窗口处理消息→应用程序对象处理消息→文档边框窗口缺省处理
任何一个环节如果处理消息,则不再向下发送消息,处理终止。如果消息仍然没有被处理,就只有交给主边框窗口了。
第二,第一步没有处理命令,继续调用CFrameWnd::OnCommand,将导致CMDIFrameWnd的OnCmdMsg被调用。从前面的分析知道,将再次把消息送给MDI边框窗口的活动文档边框窗口,第一步的过程除了文档边框窗口缺省处理外都将被重复。具体的处理过程见前文的CMDIFrameWnd::OnCmdMsg函数。
对于MDI消息,如果主边框窗口还不处理的话,交给CMDIFrameWnd的DefWindowProc作缺省处理。
消息没有处理,返回FALSE。
上述分析综合了OnCommand和OnCmdMsg的处理,它们是在MFC内部MDI边框窗口处理命令消息的完整的流程和标准的步骤。整个处理过程再次表明了边框窗口在处理命令消息时的中心作用。从程序员的角度来看,可以认为整个标准处理路径如下:
活动视处理消息→与视关联的文档处理消息→本文档边框窗口处理消息→应用程序对象处理消息→文档边框窗口缺省处理→MDI边框窗口处理消息→MDI边框窗口缺省处理
任何一个环节如果处理消息,不再向下发送消息,急处理终止。
对控制通知消息的接收和处理
WM_COMMAND控制通知消息的处理
WM_COMMAND控制通知消息的处理和WM_COMMAND命令消息的处理类似,但是也有不同之处。
首先,分析处理WM_COMMAND控制通知消息和命令消息的相似处。如前所述,命令消息和控制通知消息都是由窗口过程给OnCommand处理(参见CWnd::OnWndMsg的实现),OnCommand通过wParam和lParam参数区分是命令消息或通知消息,然后送给OnCmdMsg处理(参见CWnd::OnCommnd的实现)。
其次,两者的不同之处是:
命令消息一般是送给主边框窗口的,这时,边框窗口的OnCmdMsg被调用;而控制通知消息送给控制子窗口的父窗口,这时,父窗口的OnCmdMsg被调用。
OnCmdMsg处理命令消息时,通过命令分发可以由多种命令目标处理,包括非窗口对象如文档对象等;而处理控制通知消息时,不会有消息分发的过程,控制通知消息最终肯定是由窗口对象处理的。
不过,在某种程度上可以说,控制通知消息由窗口对象处理是一种习惯和约定。当使用ClassWizard进行消息映射时,它不提供把控制通知消息映射到非窗口对象的机会。但是,手工地添加消息映射,让非窗口对象处理控制通知消息的可能是存在的。例如,对于CFormView,一方面它具备接受WM_COMMAND通知消息的条件,另一方面,具备把WM_COMMAND消息派发给关联文档对象处理的能力,所以给CFormView的通知消息是可以让文档对象处理的。
事实上,BN_CLICKED控制通知消息的处理和命令消息的处理完全一样,因为该消息的通知代码是0,ON_BN_CLICKED(id,memberfunction)和ON_COMMAND(id,memberfunction)是等同的。
此外,MFC的状态更新处理机制就是建立在通知消息可以发送给各种命令目标的基础之上的。关于MFC的状态更新处理机制,见后面4.4.4.4节的讨论。
控制通知消息可以反射给子窗口处理。OnCommand判定当前消息是WM_COMAND通知消息之后,首先它把消息反射给控制子窗口处理,如果子窗口处理了反射消息,OnCommand不会继续调用OnCmdMsg让父窗口对象来处理通知消息。
WM_NOTIFY消息及其处理:
(1)WM_NOTIFY消息
还有一种通知消息WM_NOTIFY,在Win32中用来传递信息复杂的通知消息。WM_NOTIFY消息怎么来传递复杂的信息呢?WM_NOTIFY的消息参数wParam包含了发送通知消息的控制窗口ID,另一个参数lParam包含了一个指针。该指针指向一个NMHDR结构,或者更大的结构,只要它的第一个结构成员是NMHDR结构。
NMHDR结构:
typedef struct tagNMHDR {
HWND hwndFrom;
UINT idFrom;
UINT code;
} NMHDR;
上述结构有三个成员,分别是发送通知消息的控制窗口的句柄、ID和通知消息代码。
举一个更大、更复杂的结构例子:列表控制窗发送LVN_KEYDOWN控制通知消息,则lParam包含了一个指向LV_KEYDOWN结构的指针。其结构如下:
typedef struct tagLV_KEYDOWN {
NMHDR hdr;
WORD wVKey;
UINT flags;
}LV_KEYDOWN;
它的第一个结构成员hdr就是NMHDR类型。其他成员包含了更多的信息:哪个键被按下,哪些辅助键(SHIFT、CTRL、ALT等)被按下。
(2)WM_NOTIFY消息的处理
在分析CWnd::OnWndMsg函数时,曾指出当消息是WM_NOTIFY时,它把消息传递给OnNotify虚拟函数处理。这是一个虚拟函数,类似于OnCommand,CWnd和派生类都可以覆盖该函数。OnNotify的函数原型如下:
BOOL CWnd::OnNotify(WPARAM, LPARAM lParam, LRESULT* pResult)
参数1是发送通知消息的控制窗口ID,没有被使用;参数2是一个指针;参数3指向一个long类型的数据,用来返回处理结果。
WM_NOTIFY消息的处理过程如下:
反射消息给控制子窗口处理。
如果子窗口不处理反射消息,则交给OnCmdMsg处理。给OnCmdMsg的四个参数分别如下:第一个是命令消息ID,第四个为空;第二个高阶word是WM_NOTIFY,低阶word是通知消息;第三个参数是指向AFX_NOTIFY结构的指针。第二、三个参数有别于OnCommand送给OnCmdMsg的参数。
AFX_NOTIFY结构:
struct AFX_NOTIFY
{
LRESULT* pResult;
NMHDR* pNMHDR;
};
pNMHDR的值来源于参数2 lParam,该结构的域pResult用来保存处理结果,域pNMHDR用来传递信息。
OnCmdMsg后续的处理和WM_COMMAND通知消息基本相同,只是在派发消息给消息处理函数时,DispatchMsdMsg的第五个参数pExtra指向OnCmdMsg传递给它的AFX_NOTIFY类型的参数,而不是空指针。这样,处理函数就得到了复杂的通知消息信息。
消息反射
(1)消息反射的概念
前面讨论 控制 通知消息时,曾经多次提到了消息反射。MFC提供了两种消息反射机制,一种用于OLE控件,一种用于Windows控制窗口。这里只讨论后一种消息反射。
Windows控制常常发送通知消息给它们的父窗口,通常控制消息由父窗口处理。但是在MFC里头,父窗口在收到这些消息后,或者自己处理,或者反射这些消息给控制窗口自己处理,或者两者都进行处理。如果 程序员 在父窗口类覆盖了通知消息的处理(假定不调用基类的实现),消息将不会反射给控制子窗口。这种反射机制是MFC实现的,便于程序员创建可重用的控制窗口类。
MFC的CWnd类处理以下控制通知消息时,必要或者可能的话,把它们反射给子窗口处理:
WM_CTLCOLOR,
WM_VSCROLL,WM_HSCROLL,
WM_DRAWITEM,WM_MEASUREITEM,
WM_COMPAREITEM,WM_DELETEITEM,
WM_CHARTOITEM,WM_VKEYTOITEM,
WM_COMMAND、WM_NOTIFY。
例如,对WM_VSCROLL、WM_HSCROLL消息的处理,其消息处理函数如下:
void CWnd::OnHScroll(UINT, UINT, CScrollBar* pScrollBar)
{
//如果是一个滚动条控制,首先反射消息给它处理
if (pScrollBar != NULL && pScrollBar->SendChildNotifyLastMsg())
return; //控制窗口成功处理了该消息
Default();
}
又如:在讨论OnCommand和OnNofity函数处理通知消息时,都曾经指出,它们首先调用ReflectLastMsg把消息反射给控制窗口处理。
为了利用消息反射的功能,首先需要从适当的MFC窗口派生出一个控制窗口类,然后使用ClassWizard给它添加消息映射条目,指定它处理感兴趣的反射消息。下面,讨论反射消息映射宏。
上述消息的反射消息映射宏的 命名 遵循以下格式:“ON”前缀+消息名+“REFLECT”后缀,例如:消息WM_VSCROLL的反射消息映射宏是ON_WM_VSCROLL_REFECT。但是通知消息WM_COMMAND和WM_NOTIFY是例外,分别为ON_CONTROL_REFLECT和ON_NOFITY_REFLECT。状态更新通知消息的反射消息映射宏是ON_UPDATE_COMMAND_UI_REFLECT。
消息处理函数的 名字 和去掉“WM_”前缀的消息名相同 ,例如WM_HSCROLL反射消息处理函数是Hscroll。
消息处理函数的原型这里不一一列举了。
这些消息映射宏和消息处理函数的原型可以借助于ClassWizard自动地添加到程序中。ClassWizard添加消息处理函数时,可以处理的反射消息前面有一个等号,例如处理WM_HSCROLL的反射消息,选择映射消息“=EN_HSC ROLL”。ClassWizard自动的添加消息映射宏和处理函数到 框架 文件。
(2)消息反射的处理过程
如果不考虑有OLE控件的情况,消息反射的处理流程如下图所示:
首先,调用CWnd的成员函数SendChildNotifyLastMsg,它从线程状态得到本线程最近一次获取的消息(关于线程状态,后面第9章会详细介绍)和消息参数,并且把这些参数传递给函数OnChildNotify。注意,当前的CWnd对象就是MFC 控制 子窗口对象。
OnChlidNofify是CWnd定义的虚拟函数,不考虑OLE控制的话,它仅仅只调用ReflectChildNotify。OnChlidNofify可以被覆盖,所以如果 程序员 希望处理某个控制的通知消息,除了采用消息映射的方法处理通知反射消息以外,还可以覆盖OnChlidNotify虚拟函数,如果 成功 地处理了通知消息,则返回TRUE。
ReflectChildNotify是CWnd的成员函数,完成反射消息的派发。对于WM_COMMAND,它直接调用CWnd::OnCmdMsg派发反射消息WM_REFLECT_BASE+WM_COMMAND;对于WM_NOTIFY,它直接调用CWnd::OnCmdMsg派发反射消息WM_REFLECT_BASE+WM_NOFITY;对于其他消息,则直接调用CWnd::OnWndMsg(即CmdTarge::OnWndMsg)派发相应的反射消息,例如WM_REFLECT_BASE+WM_HSCROLL。
注意:ReflectChildNotify直接调用了CWnd的OnCmdMsg或OnWndMsg,这样反射消息被直接派发给控制子窗口,省却了消息发送的过程。
接着,控制子窗口如果处理了当前的反射消息,则返回反射消息被成员处理的信息。
(3)一个示例
如果要创建一个编辑框控制,要求它 背景 使用黄色,其他特性不变,则可以从CEdit派生一个类CYellowEdit,处理通知消息WM_CTLCOLOR的反射消息。CYellowEdit有三个属性,定义如下:
CYellowEdit::CYellowEdit()
{
m_clrText = RGB( 0, 0, 0 );
m_clrBkgnd = RGB( 255, 255, 0 );
m_brBkgnd.CreateSolidBrush( m_clrBkgnd );
}
使用ClassWizard添加反射消息处理函数:
函数原型:
afx_msg void HScroll();
消息映射宏:
ON_WM_CTLCOLOR_REFLECT()
函数的框架
HBRUSH CYellowEdit::CtlColor(CDC* pDC, UINT nCtlColor)
{
// TODO:添加 代码 改变设备描述表的属性
// TODO: 如果不再调用父窗口的处理,则返回一个非空的刷子句柄
return NULL;
}
添加一些处理到函数CtlColor中,如下:
pDC->SetTextColor( m_clrText );//设置文本颜色
pDC->SetBkColor( m_clrBkgnd );//设置 背景 颜色
return m_brBkgnd; //返回背景刷
这样,如果某个地方需要使用黄色背景的编辑框,则可以使用CYellowEdit控制。
对更新命令的接收和处理
用户接口对象如菜单、 工具 条有多种状态,例如:禁止,可用,选中,未选中,等等。这些状态随着运行条件的变化,由程序来进行更新。虽然 程序员 可以自己来完成更新,但是MFC框架为自动更新用户接口对象提供了一个方便的接口,使用它对程序员来说可能是一个好的选择。
实现方法
每一个用户接口对象,如菜单、工具条、控制窗口的子窗口,都由唯一的ID号 标识 ,用户和它们交互时,产生相应ID号的命令消息。在MFC里,一个用户接口对象还可以响应CN_UPDATE_COMMAND_UI通知消息。因此,对每个标号ID的接口对象,可以有两个处理函数:一个消息处理函数用来处理该对象产生的命令消息ID,另一个状态更新函数用来处理给该对象的CN_UPDATE_COMMAND_UID的通知消息。
使用ClassWizard可把状态更新函数加入到某个消息处理类,其结果是:
在类的定义中声明一个状态函数;
在消息映射中使用ON_UPDATE_COMMAND_UI宏添加一个映射条目;
在类的实现文件中实现状态更新函数的定义。
ON_UPDATE_COMMAND_UI给指定ID的用户对象指定状态更新函数,例如:
ON_UPDATE_COMMAND_UI(ID_EDIT_COPY, OnUpdateEditCopy)
映射标识号ID为ID_EDIT_COPY菜单的通知消息CN_UPDATE_COMMAND_UI到函数OnUpdateEditCopy。用于给EDIT(编辑菜单)的菜单项ID_EDIT_COPY(复制)添加一个状态处理函数OnUpdateEditCopy,通过处理通知消息CN_UPDATE_COMMAND_UI实现该菜单项的状态更新。
状态处理函数的原型如下:
afxmsg void ClassName::OnUpdateEditPaste(CCmdUI* pCmdUI)
CCmdUI对象由MFC自动地构造。在完善函数的实现时,使用pCmdUI对象和CmdUI的成员函数实现菜单项ID_EDIT_COPY的状态更新,让它变灰或者变亮,也就是禁止或者允许用户使用该菜单项。
状态更新命令消息
要讨论MFC的状态更新处理,先得了解一条特殊的消息。MFC的消息映射机制除了处理各种Windows消息、控制通知消息、命令消息、反射消息外,还处理一种特别的“通知命令消息”,并通过它来更新菜单、工具栏(包括对话框工具栏)等命令目标的状态。
这种“通知命令消息”是MFC内部定义的,消息ID是WM_COMMAND,通知 代码 是CN_UPDATE_COMMAND_UI(0XFFFFFFFF)。
它不是一个真正意义上的通知消息,因为没有控制窗口产生这样的通知消息,而是MFC自己主动产生,用于送给工具条窗口或者主边框窗口,通知它们更新用户接口对象的状态。
它和 标准 WM_COMMAND命令消息也不相同,因为它有特定的通知代码,而命令消息通知代码是0。
但是,从消息的处理角度,可以把它看作是一条通知消息。如果是工具条窗口接收该消息,则在发送机制上它和WM_COMMAND控制通知消息是相同的,相当于让工具条窗口处理一条通知消息。如果是边框窗口接收该消息,则在消息的发送机制上它和WM_COMMAND命令消息是相同的,可以让任意命令目标处理该消息,也就是说边框窗口可以把该条通知消息发送给任意命令目标处理。
从 程序员 的角度,可以把它看作一条“状态更新命令消息”,像处理命令消息那样处理该消息。每条命令消息都可以对应有一条“状态更新命令消息”。ClassWizard也支持让任意消息目标处理“状态更新命令消息”(包括非窗口命令目标),实现用户接口状态的更新。
在这条消息发送时,通过OnCmdMsg的第三个参数pExtra传递一些 信息 ,表示要更新的用户接口对象。pExtra指向一个CCmdUI对象。这些信息将传递给状态更新命令消息的处理函数。
下面讨论用于更新用户接口对象状态的类CCmdUI。
类CCmdUI
CCmdUI不是从CObject派生,没有基类。
成员变量
m_nID 用户接口对象的ID
m_nIndex 用户接口对象的index
m_pMenu 指向CCmdUI对象表示的菜单
m_pSubMenu 指向CCmdUI对象表示的子菜单
m_pOther 指向其他发送通知消息的窗口对象
m_pParentMenu 指向CCmdUI对象表示的子菜单
成员函数
Enable(BOOL bOn = TRUE ) 禁止用户接口对象或者使之可用
SetCheck( int nCheck = 1) 标记 用户接口对象选中或未选中
SetRadio(BOOL bOn = TRUE)
SetText(LPCTSTR lpszText)
ContinueRouting()
还有一个MFC内部使用的成员函数:
DoUpdate(CCmdTarget* pTarget, BOOL bDisableIfNoHndler)
其中,参数1指向处理接收更新通知的命令目标,一般是边框窗口;参数2指示如果没有提供处理函数(例如某个菜单没有对应的命令处理函数),是否禁止用户对象。
DoUpdate作以下事情:
首先,发送状态更新命令消息给参数1表示的命令目标:调用pTarget->OnCmdMsg(m_nID, CN_UPDATE_COMMAND_UI, this, NULL)发送m_nID对象的通知消息CN_UPDATE_COMMAND_UI。OnCmdMsg的参数3取值this,包含了当前要更新的用户接口对象的信息。
然后,如果参数2为TRUE,调用pTarget->OnCmdMsg(m_nID, CN_COMMAND, this, &info)测试命令消息m_nID是否被处理。这时,OnCmdMsg的第四个参数非空,表示仅仅是测试,不是真的要派发消息。如果没有提供命令消息m_nID的处理函数,则禁止用户对象m_nID,否则使之可用。
从上面的讨论可以知道:通过其结构,一个CCmdUI对象标识它表示了哪一个用户接口对象,如果是菜单接口对象,pMenu表示了要更新的菜单对象;如果是工具条,pOther表示了要更新的工具条窗口对象,nID表示了工具条按钮ID。
所以,由参数上状态更新消息的消息处理函数就知道要更新什么接口对象的状态。例如,第1节的函数OnUpdateEditPaste,函数参数pCmdUI表示一个菜单对象,需要更新该菜单对象的状态。
通过其成员函数,一个CCmdUI可以更新、改变用户接口对象的状态。例如,CCmdUI可以管理菜单和对话框控制的状态,调用Enable禁止或者允许菜单或者控制子窗口,等等。
所以,函数OnUpdateEditPaste可以直接调用参数的成员函数(如pCmdUI->Enable)实现菜单对象的状态更新。
由于接口对象的多样性,其他接口对象将从CCmdUI派生出管理自己的类来,覆盖基类的有关成员函数如Enable等,提供对自身状态更新的功能。例如管理状态条和工具栏更新的CStatusCmdUI类和CToolCmdUI类。
自动更新用户接口对象状态的机制
MFC提供了分别用于更新菜单和工具条的两种途径。
更新菜单状态
当用户对菜单如File单击鼠标时,就产生一条WM_INITMENUPOPUP消息,边框窗口在菜单下拉之前响应该消息,从而更新该菜单所有项的状态。
在应用程序开始运行时,边框也会收到WM_INITMENUPOPUP消息。
更新工具条等状态
当应用程序进入空闲处理状态时,将发送WM_IDLEUPDATECMDUI消息,导致所有的工具条用户对象的状态处理函数被调用,从而改变其状态。WM_IDLEUPDATECMDUI是MFC自己定义和使用的消息。
在窗口初始化时,工具条也会收到WM_IDLEUPDATECMDUI消息。
菜单状态更新的实现
MFC让边框窗口来响应WM_INITMENUPOPUP消息,消息处理函数是OnInitMenuPopup,其原型如下:
afx_msg void CFrameWnd::OnInitMenuPopup( CMenu* pPopupMenu,
UINT nIndex, BOOL bSysMenu );
第一个参数指向一个CMenu对象,是当前按击的菜单;第二个参数是菜单索引;第三个参数表示子菜单是否是系统控制菜单。
函数的处理:
如果是系统控制菜单,不作处理;否则,创建CCmdUI对象state,给它的各个成员如m_pMenu,m_pParentMenu,m_pOther等赋值。
对该菜单的各个菜单项,调函数state.DoUpdate,用CCmdUI的DoUpdate来更新状态。DoUpdate的第一个参数是this,表示命令目标是边框窗口;在CFrameWnd的成员变量m_bAutoMenuEnable为TRUE时(表示如果菜单m_nID没有对应的消息处理函数或状态更新函数,则禁止它),把DoUpdate的第二个参数bDisableIfNoHndler置为TRUE。
顺便指出,m_bAutoMenuEnable缺省时为TRUE,所以,应用程序启动时菜单经过初始化处理,没有提供消息处理函数或状态更新函数的菜单项被禁止。
工具条等状态更新的实现
图4-5表示了消息空闲时MFC更新用户对象状态的流程:
MFC提供的缺省空闲处理向顶层窗口(框架窗口)的所有子窗口发送消息WM_IDLEUPDATECMDUI;MFC的控制窗口(工具条、状态栏等)实现了对该消息的处理,导致用户对象状态处理函数的调用。
虽然两种途径调用了同一状态处理函数,但是传递的 CCmdUI参数从内部构成上是不一样的:第一种传递的CCmdUI对象表示了一菜单对象,(pMenu域被赋值);第二种传递了一个窗口对象(pOther域被赋值)。同样的状态改变动作,如禁止、允许状态的改变,前者调用了CMenu的成员函数EnableMenuItem,后者使用了CWnd的成员函数EnabelWindow。但是,这些不同由CCmdUI对象内部区分、处理,对用户是透明的:不论菜单还是对应的工具条,用户都用同一个状态处理函数使用同样的形式来处理。
这一节分析了用户界面更新的原理和机制。在后面第13章讨论工具条和状态栏时,将详细的分析这种机制的具体实现。
到现在为止,详细的讨论了MFC的消息映射机制。但是,为了提高效率和简化处理,MFC提供了一种消息预处理机制,如果一条消息在预处理时被 过滤 掉了(被处理),则不会被派发给目的窗口的窗口过程,更不会进入消息循环了。
显然,能够进行预处理的消息只可能是队列消息,而且必须在消息派发之前进行预处理。因此,MFC在实现消息循环时,对于得到的每一条消息,首先送给目的窗口、其父窗口、其祖父窗口乃至最顶层父窗口,依次进行预处理,如果没有被处理,则进行消息转换和消息派发,如果某个窗口实现了预处理,则终止。有关实现见后面关于CWinThread线程类的章节,CWinThread的Run函数和PreTranslateMessage函数以及CWnd的函数WalkPreTranslateTree实现了上述要求和功能。这里要讨论的是MFC窗口类如何进行消息预处理。
CWnd提供了虚拟函数PreTranslateMessage来进行消息预处理。CWnd的派生类可以覆盖该函数,实现自己的预处理。下面,讨论几个典型的预处理。
首先,是CWnd的预处理:
预处理函数的原型为:
BOOL CWnd::PreTranslateMessage(MSG* pMsg)
CWnd类主要是处理和过滤Tooltips消息。关于该函数的实现和Tooltips消息,见后面第13章关于工具栏的讨论。
然后,是CFrameWnd的预处理:
CFrameWnd除了调用基类CWnd的实现过滤Tooltips消息之外,还要判断当前消息是否是 键盘 快捷键被按下,如果是,则调用函数::TranslateAccelerator(m_hWnd, hAccel, pMsg)处理快捷键。
接着,是CMDIChildWnd的预处理:
CMDIChildWnd的预处理过程和CFrameWnd的一样,但是不能依靠基类CFrameWnd的实现,必须覆盖它。因为MDI子窗口没有菜单,所以它必须在MDI边框窗口的上下文中来处理快捷键,它调用了函数::TranslateAccelerator(GetMDIFrame()->m_hWnd, hAccel, pMsg)。
讨论了MDI子窗口的预处理后,还要讨论MDI边框窗口:
CMDIFrameWnd的实现除了CFrameWnd的实现的功能外,它还要处理MDI快捷键( 标准 MDI界面统一使用的系统快捷键)。
在后面,还会讨论CDialog、CFormView、CToolBar等的消息预处理及其实现。
至于CWnd::WalkPreTranslateTree函数,它从接受消息的窗口开始,逐级向父窗回溯,逐一对各层窗口调用PreTranslateMessage函数,直到消息被处理或者到最顶层窗口为止。
MFC消息映射的回顾
从处理命令消息的过程可以看出,Windows消息和 控制 消息的处理要比命令消息的处理简单,因为查找消息处理函数时,后者只要 搜索 当前窗口对象(this所指)的类或其基类的消息映射入口表。但是,命令消息就要复杂多了,它沿一定的顺序链查找链上的各个命令目标,每一个被查找的命令目标都要搜索它的类或基类的消息映射入口表。
MFC通过消息映射的手段,以一种类似C++虚拟函数的概念向程序员提供了一种处理消息的方式。但是,若使用C++虚拟函数实现众多的消息,将导致虚拟函数表极其庞大;而使用消息映射,则仅仅感兴趣的消息才加入映射表,这样就要节省资源、提高效率。这套消息映射机制的基础包括以下几个方面:
消息映射入口表的实现:采用了C++静态成员和虚拟函数的方法来表示和得到一个消息映射类(CCmdTarget或派生类)的映射表。
消息查找的实现:从低层到高层搜索消息映射入口表,直至根类CCmdTarget。
消息发送的实现:主要以几个虚拟函数为基础来实现标准MFC消息发送路径:OnComamnd、OnNotify、OnWndMsg和OnCmdMsg。、
OnWndMsg是CWnd类或其派生类的成员函数,由窗口过程调用。它处理标准的Windows消息。
OnCommand是CWnd类或其派生类的成员函数,由OnWndMsg调用来处理WM_COMMAND消息,实现命令消息或者控制通知消息的发送。如果派生类覆盖该函数,则必须调用基类的实现,否则将不能自动的处理命令消息映射,而且必须使用该函数接受的参数(不是程序员给定值)调用基类的OnCommand。
OnNotify是CWnd类或其派生类的成员函数,由OnWndMsg调用来处理WM_NOTIFY消息,实现控制通知消息的发送。
OnCmdMsg是CCmdTarget类或其派生类的成员函数。被OnCommand调用,用来实现命令消息发送和派发命令消息到命令消息处理函数。
自动更新用户对象状态是通过MFC的命令消息发送机制实现的。
控制消息可以反射给控制窗口处理。
队列消息在发送给窗口过程之前可以进行消息预处理,如果消息被MFC窗口对象预处理了,则不会进入消息发送过程。