深入解析MFC消息响应和消息路由

深入解析MFC消息响应和消息路由   转载链接 点击打开链接

1.MFC中的消息分为三种

(1)标准消息,也叫窗口消息(例:WM_PAINT,WM_CREATE,WM_LBUTTONDOWN,WM_CHAR)

(2)命令消息,来自菜单,工具栏和加速键,都以WM_COMMAND表示

(3)控件消息,控件消息又分为三小类,第一类和标准消息格式一样,第二类和命令消息格式一样(不过多了一个控件窗口的句柄),第三类是WM_NOTIFY.其具体细节不是本文叙述的重点.

2.为什么要消息路由?什么叫消息路由?

  如果像SDK那样,我们的程序只有一个窗口,一个窗口函数,那哪还有消息路由呢?所有的消息都有一个窗口函数来处理了。至所以要消息路由,是因为MFC程序中有CMyView,CMyDoc,CMyFrameWnd,CMyApp等,MFC框架要做的工作是给用户提供一个机会,让用户可以选择这些类当中的任意一个来处理我们的命令消息。

  注意,消息路由主要是针对上述的第二类消息(命令消息)。对于第一类窗口消息,其消息的接收者是确定的,不需要路由。比如:对于WM_CREATE消息,处理这个消息的类就是产生这个消息的窗口,你不可能让CMyDoc,CMyFrame,CMyApp去处理CView的WM_CREATE消息,那根本不符合逻辑,MFC框架当然也不会让你那么做。而对于来自菜单,工具栏的命令消息,用户是有选择权的,用户可以选择其接收者为View,Doc,App,Frame等当中的任意一个。下面就详细说一下命令消息的路由过程,主要通过分析MFC源代码。

3.首先说一下函数的调用顺序.所有的三类消息初始都被送入一个全局函数AfxWndProc,

之后是pWnd->WindowProc,pWnd->OnWndMsg,在OnWndMsg()中这三类消息分道扬镳了,其中第一类消息由OnWndMsg自己处理,第二类交给了OnCommand(),第三类交给了OnNotify(),下面主要说第二类的处理过程:

 AfxWndProc()

AfxCallWndProc

pWnd->WindowProc(注意,这里的pWnd指向的是产生消息的那个窗口,可能是CMyView,CMyFrameWnd等)

pWnd->OnWndMsg()

pWnd->OnCommand()

....

注意:WindowProc和OnWndMsg这两个函数实际只有CWnd类中才有,在其它类中并没有重写这两个虚函数,所以我们调用的是CWnd::WindowProc()和CWnd::OnWndMsg(),但要注意:它们的this指针是指向我们的程序中的具体类对象(这是下面运用多态的前提)。

下面以多文档为例,解析其对第二类消息的处理过程:

对于多文档来说,命令消息都来自我们自己的主框架窗口(CMyFrameWnd),因为菜单是属于主框架窗口。

AfxWndProc-->CWnd::WindProc()-->CWnd::OnWndMsg()(此处三类消息分道扬镳)-->CMyFrameWnd::OnCommand(不存在)-->CMDIFrameWnd::OnCommand(),

下面是CMDIFrameWnd::OnCommand()的源代码:

{

CMDIChildWnd* pActiveChild = MDIGetActive();

if (pActiveChild != NULL && AfxCallWndProc(pActiveChild,          (1)

  pActiveChild->m_hWnd, WM_COMMAND, wParam, lParam) != 0)

return TRUE; // handled by child

if (CFrameWnd::OnCommand(wParam, lParam)) (2)

return TRUE; // handled through normal mechanism (MDI child or frame)

.....}

父框架窗口是先认子框架窗口来处理,如果其不处理,再自己处理

  那子框架窗口又是如何处理的呢?由于子框架窗口没有重写上面的WindowProc,OnWndMsg,OnCommand,所以最终调用的是其父类CFrameWnd::OnCommand().注意:这里虽然调用了两次CFrameWnd::OnCommand,

但其this指针却不同,一个this指针指向的是CMyChildWnd,一个指向的是CFrameWnd.下面是CFrameWnd::OnCommand()源代码:

{...

CMyView* pView=GetActiveView()

pView->OnCmdMsg() (1.1)

CWnd::OnCmdMsg(); (1.2)

CMyApp* pApp=(CMyApp*)AfxGetApp(); (1.3)

pApp->OnCmdMsg();

.....}

子框架窗口是先让CMyView处理,如果其不处理,再自己处理(CWnd::OnCmdMsg),否则,再 App处理.

  那CMyView又是如何处理的呢,下面是其CView::OnCmdMsg()源代码

{

CWnd::OnCmdMsg(); (1.1.1)

m_pDocument->OnCmdMsg(); (1.1.2)

}

CMyView是先让自己处理,如果其不处理,再让其对应的Document处理,而Document是先自己处理,如果自己不处理,现让CDocTemplate处理(此处这一代码就不在写出,因为通常不会让文档模板处理)

  所谓自己处理,就是调用CCmdTarget::OnCmdMsg(),CWnd并没有重写这个函数,调用CWnd::OnCmdMsg就是调用CCmdTarget::OnCmdMsg(),这个函数的主要干的事情就是:调用GetMessageMap()这个虚函数,获得我们的子类的消息映射表,然后把该消息和此消息映射表对照,看其是否有对应的消息响应函数。如果没有,再看其父类是否有,父类也没有,就返回false,让其它的类来处理了。

通过以上函数调用和返回顺序,可以总结出其消息处理顺序:首先命令消息来自主框架窗口,它把消息交给了子框架窗口,子框架窗口又交给了View,View自己处理,否则就交给文档,所以最终的顺序是:

CMyView--->CMyDoc-->CMultiDocTemplate(它通常不会处理)-->CMyChildFrame-->CMyApp->CMyFrameWnd

注意:这里App先于MainFrame,在有些书上说成是App最后(特纠正)

对於单文档,那就比这简单了,

CMyView-->CMyDoc-->CSingleDocTemplate-->CMyFrame-->CMyApp

注意:在单文档中CMyApp是最后,因为这里的主框架实际占据的是MDI中子框架的位置。

 

 

深入解析MFC消息路由(续)点击打开链接

 

知道了MFC的消息路由的顺序,我们就可以重写某些虚函数,从而改变其路由顺序。比如我们想让CMyChildFrame首先响应菜单消息,就可以重写其OnCommand函数

BOOL CChildFrame::OnCommand(WPARAM wParam, LPARAM lParam)
{
// TODO: Add your specialized code here and/or call the base class
UINT nID = LOWORD(wParam);
int nCode = HIWORD(wParam);
return CCmdTarget::OnCmdMsg(nID, nCode, NULL, NULL); //让其最先响应命令消息

 //return CMDIChildWnd::OnCommand(wParam, lParam);
}
加上这个函数之后,其处理顺序就变成了:

CChildFrame-->CMyView-->CMyDoc-->CMyApp-->CMainFrame.

CMainFrame其实给了两次CChildFrame处理消息的机会,一次是在CMDIFrameWnd::OnCommand()中,另一次是在CMDIFrameWnd::OnCmdMsg()中,两次最终都转变为调用CFrameWnd::OnCmdMsg.

从这也总结出应用程序使用MFC框架的两种方式,也是使用其它框架的主要方式:

1. 重写其虚函数

2. 添加消息响应函数

对于1,我认为又有两种情况:

第一种情况:框架不知道子类的名字,更不知道子类的对象的名字。只是在函数中调用虚函数,利用模板方法来实现。但前提是:必须让被调用的第一个函数的this的指针是指向子类(具体类)的对象。

第二种情况:框架知道子类的对象的名字(比如theApp全局对象,框架是知道的),这时可以直接加上::作用符来调用子类中的函数,如果子类没有重写这个函数,此调用父类的函数。

无论第一种情况,还是第二种情况,都必须解决一个问题:让接口(即框架)与接口的实现(即子类)绑定,框架定义接口,你的应用程序实现接口,说白了也就是要让框架知道你所定义的子类的名字或子类对象的名字。这在设计模式中可以通过抽象工厂+数据驱动方法来实现,在MFC中是通过声明一个全局的theApp对象,加上注册文档模板RUNTIME_CLASS(具体类名)来完成的,我认为MFC中的文档模板就相当于设计模式中的工厂+数据驱动方法

对于2.在子类中添加消息响应函数。

  消息响应函数不是虚函数,函数名字也是用户自已取的,并且函数是在子类中(框架并不知道这个子类的名字),那框架是如何知道调用这个函数的呢? 

  这是因为在我们的子类中,在DECLARE_MESSAGE_MAP,BEGIN_MESSAGE_MAP,END_MESSAGE_MAP中完成了消息映射表的定义,表的填充并重写了CCmdTarget的虚函数GetMessageMap.

所以在CCmdTarget中能够获得我们的子类的消息映射表,从而根据消息映射表来对消息进行处理。

设计模式: ”职责链”+“模板方法”+”文档模板”(工厂+数据驱动)+”观察者”

通过分析,我们可以看出MFC中所使用的最多的几种设计模式

1."职责链"(主要用在消息路由)

2.模板方法

3.文档模板(实质就是工厂+数据驱动,如果是在Java中,就不需要这样做了,因为Java中有反射API,可以直接由类的名字,字符串来生成对象)-->而这一过程的实质就是完成:接口与接口的实现的绑定

4.观察者模式(文档/视图)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章