作為C++程式設計師,我們總是希望自己程式的所有程式碼都是自己寫出來的,如果使用了其他的一些庫,也總是千方百計想弄清楚其中的類和函式的原理,否則就會感覺不踏實。所以,我們對於在進行MFC視窗程式設計時經常要用到的訊息機制也不滿足於會使用,而是希望能理解箇中道理。本文就為大家剖析MFC訊息對映和命令傳遞的原理。
理解MFC訊息機制的必要性
說到訊息,在MFC中,“最熟悉的神祕”可以說是訊息對映了,那是我們剛開始接觸MFC時就要面對的東西。有過SDK程式設計經驗的朋友轉到MFC程式設計的時候,一下子覺得什麼都變了樣。特別是視窗訊息及對訊息的處理跟以前相比,更是風馬牛不相及的。如文件不是視窗,是怎樣響應命令訊息的呢?
初次用MFC程式設計,我們只會用MFC ClassWizard為我們做大量的東西,最主要的是新增訊息響應。記憶中,如果是自已新增訊息響應,我們應何等的小心翼翼,對BEGIN_MESSAGE_MAP()……END_MESSAGE_MAP()更要奉若神靈。它就是一個魔盒子,把我們的咒語放入恰當的地方,就會發生神奇的力量,放錯了,自己的程式就連“命”都沒有。
據說,知道得太多未必是好事。我也曾經打算不去理解這神祕的區域,覺得程式設計的時候知道自己想做什麼就行了。MFC外表上給我們提供了東西,直觀地說,不但給了我個一個程式的外殼,更給我們許多方便。微軟的出發點可能是希望達到“傻瓜程式設計”的結果,試想,誰不會用ClassWizard?大家知道,Windows是基於訊息的,有了ClassWizard,你又會新增類,又會新增訊息,那麼你所學的東西似乎學到頭了。於是許多程式設計師認為“我們沒有必要走SDK的老路,直接用MFC程式設計,新的東西通常是簡單、直觀、易學……”。
到你真正想用MFC程式設計的時候,你會發覺光會ClassWizard的你是多麼的愚蠢。MFC不是一個普通的類庫,普通的類庫我們完全可以不理解裡面的細節,只要知道這些類庫能幹什麼,介面引數如何就萬事大吉。如string類,操作順序是定義一個string物件,然後修改屬性,呼叫方法。但對於MFC,並不是在你的程式中寫上一句“#include MFC.h”,然後就使用MFC類庫的。
MFC是一塊包著糖衣的牛骨頭。你很輕鬆地寫出一個單文件視窗,在視窗中間列印一句“I love MFC!”,然後,惡夢開始了……想逃避,打算永遠不去理解MFC內幕?門都沒有!在MFC這個黑暗神祕的洞中,即使你打算摸著石頭前行,也註定找不到出口。對著MFC這塊牛骨頭,微軟溫和、民主地告訴你“你當然可以選擇不啃掉它,咳咳……但你必然會因此而餓死!”
MFC訊息機制與SDK的不同
訊息對映與命令傳遞體現了MFC與SDK的不同。在SDK程式設計中,沒有訊息對映的概念,它有明確的回撥函式,通過一個switch語句去判斷收到了何種訊息,然後對這個訊息進行處理。所以,在SDK程式設計中,會傳送訊息和在回撥函式中處理訊息就差不多可以寫SDK程式了。
在MFC中,看上去傳送訊息和處理訊息比SDK更簡單、直接,但可惜不直觀。舉個簡單的例子,如果我們想自定義一個訊息,SDK是非常簡單直觀的,用一條語句:SendMessage(hwnd,message/*一個大於或等於WM_USER的數字*/,wparam,lparam),之後就可以在回撥函式中處理了。但MFC就不同了,因為你通常不直接去改寫視窗的回撥函式,所以只能亦步亦趨對照原來的MFC程式碼,把訊息放到恰當的地方。這確實是一樣很痛苦的勞動。
要了解MFC訊息對映原理並不是一件輕鬆的事情。我們可以逆向思維,想象一下訊息對映為我們做了什麼工作。MFC在自動化給我們提供了很大的方便,比如,所有的MFC視窗都使用同一視窗過程,即所有的MFC視窗都有一個預設的視窗過程。不像在SDK程式設計中,要為每個視窗類寫一個視窗過程。
MFC訊息對映原理
對於訊息對映,最直截了當地猜想是:訊息對映就是用一個資料結構把“訊息”與“響應訊息函式名”串聯起來。這樣,當視窗感知訊息發生時,就對結構查詢,找到相應的訊息響應函式執行。其實這個想法也不能簡單地實現:我們每個不同的MFC視窗類,對同一種訊息,有不同的響應方式。即是說,對同一種訊息,不同的MFC視窗會有不同的訊息響應函式。
這時,大家又想了一個可行的方法。我們設計視窗基類(CWnd)時,我們讓它對每種不同的訊息都來一個訊息響應,並把這個訊息響應函式定義為虛擬函式。這樣,從CWnd派生的視窗類對所有訊息都有了一個空響應,我們要響應一個特定的訊息就過載這個訊息響應函式就可以了。但這樣做的結果,一個幾乎什麼也不做的CWnd類要有幾百個“多餘”的函式,哪怕這些訊息響應函式都為純虛擬函式,每個CWnd物件也要揹負著一個巨大的虛擬表,這也是得不償失的。
許多朋友在學習訊息對映時苦無突破,其原因是一開始就認為MFC的訊息對映的目的是為了替代SDK視窗過程的編寫——這本來沒有理解錯。但他們還有多一層的理解,認為既然是替代“舊”的東西,那麼MFC訊息映身應該是更高層次的抽象、更簡單、更容易認識。但結果是,如果我們不通過ClassWizard工具,手動新增訊息是相當迷茫的一件事。
所以,我們在學習MFC訊息對映時,首先要弄清楚:訊息對映的目的,不是為是更加快捷地向視窗過程新增程式碼,而是一種機制的改變。如果不想改變視窗過程函式,那麼應該在哪裡進行訊息響應呢?許多朋友一知半解地認為:我們可以用HOOK技術,搶在訊息佇列前把訊息抓取,把訊息響應提到視窗過程的外面。再者,不同的視窗,會有不同的感興趣的訊息,所以每個MFC視窗都應該有一個表把感興趣的訊息和相應訊息響應函式連繫起來。然後得出——訊息對映機制執行步驟是:當訊息發生,我們用HOOK技術把本來要傳送到視窗過程的訊息抓獲,然後對照一下MFC視窗的訊息對映表,如果是表裡面有的訊息,就執行其對應的函式。
當然,用HOOK技術,我們理論上可以在不改變視窗過程函式的情況下,可以完成訊息響應。MFC確實是這樣做的,但實際操作起來可能跟你的想象差別很大。
現在我們來編寫訊息對映表,我們先定義一個結構,這個結構至少有兩個項:一是訊息ID,二是響應該訊息的函式。如下:
- struct AFX_MSGMAP_ENTRY
- {
- UINT nMessage; //感興趣的訊息
- AFX_PMSG pfn; //響應以上訊息的函式指標
- }
當然,只有兩個成員的結構連線起來的訊息對映表是不成熟的。Windows訊息分為標準訊息、控制元件訊息和命令訊息,每型別的訊息都是包含數百不同ID、不同意義、不同引數的訊息。我們要準確地判別發生了何種訊息,必須再增加幾個成員。還有,對於AFX_PMSG pfn,實際上等於作以下宣告:
void (CCmdTarget::*pfn)(); // 提示:AFX_PMSG為型別標識,具體宣告是:typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);
pfn是一個不帶引數和返回值的CCmdTarget型別函式指標,只能指向CCmdTarget類中不帶引數和返回值的成員函式,這樣pfn更為通用,但我們響應訊息的函式許多需要傳入引數的。為了解決這個矛盾,我們還要增加一個表示引數型別的成員。當然,還有其它……
最後,MFC我們訊息對映表成員結構如下定義:
- struct AFX_MSGMAP_ENTRY
- {
- UINT nMessage; //Windows 訊息ID
- UINT nCode; // 控制訊息的通知碼
- UINT nID; //命令訊息ID範圍的起始值
- UINT nLastID; //命令訊息ID範圍的終點
- UINT nSig; // 訊息的動作標識
- AFX_PMSG pfn;
- };
有了以上訊息對映表成員結構,我們就可以定義一個AFX_MSGMAP_ENTRY型別的陣列,用來容納訊息對映項。定義如下:
AFX_MSGMAP_ENTRY _messageEntries[];
但這樣還不夠,每個AFX_MSGMAP_ENTRY陣列,只能儲存著當前類感興趣的訊息,而這僅僅是我們想處理的訊息中的一部分。對於一個MFC程式,一般有多個視窗類,裡面都應該有一個AFX_MSGMAP_ENTRY陣列。
我們知道,MFC還有一個訊息傳遞機制,可以把自己不處理的訊息傳送給別的類進行處理。為了能查詢各下MFC物件的訊息對映表,我們還要增加一個結構,把所有的AFX_MSGMAP_ENTRY陣列串聯起來。於是,我們定義了一個新結構體:
- struct AFX_MSGMAP
- {
- const AFX_MSGMAP* pBaseMap; //指向別的類的AFX_MSGMAP物件
- const AFX_MSGMAP_ENTRY* lpEntries; //指向自身的訊息表
- };
之後,在每個打算響應訊息的類中宣告這樣一個變數:AFX_MSGMAP messageMap,讓其中的pBaseMap指向基類或另一個類的messageMap,那麼將得到一個AFX_MSGMAP元素的單向連結串列。這樣,所有的訊息對映資訊形成了一張訊息網。
當然,僅有訊息對映表還不夠,它只能把各個MFC物件的訊息、引數與相應的訊息響應函式連成一張網。為了方便查詢,MFC在上面的類中插入了兩個函式(其中theClass代表當前類):
一個是_GetBaseMessageMap(),用來得到基類訊息對映的函式。函式原型如下:
- const AFX_MSGMAP* PASCAL theClass::_GetBaseMessageMap() /
- { return &baseClass::messageMap; } /
另一個是GetMessageMap() ,用來得到自身訊息對映的函式。函式原型如下:
- const AFX_MSGMAP* theClass::GetMessageMap() const /
- { return &theClass::messageMap; } /
有了訊息對映表之後,我們得討論到問題的關鍵,那就是訊息發生以後,其對應的響應函式如何被呼叫。大家知道,所有的MFC視窗,都有一個同樣的視窗過程——AfxWndProc(…)。在這裡順便要提一下的是,看過MFC原始碼的朋友都得,從AfxWndProc函式進去,會遇到一大堆曲折與迷團,因為對於這個龐大的訊息對映機制,MFC要做的事情很多,如優化訊息,增強相容性等,這一大量的工作,有些甚至用匯編語言來完成,對此,我們很難深究它。所以我們要省略大量程式碼,理性地分析它。
對已定型的AfxWndProc來說,對所有訊息,最多隻能提供一種預設的處理方式。這當然不是我們想要的。我們想通過AfxWndProc最終執行訊息對映網中對應的函式。那麼,這個執行路線是怎麼樣的呢?
從AfxWndProc下去,最終會呼叫到一個函式OnWndMsg。請看程式碼:
- LRESULT CALLBACK AfxWndProc(HWND hWnd,UINT nMsg,WPARAM wParam, LPARAM lParam)
- {
- ……
- CWnd* pWnd = CWnd::FromHandlePermanent(hWnd); //把對控制程式碼的操作轉換成對CWnd物件。
- Return AfxCallWndProc(pWnd,hWnd,nMsg,wParam,lParam);
- }
把對控制程式碼的操作轉換成對CWnd物件是很重要的一件事,因為AfxWndProc只是一個全域性函式,當然不知怎麼樣去處理各種windows視窗訊息,所以它聰明地把處理權交給windows視窗所關聯的MFC視窗物件。
現在,大家幾乎可以想象得到AfxCallWndProc要做的事情,不錯,它當中有一句:
pWnd->WindowProc(nMsg,wParam,lParam);
到此,MFC視窗過程函式變成了自己的一個成員函式。WindowProc是一個虛擬函式,我們甚至可以通過改寫這個函式去響應不同的訊息,當然,這是題外話。
WindowProc會呼叫到CWnd物件的另一個成員函式OnWndMsg,下面看看大概的函式原型是怎麼樣的:
- BOOL CWnd::OnWndMsg(UINT message,WPARAM wParam,LPARAM lParam,LRESULT* pResult)
- {
- if(message==WM_COMMAND)
- {
- OnCommand(wParam,lParam);
- ……
- }
- if(message==WM_NOTIFY)
- {
- OnCommand(wParam,lParam,&lResult);
- ……
- }
- const AFX_MSGMAP* pMessageMap; pMessageMap=GetMessageMap();
- const AFX_MSGMAP_ENTRY* lpEntry;
- /*以下程式碼作用為:用AfxFindMessageEntry函式從訊息入口pMessageMap處查詢指定訊息,如果找到,返回指定訊息對映表成員的指標給lpEntry。然後執行該結構成員的pfn所指向的函式*/
- if((lpEntry=AfxFindMessageEntry(pMessageMap->lpEntries,message,0,0)!=NULL)
- {
- lpEntry->pfn();/*注意:真正MFC程式碼中沒有用這一條語句。上面提到,不同的訊息引數代表不同的意義和不同的訊息響應函式有不同型別的返回值。而pfn是一個不帶引數的函式指標,所以真正的MFC程式碼中,要根據物件lpEntry的訊息的動作標識nSig給訊息處理函式傳遞引數型別。這個過程包含很複雜的巨集代換,大家在此知道:找到匹配訊息,執行相應函式就行!*/
- }
- }
MFC命令傳遞
在上面的程式碼中,大家看到了OnWndMsg能根據傳進來的訊息引數,查詢到匹配的訊息和執行相應的訊息響應。但這還不夠,我們平常響應選單命令訊息的時候,原本屬於框架視窗(CFrameWnd)的WM_COMMAND訊息,卻可以放到視物件或文件物件中去響應。其原理如下:
我們看上面函式OnWndMsg原型中看到以下程式碼:
if(message==WM_COMMAND)
{
OnCommand(wParam,lParam);
……
}
即對於命令訊息,實際上是交給OnCommand函式處理。而OnCommand是一個虛擬函式,即WM_COMMAND訊息發生時,最終是發生該訊息所對應的MFC物件去執行OnCommand。比如點框架視窗選單,即向CFrameWnd傳送一個WM_COMMAND,將會導致CFrameWnd::OnCommand(wParam,lParam)的執行。且看該函式原型:
- BOOL CFrameWnd::OnCommand(WPARAM wParam,LPARAM lParam)
- {
- ……
- return CWnd:: OnCommand(wParam,lParam);
- }
可以看出,它最後把該訊息交給CWnd:: OnCommand處理。再看:
- BOOL CWnd::OnCommand(WPARAM wParam,LPARAM lParam)
- {
- ……
- return OnCmdMsg(nID,nCode,NULL,NULL);
- }
這裡包含了一個C++多型性很經典的問題。在這裡,雖然是執行CWnd類的函式,但由於這個函式在CFrameWnd:: OnCmdMsg裡執行,即當前指標是CFrameWnd類指標,再有OnCmdMsg是一個虛擬函式,所以如果CFrameWnd改寫了OnCommand,程式會執行CFrameWnd::OnCmdMsg(…)。
對CFrameWnd::OnCmdMsg(…)函式的原理扼要分析如下:
- BOOL CFrameWnd:: OnCmdMsg(…)
- {
- CView pView = GetActiveView();//得到活動視指標。
- if(pView-> OnCmdMsg(…))
- return TRUE; //如果CView類物件或其派生類物件已經處理該訊息,則返回。
- ……//否則,同理向下執行,交給文件、框架、及應用程式執行自身的OnCmdMsg。
- }
到此,CFrameWnd:: OnCmdMsg完成了把WM_COMMAND訊息傳遞到視物件、文件物件及應用程式物件實現訊息響應。
寫了這麼多,我們已經清楚了MFC訊息對映與命令傳遞的大致過程。
MFC訊息對映巨集
現在,我們來看MFC“神祕程式碼”,會發覺好看多了。
先看DECLARE_MESSAGE_MAP()巨集,它在MFC中定義如下:
- #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; /
可以看出DECLARE_MESSAGE_MAP()定義了我們熟悉的兩個結構和一個函式,顯而易見,這個巨集為每個需要實現訊息對映的類提供了相關變數和函式。
現在集中精力來看一下BEGIN_MESSAGE_MAP,END_MESSAGE_MAP和ON_COMMAND三個巨集,它們在MFC中定義如下(其中ON_COMMAND與另外兩個巨集並沒有定義在同一個檔案中,把它放到一起是為了好看):
- #define BEGIN_MESSAGE_MAP(theClass, baseClass) /
- const AFX_MSGMAP* theClass::GetMessageMap() const /
- { return &theClass::messageMap; } /
- AFX_COMDAT AFX_DATADEF const AFX_MSGMAP theClass::messageMap = /
- { &baseClass::messageMap, &theClass::_messageEntries[0] }; /
- AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = /
- { /
- #define ON_COMMAND(id, memberFxn) /
- { WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSig_vv, (AFX_PMSG)&memberFxn },
- #define END_MESSAGE_MAP() /
- {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } /
- }; /
一下子看三個巨集覺得有點複雜,但這僅僅是複雜,公式性的文字代換並不是很難。且看下面例子,假設我們框架中有一選單項為“Test”,即定義瞭如下巨集:
- BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
- ON_COMMAND(ID_TEST, OnTest)
- END_MESSAGE_MAP()
那麼巨集展開之後得到如下程式碼:
- const AFX_MSGMAP* CMainFrame::GetMessageMap() const
- { return &CMainFrame::messageMap; }
- ///以下填入訊息表對映資訊
- const AFX_MSGMAP CMainFrame::messageMap =
- { &CFrameWnd::messageMap, &CMainFrame::_messageEntries[0] };
- //下面填入儲存著當前類感興趣的訊息,可填入多個AFX_MSGMAP_ENTRY物件
- const AFX_MSGMAP_ENTRY CMainFrame::_messageEntries[] =
- {
- { WM_COMMAND, CN_COMMAND, (WORD)ID_TEST, (WORD)ID_TEST, AfxSig_vv, (AFX_PMSG)&OnTest }, // 加入的ID_TEST訊息引數
- {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } //本類的訊息對映的結束項
- };
大家知道,要完成ID_TEST訊息對映,還要定義和實現OnTest函式。即在標頭檔案中寫afx_msg void OnTest()並在原始檔中實現它。根據以上所學的東西,我們知道了當ID為ID_TEST的命令訊息發生,最終會執行到我們寫的OnTest函式。
至此,MFC六大關鍵技術寫完了。其中寫得最難的是訊息對映與命令傳遞,除了技術複雜之外,最難的是有許多避不開的程式碼。為了大家看得輕鬆一點,我把那繁雜的巨集放在文章最後,希望能給你閱讀帶來方便。
來自:http://blog.csdn.net/liyi268/article/details/623391