首先,先看總體的消息流向圖:程序員
上圖解釋:shell
起點是消息循環,在winmain函數中(mfc中winmain函數是隱含的調用的,在app全局對象構造完後緊接着調用winmain函數),while循環中不斷從應用程序隊列中取消息,當取得一個消息時(含HWnd句柄),調用全局的AfxWndProc窗口函數(含有HWnd句柄做爲參數),這個全局的函數根據HWnd獲得具體的窗體wnd對象,而後調用對象的Cwnd::WindowProc窗口函數(此時不含HWnd句柄做爲參數了)。至此就從全局進入到具體窗體對象的窗口函數了。而後Cwnd::WindowProc遍歷具體窗口類(對象)的消息映射條目集合(又稱爲窗口的消息映射表),一旦對應的條目,就經過對應條目的函數指針成員調用具體的消息處理函數了,可是若是遍歷完後沒找到,則經過messageMap(我稱爲大表map,更準確的說是大節點,由於一個messageMap就是一個鏈表的節點)的pBaseMap成員找到其父類的messageMap,經過這個大節點map的lpEntries成員找到父類的消息映射條目集合表,而後遍歷這個表,若是找到某個條目則執行,而後結束,不然繼續向上一層父類重複上述過程,這就是mfc獨有的消息映射機制,它模仿了虛函數的實現(即從子類向父類的執行流向)。【但之因此不用虛函數而用這麼複雜的機制,應該是若是用虛函數,則每一個類(準確的說是具體的子類)都揹負一個龐大的虛函數表,而這個機制的消息映射條目集合只是按需添加須要處理的消息,效率更高了(對子類來講效率高了,若是是向上傳遞到最頂層的父類的消息查找,則會比虛函數表效率更低)。】數組
另外,補充一下,對於while消息循環app
DispatchMessage(&msg); //把消息路由給(發送給)操做系統,最後由操做系統負責調用具體的消息處理函數(窗口函數)框架
經過上面語句才把消息隊列的消息最終發送到某個窗口函數函數的(好比WndProc),而這些窗口函數不是說定義好後,操做系統就能識別的,必須經過窗口類註冊事後才能被操做系統識別,進而把msg發送到某個註冊過的窗口類對應的窗口函數【應該是經過HWnd來找到註冊過的窗口類的,還記得win32應用程序框架的CreateWindow(LPCTSTR lpClassName,...)函數嗎,說明每一個創建好的窗口都應該保存有窗口類的名字lpClassName】。至於窗口類怎麼和窗口函數對應,就得去複習wn32應用程序框架了,下面簡單複習下:函數
WNDCLASS wc; //定義一個窗口類的實例,即一個空清單工具
wc.lpfnWndProc = WndProc; //窗口函數的首地址記錄下來(窗口類也和一個窗口函數就一一對應了)this
......spa
RegisterClass(&wc); //窗口類實例的註冊操作系統
經過上面的語句即註冊窗口類了,同時這個窗口類也和一個窗口函數一一對應了。
解決了上面所說的操做系統只是把註冊過的窗口類對應的窗口函數的問題後,就有疑問了,是否是每一個窗體包括控件都註冊成了窗體類了,好像沒看到這樣的代碼啊?何況已經知道每一個窗口包括控件都有本身的窗口函數即Cwnd::WindowProc函數,那麼若是有上百個控件,豈不是系統中保存了上百個窗口類(對應了窗口函數),效率能高嗎?實際上是mfc更改了每一個窗口對應的窗口類,讓每一個窗口wnd的窗口類對應的窗口函數都是惟一的全局的AfxWndProc函數,因此操做系統經過DispatchMessage轉發消息時只須要識別這一個窗口函數就好了,之後轉到哪一個具體的窗口函數就是上面圖中所示的東西了。
其次,下面補充一個本身寫的一個簡單的仿MFC框架的完整的例子(vs2010下):
(用消息映射機制實現的一個簡單窗體CMyWnd,它繼承於CFrameWnd)
就只有一個cpp文件(爲了簡化過程,把類聲明和實現寫到一個文件中了),內容以下:
首先,新建一個空win32應用程序,而後添加一個cpp文件,在文件中書寫以下代碼:
#include <afxwin.h>
class CMyWnd:public CFrameWnd
{
private:
char* ShowText; //聲明一個字符串變量爲數據成員
public:
afx_msg void OnPaint(); //聲明WM_PAINT消息處理函數,注意afx_msg只是一個佔位符供類嚮導定位用
afx_msg void OnLButtonDown(); //聲明鼠標左鍵按下消息處理函數
DECLARE_MESSAGE_MAP(); //聲明消息映射。這個宏本質是給類添加了:聲明靜態的_messageEntries[]數組和messageMap指針,可能還有其餘的東西
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
private:
CButton btn; //一個按鈕做爲成員變量
};
//消息映射的實現
BEGIN_MESSAGE_MAP(CMyWnd,CFrameWnd) //初始化初始化聲明中定義的消息映射條目集合_messageEntries[]和messageMap
ON_WM_PAINT() ON_WM_LBUTTONDOWN()
END_MESSAGE_MAP()
/*對BEGIN_MESSAGE_MAP和END_MESSAGE_MAP更詳細的說明,大概內容以下:
#define BEGIN_MESSAGE_MAP(theClass, baseClass)
theClass::messageMap = //【填寫子類的大表或大節點messageMap,這個節點裏面就放了兩個指針,分別指向父類的大節點以及本類的消息映射條目集合的首地址_messageEntries[0]】
{ &baseClass::messageMap, &theClass::_messageEntries[0] };
theClass::_messageEntries[] = //【填寫子類的消息映射條目集合。每個條目主要包含兩個東西:消息標誌如WM_PAINT和消息處理函數的指針。
{ //【這裏是根據「ON_WM_PAINT()」之類的宏展開,一條一條的往數組中填充消息映射條目】}
}; //【END_MESSAGE_MAP()宏,先簡單的理解爲就是一個右大括號和一個分號,固然其實際上表明更多,要否則也不會帶有小括號了。
*/
//WM_PAINT消息處理函數的實現
void CMyWnd::OnPaint()
{
CPaintDC dc(this);
dc.TextOut(20,20,ShowText);
}
//WM_LBUTTONDOWN消息處理函數的實現
void CMyWnd::OnLButtonDown(UINT nFlags, CPoint point)
{
ShowText="有消息映射表的程序"; //當鼠標按下時顯示的字符串
InvalidateRect(NULL,TRUE); //通知更新或重繪窗口
CFrameWnd::OnLButtonDown(nFlags, point);
}
//OnCreate類的函數都是在窗體建立完了以後調用的,因此能夠在這個函數裏建立一些按鈕。
BOOL CMyWnd::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
{
//CButton btn; 注意,不能把btn定義成局部的,不然函數執行完後就把btn類銷燬了,而btn類會自動銷燬相關的資源,因此btn就顯示不出來了。
//對控件類的窗體來講,光構造對象還不行,還必需要初始化才能顯示,初始化用的都是create或createEx之類的函數。這裏btn已是類的成員變量了,因此構造就省略了。
btn.Create(L"my按鈕",WS_CHILD|BS_DEFPUSHBUTTON,CRect(50,120,50+100,120+100),this,123); //最後一個參數123是控件的ID號,隨便取一個值便可。 按鈕是正方形的。
btn.ShowWindow(SW_SHOWNORMAL);
return CFrameWnd::OnCreateClient(lpcs, pContext);
}
//程序員由CWinApp派生的應用程序類
class CMyApp:public CWinApp
{
public:
BOOL InitInstance();
};
BOOL CMyApp::InitInstance()
{
CMyWnd* pMainWnd=new CMyWnd;
pMainWnd->Create(0,L"窗口標題"); //這個函數中完成了註冊窗體類和建立一個具體的窗體,固然中間可能通過了多個mfc的函數,機制很複雜的
pMainWnd->ShowWindow(m_nCmdShow);
pMainWnd->UpdateWindow();
m_pMainWnd=pMainWnd;
return TRUE;
}
//定義APP全局對象
CMyApp MyApp;
以上就是整個cpp文件中的內容。在編譯以前,修改vs2010項目的配置屬性->常規->MFC的使用選項選擇使用共享或靜態的mfc庫,最後編譯運行便可,運行效果以下圖:
另外,上面的CMyWnd::OnCreateClient函數是虛函數並且是建立窗體過程當中系統本身發ON_CREATE消息而後調用的,這是由於窗體類是繼承與CFrameWnd類的,而CFrameWnd類都有默認的這個虛函數,可是若是是CView類,則默認沒有OnCreate函數,也就是對於CView來講,默認狀況下是不把(ON_CREATE,對應的函數地址)這個的消息映射條目加入到_messageEntries[]數組中的(而CFrameWnd類的OnCreate函數對應的消息映射條目默認是加入到那個數組的),因此須要手動添加消息處理函數,而不像CMyWnd::OnCreateClient那樣只須要改寫對應的虛函數OnCreateClient就好了。
【補充孫鑫教程中瞭解到的MFC標準的單文檔應用程序框架流程:首先是全局MyApp對象的構造函數調用(即全局對象的構造),而後操做系統啓動winmain函數,在這個winmain函數中調用afxMain函數,進入afxMain函數中:先是調用了MyApp對象的InitInstance函數,最後調用MyApp對象的Run函數啓動消息循環。
對於InitInstance函數內部調用了一些主要的函數:首先是經過shell命令即「ParseCommandLine(cmdInfo)這句」調用了AfxEndDeferRegisterClass()即afx框架的註冊窗口類函數,而後shell命令又調用了CWnd::PreCreateWindiw()函數,而這個PreCreateWindiw函數中又一次調用AfxEndDeferRegisterClass函數(之因此調用了兩次多是爲了處理涉及文檔框架等細節的東西),在這裏可能修改窗口類保存的窗口處理函數的地址。而後shell又調用了CFrame::LoadFrame函數,這個函數(具體做用不重要先無論)調用CFrame::Create函數建立一個具體的窗體(實際山create中調用了createEx來完成的),特別注意的是CFrame::Create中還調用了子類CMainFrame::PreCreateWindow函數目的是爲了修改窗體的樣式,注意雖然總共調用了兩個PreCreateWindow函數實際上調用的是不一樣類的函數,第一次是爲了實現afx框架的窗口類的創建,而第二次是爲了讓用戶在建立窗口以前有機會修改窗體的外觀。
至此窗口就建立完成了,窗口建立的全部工做都是由shell命令完成的,最後回到InitInstance函數調用窗口的ShowWindow和UpdateWindow來顯示窗口。】
第3、下面深刻講解細節原理:
1、消息映射不用深刻掌握狀況下的用法
先複習下msg結構體的定義,共六個成員:
typedef struct tagMSG {
HWND hwnd;
UINT message; //消息的類型(即事件的類型)
WPARAM wParam; //附加或稱數據參數1
LPARAM lParam;
DWORD time;
POINT pt;
} MSG;
再複習下窗口過程的定義(四個參數,分別是msg的前四個成員):
LRESULT CALLBACK MainWndProc (
HWND hwnd,// 窗口句柄
UINT msg,// 消息標識
WPARAM wParam,//消息參數1
LPARAM lParam//消息參數2
)
消息的分類(從發送途徑上看)和傳遞過程:
消息分兩種:隊列消息和非隊列消息。隊列消息送到系統消息隊列,而後到線程消息隊列;非隊列消息直接送給目的窗口過程。
這裏,對消息隊列闡述以下:
Windows維護一個系統消息隊列(System message queue),每一個GUI線程有一個線程消息隊列(Thread message queue)。
鼠標、鍵盤事件由鼠標或鍵盤驅動程序轉換成輸入消息並把消息放進系統消息隊列,例如WM_MOUSEMOVE、WM_LBUTTONUP、WM_KEYDOWN、WM_CHAR等等。Windows每次從系統消息隊列移走一個消息,肯定它是送給哪一個窗口的和這個窗口是由哪一個線程建立的,而後,把它放進窗口建立線程的線程消息隊列。線程消息隊列接收送給該線程所建立窗口的消息。線程從消息隊列取出消息,經過Windows把它送給適當的窗口過程來處理。
除了鍵盤、鼠標消息之外,隊列消息還有WM_PAINT、WM_TIMER和WM_QUIT。
這些隊列消息之外的絕大多數消息是非隊列消息。
從消息的來源來看,能夠分爲:系統定義的消息和應用程序定義的消息。
系統消息ID的範圍是從0到WM_USER-1,或0X80000到0XBFFFF;應用程序消息從WM_USER(0X0400)到0X7FFF,或0XC000到0XFFFF;WM_USER到0X7FFF範圍的消息由應用程序本身使用;0XC000到0XFFFF範圍的消息用來和其餘應用程序通訊,爲了ID的惟一性,使用::RegisterWindowMessage來獲得該範圍的消息ID。
MFC處理的三類消息:
Windows消息,前綴以「WM_」打頭,WM_COMMAND例外。Windows消息直接送給MFC窗口過程處理,窗口過程調用對應的消息處理函數。通常,由窗口對象來處理這類消息,也就是說,這類消息處理函數通常是MFC窗口類的成員函數。
控制通知消息,是控制子窗口送給父窗口的WM_COMMAND通知消息。窗口過程調用對應的消息處理函數。通常,由窗口對象來處理這類消息,也就是說,這類消息處理函數通常是MFC窗口類的成員函數。
命令消息,這是來自菜單、工具條按鈕、加速鍵等用戶接口對象的WM_COMMAND通知消息,屬於應用程序本身定義的消息。經過消息映射機制,MFC框架把命令按必定的路徑分發給多種類型的對象(具有消息處理能力)處理,如文檔、窗口、應用程序、文檔模板等對象。能處理消息映射的類必須從CCmdTarget類派生。
通常使用方法,即MFC消息映射的實現方法(即便用過程當中的作法,不須要掌握內部運行原理):
應用程序類的定義(頭文件)包含了相似以下的代碼:
//{{AFX_MSG(CTttApp) //這句比較特殊的註釋是給類嚮導定位用的,能夠刪除,不影響編譯和運行,但影響類嚮導的顯示
afx_msg void OnAppAbout(); //afx_msg只是個佔位符,編譯器會忽略,他是給類嚮導定位用的,可能還有其餘做用,因此必不可少
//}}AFX_MSG //這句比較特殊的註釋是給類嚮導定位用的
DECLARE_MESSAGE_MAP()
應用程序類的實現文件中包含了相似以下的代碼:
BEGIN_MESSAGE_MAP(CTApp, CWinApp)
//{{AFX_MSG_MAP(CTttApp)
ON_COMMAND(ID_APP_ABOUT, OnAppAbout) //宏就不須要定位用的afx_msg之類的東西了,指不定這些定位的東西都寫在宏裏了呢。
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
頭文件裏是消息映射和消息處理函數的聲明,實現文件裏是消息映射的實現和消息處理函數的實現。它表示讓應用程序對象處理命令消息ID_APP_ABOUT,消息處理函數是OnAppAbout。
void CMyApp::OnAppAbout()
{
...... //函數的具體實現代碼略
}
本文最後還有一個完整的例子。
2、消息映射機制的內部原理:
(1)消息映射聲明的解釋
#define DECLARE_MESSAGE_MAP() \ //一個右斜槓是鏈接兩行的意思,爲了便於把代碼書寫成多行
private: \
static const AFX_MSGMAP_ENTRY _messageEntries[]; \
protected: \
static AFX_DATA const AFX_MSGMAP messageMap; \
virtual const AFX_MSGMAP* GetMessageMap() const;
上面這句消息映射聲明的實質是給所在類添加幾個靜態成員變量和靜態或虛擬函數。
成員變量
有兩個成員變量被添加,第一個是_messageEntries,第二個是messageMap。
_messageEntries[]是一個AFX_MSGMAP_ENTRY 類型的數組變量,是一個靜態成員變量,用來容納類的消息映射條目。一個消息映射條目能夠用AFX_MSGMAP_ENTRY結構來描述。
AFX_MSGMAP_ENTRY結構的定義以下:
struct AFX_MSGMAP_ENTRY
{
UINT nMessage; //Windows消息ID
UINT nCode; //控制消息的通知碼
UINT nID; //Windows Control的ID
UINT nLastID; //若是是必定範圍的消息被映射,則nLastID指定其範圍
UINT nSig; //消息的動做標識
AFX_PMSG pfn; //響應消息時應執行的函數(routine to call (or special value))
};
從上述結構能夠看出,每條映射有兩部分的內容:第一部分是關於消息ID的,包括前四個域;第二部分是關於消息對應的執行函數,包括後兩個域。
在上述結構的六個域中,pfn是一個指向CCmdTarger成員函數的指針。函數指針的類型定義以下:
typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);當使用一條或者多條消息映射條目初始化消息映射數組時,各類不一樣類型的消息函數都被轉換成這樣的類型:不接收參數,也不返回參數的類型。由於全部能夠有消息映射的類都是從CCmdTarge派生的,因此能夠實現這樣的轉換。nSig是一個標識變量,用來標識不一樣原型的消息處理函數,每個不一樣原型的消息處理函數對應一個不一樣的nSig。在消息分發時,MFC內部根據nSig把消息派發給對應的成員函數處理,實際上,就是根據nSig的值把pfn還原成相應類型的消息處理函數並執行它。
第二個成員變量的聲明
AFX_MSGMAP messageMap;是一個AFX_MSGMAP類型的靜態成員變量,從其類型名稱和變量名稱能夠猜出,它是一個包含了消息映射信息的變量。。的確,它把消息映射的信息(消息映射數組)和相關函數打包在一塊兒,也就是說,獲得了一個消息處理類的該變量,就獲得了它所有的消息映射數據和功能【我稱之爲消息映射大表】。AFX_MSGMAP結構的定義以下:
struct AFX_MSGMAP
{
//pBaseMap保存基類消息映射入口_messageEntries的地址【實際上是保存基類大表,大表中含有消息映射條目數組的首地址】
const AFX_MSGMAP* pBaseMap;
//lpEntries保存消息映射入口_messageEntries的地址
const AFX_MSGMAP_ENTRY* lpEntries;
};
從上面的定義能夠看出,經過messageMap能夠獲得類的消息映射數組_messageEntries和基類消息映射數組的地址。
(2)消息映射實現的解釋
下面是BEGIN_MESSAGE_MAP宏:
#define BEGIN_MESSAGE_MAP(theClass, baseClass) \
const AFX_MSGMAP* theClass::GetMessageMap() const \
{ return &theClass::messageMap; } \
AFX_DATADEF const AFX_MSGMAP theClass::messageMap = \
{ &baseClass::messageMap, &theClass::_messageEntries[0] }; \
const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = \
{ \
下面是END_MESSAGE_MAP宏:
#define END_MESSAGE_MAP() \
{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \
};
消息映射實現的實質是初始化聲明中定義的靜態成員函數_messageEntries和messageMap。
這樣,在進入WinMain函數以前,每一個能夠響應消息的MFC類都生成了一個消息映射表,程序運行時經過查詢該表判斷是否須要響應某條消息。
<1>消息映射數組_messageEntries[]的初始化
消息映射數組的元素是消息映射條目,條目的格式符合結構AFX_MESSAGE_ENTRY的描述。因此,要初始化消息映射數組,就必須使用符合該格式的數據來填充.顯然,這是一個繁瑣的工做。爲了簡化操做,MFC根據消息的不一樣和消息處理方式的不一樣,把消息映射劃分紅若干類別,每一類的消息映射至少有一個共性:消息處理函數的原型相同。對每一類消息映射,MFC定義了一個宏來簡化初始化消息數組的工做。例如,前文提到的ON_COMMAND宏用來映射命令消息,只要指定命令ID和消息處理函數便可,由於對這類命令消息映射條目,其餘四個屬性都是固定的。
<2>messageMap的初始化
messageMap的類型是AFX_MESSMAP。通過初始化,域lpEntries保存了消息映射數組_messageEntries的地址;pBaseMap保存了基類的消息映射數組的地址。
(3)消息映射宏的解釋
下面是ON_COMMAND宏:
#define ON_COMMAND(id, memberFxn) \
{\
WM_COMMAND,\
CN_COMMAND,\
(WORD)id,\ //最終是控件的ID值
(WORD)id,\
AfxSig_vv,\ //是AfxSig_vv是MFC預約義的枚舉變量
(AFX_PMSG)memberFxn\
};
爲了簡化程序員的工做,MFC定義了一系列的消息映射宏和像AfxSig_vv這樣的枚舉變量,以及標準消息處理函數,而且具體地實現這些函數。這裏主要討論消息映射宏,經常使用的分爲
如下幾類。
<1>用於Windows消息的宏,前綴爲「ON_WM_」。
這樣的宏不帶參數,由於它對應的消息和消息處理函數的函數名稱、函數原型是肯定的。MFC提供了這類消息處理函數的定義和缺省實現。每一個這樣的宏處理不一樣的Windows消息。
例如:宏ON_WM_CREATE()把消息WM_CREATE映射到OnCreate函數,消息映射條目的第一個成員nMessage指定爲要處理的Windows消息的ID,第二個成員nCode指定爲0。
<2>用於命令消息的宏ON_COMMAND
這類宏帶有參數,須要經過參數指定命令ID和消息處理函數。這些消息都映射到WM_COMMAND上,也就是將消息映射條目的第一個成員nMessage指定爲WM_COMMAND,第二個成員nCode指定爲CN_COMMAND(即0)。消息處理函數的原型是void (void),不帶參數,不返回值。另外對於ON_COMMAND_RANGE,這裏暫不討論。
<3>用於控制通知消息的宏
這類宏可能帶有三個參數,如ON_CONTROL,就須要指定控制窗口ID,通知碼和消息處理函數;也可能帶有兩個參數,如具體處理特定通知消息的宏ON_BN_CLICKED、ON_LBN_DBLCLK、ON_CBN_EDITCHANGE等,須要指定控制窗口ID和消息處理函數。
控制通知消息也被映射到WM_COMMAND上,也就是將消息映射條目的第一個成員的nMessage指定爲WM_COMMAND,可是第二個成員nCode是特定的通知碼,第三個成員nID是控制子窗口的ID,第四個成員nLastID等於第三個成員的值。消息處理函數的原型是void (void),沒有參數,不返回值。另外ON_NOTIFY相似於ON_CONTROL,這裏暫不討論。
<4>用於用戶界面接口狀態更新的ON_UPDATE_COMMAND_UI宏
這類宏被映射到消息WM_COMMND上,帶有兩個參數,須要指定用戶接口對象ID和消息處理函數。消息映射條目的第一個成員nMessage被指定爲WM_COMMAND,第二個成員nCode被指定爲-1,第三個成員nID和第四個成員nLastID都指定爲用戶接口對象ID。消息處理函數的原型是 void (CCmdUI*),參數指向一個CCmdUI對象,不返回值。另外還有ON_UPDATE_COMMAND_UI_RANGE宏,這裏暫不討論。
<5>用於其餘消息的宏
例如用於用戶定義消息的ON_MESSAGE。這類宏帶有參數,須要指定消息ID和消息處理函數。消息映射條目的第一個成員nMessage被指定爲消息ID,第二個成員nCode被指定爲0,第三個成員nID和第四個成員也是0。消息處理的原型是LRESULT (WPARAM, LPARAM),參數1和參數2是消息參數wParam和lParam,返回LRESULT類型的值。
<6>擴展消息映射宏
不少普通消息映射宏都有對應的擴展消息映射宏,例如:ON_COMMAND對應的ON_COMMAND_EX,ON_ONTIFY對應的ON_ONTIFY_EX,等等。擴展宏除了具備普通宏的功能,還有特別的用途,這裏暫不討論。
(4)CcmdTarget類
全部響應消息或事件的類都從它派生。例如,CWinapp,CWnd,CDocument,CView,CDocTemplate,CFrameWnd,等等。CCmdTarget類是MFC處理命令消息的基礎、核心。MFC爲該類設計了許多成員函數和一些成員數據,基本上是爲了解決消息映射問題的。
CCmdTarget有兩個與消息映射有密切關係的成員函數:DispatchCmdMsg和OnCmdMsg。
<1>靜態成員函數DispatchCmdMsg
CCmdTarget的靜態成員函數DispatchCmdMsg,用來分發Windows消息。此函數是MFC內部使用的,其原型以下:
static BOOL DispatchCmdMsg(
CCmdTarget* pTarget,
UINT nID,
int nCode,
AFX_PMSG pfn,
void* pExtra,
UINT nSig,
AFX_CMDHANDLERINFO* pHandlerInfo)
<2>虛擬函數OnCmdMsg
CCmdTarget的虛擬函數OnCmdMsg,用來傳遞和發送消息、更新用戶界面對象的狀態,其原型以下:
OnCmdMsg(
UINT nID,
int nCode,
void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
框架的命令消息傳遞機制主要是經過該函數來實現的。
CCmdTarget對OnCmdMsg的默認實現簡單描述是:在當前命令目標(this所指,也即處理消息的對象)的類和基類的消息映射數組裏搜索指定命令消息的消息處理函數(標準Windows消息不會送到這裏處理)。
這裏使用虛擬函數GetMessageMap獲得命令目標類的消息映射入口數組_messageEntries,而後在數組裏匹配指定的消息映射條目。匹配標準:命令消息ID相同,控制通知代碼相同。由於GetMessageMap是虛擬函數,因此能夠確認當前命令目標的確切類。
若是找到了一個匹配的消息映射條目,則使用DispachCmdMsg調用這個處理函數;
若是沒有找到,則使用_GetBaseMessageMap獲得基類的消息映射數組,查找,直到找到或搜尋了全部的基類(到CCmdTarget)爲止;
若是最後沒有找到,則返回FASLE。
每一個從CCmdTarget派生的命令目標類均可以覆蓋OnCmdMsg,利用它來肯定是否能夠處理某條命令,若是不能,就經過調用下一命令目標的OnCmdMsg,把該命令送給下一個命令目標處理。一般,派生類覆蓋OnCmdMsg時,要調用基類的被覆蓋的OnCmdMsg。
在MFC框架中,一些MFC命令目標類覆蓋了OnCmdMsg,如框架窗口類覆蓋了該函數,實現了MFC的標準命令消息發送路徑。
(5)MFC窗口過程
全部的消息都送給窗口過程處理,MFC的全部窗口都使用同一窗口過程,消息或者直接由窗口過程調用相應的消息處理函數處理,或者按MFC命令消息派發路徑送給指定的命令目標處理。那麼,MFC的窗口過程是什麼?怎麼處理標準Windows消息?怎麼實現命令消息的派發?請看下面。
按理說,每個「窗口類」都有本身的窗口過程。正常狀況下使用該「窗口類」建立的窗口都使用它的窗口過程。
那麼,爲何說MFC建立的全部HWND窗口使用同一個窗口過程呢?
在MFC中,的確全部的窗口都使用同一個窗口過程:AfxWndProc或AfxWndProcBase(若是定義了_AFXDLL)。它們的原型以下:
LRESULT CALLBACK
AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam) //記住有一個HWND參數
LRESULT CALLBACK //不是dll時不用管這個函數
AfxWndProcBase(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
上面的窗口過程和win32的標準窗口過程是同樣的,只有四個參數,而且是msg結構的前四個成員,因此四個參數的值其實都是來自一個從消息隊列取出的msg結構變量,也即四個參數的值,其實都是操做系給的值到msg,而後傳到這四個參數。
實際上在窗體初始化過程當中經過複雜的過程(有空研究這個複雜的過程吧,好像涉及到Hook)調用了::SetWindowLong函數人爲的設置窗口過程爲AfxWndProc,並保存原窗口過程在窗口類成員變量m_pfnSuper中,這樣造成一個窗口過程鏈。須要的時候,原窗口過程地址能夠經過窗口類成員函數GetSuperWndProcAddr獲得。
這樣,AfxWndProc就成爲CWnd或其派生類的窗口過程。不論隊列消息,仍是非隊列消息,都送到AfxWndProc窗口過程來處理(若是使用MFC DLL,則AfxWndProcBase被調用,而後是AfxWndProc)。通過消息分發以後沒有被處理的消息,將送給原窗口過程(m_pfnSuper)處理。
(6)對Windows消息的接收和處理
Windows消息送給AfxWndProc窗口過程以後,AfxWndProc獲得HWND窗口對應的MFC窗口對象,而後,搜索該MFC窗口對象和其基類的消息映射數組,斷定它們是否處理當前消息,若是是則調用對應的消息處理函數,不然,進行缺省處理。
下面,以一個應用程序的視窗口建立時,對WM_CREATE消息的處理爲例,詳細地討論Windows消息的分發過程。
好比類CTview要處理WM_CREATE消息,使用ClassWizard加入消息處理函數CTview::OnCreate。下面,看這個函數怎麼被調用:
下面是對上面過程的分析:
<1>首先,分析AfxWndProc窗口過程函數。
AfxWndProc的原型以下:
LRESULT AfxWndProc(HWND hWnd,
UINT nMsg, WPARAM wParam, LPARAM lParam)
若是收到的消息nMsg不是WM_QUERYAFXWNDPROC(該消息被MFC內部用來確認窗口過程是否使用AfxWndProc),則從hWnd獲得對應的MFC Windows對象指針pWnd。pWnd所指的MFC窗口對象將負責完成消息的處理。這裏,pWnd所指示的對象是MFC視窗口對象,即CTview對象。
而後,把pWnd和AfxWndProc接受的四個參數傳遞給函數AfxCallWndProc執行。
<2>AfxCallWndProc原型以下:
LRESULT AFXAPI AfxCallWndProc(CWnd* pWnd, HWND hWnd,
UINT nMsg, WPARAM wParam = 0, LPARAM lParam = 0)
MFC使用AfxCallWndProc函數把消息送給CWnd類或其派生類的對象。該函數主要是把消息和消息參數(nMsg、wParam、lParam)傳遞給MFC窗口對象的成員函數WindowProc
(pWnd->WindowProc)做進一步處理。若是是WM_INITDIALOG消息,則在調用WindowProc先後要做一些處理。
<3>WindowProc的函數原型以下:
LRESULT CWnd::WindowProc(UINT message, //他是具體窗口的函數了,因此就不須要窗口句柄參數了
WPARAM wParam, LPARAM lParam)
這是一個虛擬函數,程序員能夠在CWnd的派生類中覆蓋它,改變MFC分發消息的方式。例如,MFC的CControlBar就覆蓋了WindowProc,對某些消息做了本身的特別處理,其餘消息處理由基類的WindowProc函數完成。
可是在當前例子中,當前對象的類CTview沒有覆蓋該函數,因此CWnd的WindowProc被調用。
這個函數把下一步的工做交給OnWndMsg函數來處理。若是OnWndMsg沒有處理,則交給DefWindowProc來處理。
OnWndMsg和DefWindowProc都是CWnd類的虛擬函數。
<4>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。
(7)Windows消息的查找和匹配
CWnd或者派生類的對象調用OnWndMsg搜索本對象或者基類的消息映射數組,尋找當前消息的消息處理函數。若是當前對象或者基類處理了當前消息,則一定在其中一個類的消息映射數組中匹配到當前消息的處理函數。具體過程複雜,暫不討論。
(8)Windows消息處理函數的調用
對一個Windows消息,匹配到了一個消息映射條目以後,將調用映射條目所指示的消息處理函數。
調用處理函數的過程就是轉換映射條目的pfn指針爲適當的函數類型並執行它:MFC定義了一個成員函數指針mmf,首先把消息處理函數的地址賦值給該函數指針,而後根據消息映射條目的nSig值轉換指針的類型。可是,要給函數指針mmf賦值,必須使該指針能夠指向全部的消息處理函數,爲此則該指針的類型是全部類型的消息處理函數指針的聯合體。
對上述過程,MFC的實現大略以下:
union MessageMapFunctions mmf;
mmf.pfn = lpEntry->pfn;
switch (value_of_nsig){
…
case AfxSig_is: //OnCreate就是該類型
lResult = (this->*mmf.pfn_is)((LPTSTR)lParam);
break;
…
default:
ASSERT(FALSE); break;
}
分析,對於上面的例子,nSig等於AfxSig_is,因此將執行語句
(this->*mmf.pfn_is)((LPTSTR)lParam)
也就是對CTview::OnCreate的調用。
(9)總結
綜上所述,標準Windwos消息和應用程序消息中的Registered消息,由窗口過程直接調用相應的處理函數處理:
若是某個類型的窗口(C++類)處理了某條消息(覆蓋了CWnd或直接基類的處理函數),則對應的HWND窗口(Winodws window)收到該消息時就調用該覆蓋函數來處理;若是該類窗口沒有處理該消息,則調用實現該處理函數最直接的基類(在C++的類層次上接近該類)來處理,上述例子中若是CTview不處理WM_CREATE消息,則調用上一層的CWnd::OnCreate處理;
若是基類都不處理該消息,則調用DefWndProc來處理。
綜合對Windows消息的處理來看,MFC使用消息映射機制完成了C++虛擬函數的功能。這主要基於如下幾點:
全部處理消息的類從CCmdTarget派生。
使用靜態成員變量_messageEntries數組存放消息映射條目,使用靜態成員變量messageMap來惟一地區別和獲得類的消息映射。【注意,屢次提到messageMap,其實messageMap能夠成爲消息映射大表,但它實際上是很是小的,就含有兩個成員,一個是pBaseMap用於指向父類的消息映射總表,一個是lpEntries用來指向_messageEntries[]數組。】
經過GetMessage虛擬函數來獲取當前對象的類的messageMap變量,進而獲得消息映射入口。
按照先底層,後基層的順序在類的消息映射數組中搜索消息處理函數。基於這樣的機制,通常在覆蓋基類的消息處理函數時,應該調用基類的同名函數。
以上論斷適合於MFC其餘消息處理機制,如對命令消息的處理等。不一樣的是其餘消息處理有一個命令派發/分發的過程。
下一節,討論命令消息的接受和處理。
(10)對命令消息的接收和處理