深入解析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.觀察者模式(文檔/視圖)