《轉》MFC六大關鍵技術之(五)(六)——消息映射與命令傳遞

題外話:剛開始學視窗程序設計的時候,我就打印了一本Windows消息詳解,裏面列舉了各類已定義消息的意義和做用,共10多頁,在編程的時候翻翻,有時以爲很受用。我發覺不少編程的朋友,雖然天天都面對消息,卻不多關注它。C++程序員有一個通病,很想寫「本身」的程序,即每一行代碼都想本身寫出來。若是用了一些庫,總但願能徹底理解庫裏的類或函數是怎麼一回事,不然就「不踏實」。對於消息,許多朋友只關心經常使用的幾個,對其他的不聞不問。其實,Windows中有不少不經常使用的消息卻頗有用,程序員可能經過響應這些消息實現更簡捷的編程。程序員

說到消息,在MFC中,「最熟悉的神祕」可算是消息映射,那是咱們剛開始接觸MFC時就要面對的東西。有過SDK編程經驗的朋友轉到MFC編程的時候,一會兒以爲什麼都變了樣。特別是窗口消息及對消息的處理跟之前相比,更是風馬牛不相及的。如文檔不是窗口,是怎樣響應命令消息的呢?編程

初次用MFC編程,咱們只會用MFC ClassWizard爲咱們作大量的東西,最主要的是添加消息響應。記憶中,若是是自已添加消息響應,咱們應何等的當心翼翼,對BEGIN_MESSAGE_MAP()……END_MESSAGE_MAP()更要奉若神靈。它就是一個魔盒子,把咱們的咒語放入恰當的地方,就會發生神奇的力量,放錯了,本身的程序就連「命」都沒有。windows

聽說,知道得太多未必是好事。我也曾經打算不去理解這神祕的區域,以爲編程的時候知道本身想作什麼就好了。MFC外表上給咱們提供了東西,直觀地說,不但給了我個一個程序的外殼,更給咱們許多方便。微軟的出發點多是但願達到「傻瓜編程」的結果,試想,誰不會用ClassWizard?你們知道,Windows是基於消息的,有了ClassWizard,你又會添加類,又會添加消息,那麼你所學的東西彷佛學到頭了。因而許多程序員認爲「咱們沒有必要走SDK的老路,直接用MFC編程,新的東西一般是簡單、直觀、易學……」數組

到你真正想用MFC編程的時候,你會發覺光會ClassWizard的你是多麼的愚蠢。MFC不是一個普通的類庫,普通的類庫咱們徹底能夠不理解裏面的細節,只要知道這些類庫能幹什麼,接口參數如何就萬事大吉。如string類,操做順序是定義一個string對象,而後修改屬性,調用方法。數據結構

但對於MFC,你並非在你的程序中寫上一句「#include MFC.h」,而後就在你的程序中用MFC類庫。框架

MFC是一塊包着糖衣的牛骨頭。你很輕鬆地寫出一個單文檔窗口,在窗口中間打印一句「I love MFC!」,而後,惡夢開始了……想逃避,打算永遠不去理解MFC內幕?門都沒有!在MFC這個黑暗神祕的洞中,即便你打算摸着石頭前行,也註定找不到出口。對着MFC這塊牛骨頭,微軟溫和、民主地告訴你「你固然能夠選擇不啃掉它,咳咳……但你必然會所以而餓死!」函數

消息映射與命令傳遞體現了MFCSDK的不一樣。在SDK編程中,沒有消息映射的概念,它有明確的回調函數中,經過一個switch語句去判斷收到了何種消息,而後對這個消息進行處理。因此,在SDK編程中,會發送消息和在回調函數中處理消息就差很少能夠寫SDK程序了。工具

MFC中,看上去發送消息和處理消息比SDK更簡單、直接,但惋惜不直觀。舉個簡單的例子,若是咱們想自定義一個消息,SDK是很是簡單直觀的,用一條語句:SendMessage(hwnd,message/*一個大於或等於WM_USER的數字*/,wparam,lparam),以後就能夠在回調函數中處理了。但MFC就不一樣了,由於你一般不直接去改寫窗口的回調函數,因此只能亦步亦趨對照原來的MFC代碼,把消息放到恰當的地方。這確實是同樣很痛苦的勞動。學習

要了解MFC消息映射原理並非一件輕鬆的事情。咱們能夠逆向思惟,想象一下消息映射爲咱們作了什麼工做。MFC在自動化給咱們提供了很大的方便,好比,全部的MFC窗口都使用同一窗口過程,即全部的MFC窗口都有一個默認的窗口過程。不象在SDK編程中,要爲每一個窗口類寫一個窗口過程。優化

對於消息映射,最直截了當地猜測是:消息映射就是用一個數據結構把「消息」與「響應消息函數名」串聯起來。這樣,當窗口感知消息發生時,就對結構查找,找到相應的消息響應函數執行。其實這個想法也不能簡單地實現:咱們每一個不一樣的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 _messageEntries[];

 

爲了能查找各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元素的單向鏈表。這樣,全部的消息映射信息造成了一張消息網。

有了以上消息映射表成員結構,咱們就能夠定義一個AFX_MSGMAP_ENTRY類型的數組,用來容納消息映射項。定義以下:但這樣還不夠,每一個AFX_MSGMAP_ENTRY數組,只能保存着當前類感興趣的消息,而這僅僅是咱們想處理的消息中的一部分。對於一個MFC程序,通常有多個窗口類,裏面都應該有一個AFX_MSGMAP_ENTRY數組。咱們知道,MFC還有一個消息傳遞機制,能夠把本身不處理的消息傳送給別的類進行處理。

固然,僅有消息映射表還不夠,它只能把各個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給消息處理函數傳遞參數類型。這個過程包含很複雜的宏代換,你們在此知道:找到匹配消息,執行相應函數就行!*/

}

}

                    

以上,你們看到了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「神祕代碼」,會發覺好看多了。

先看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; /

 

 

如今集中精力來看一下BEGIN_MESSAGE_MAPEND_MESSAGE_MAPON_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()並在源文件中實現它。根據以上所學的東西,咱們知道了當IDID_TEST的命令消息發生,最終會執行到咱們寫的OnTest函數。

 

至此,MFC六大關鍵技術寫完了。其中寫得最難的是消息映射與命令傳遞,除了技術複雜以外,最難的是有許多避不開的代碼。爲了你們看得輕鬆一點,我把那繁雜的宏放在文章最後,但願能給你閱讀帶來方便。

其實,較早前這就描述過MFC六大關鍵技術,也在一些論壇上發表過,但後三篇不知如何遺失了。許多朋友向我索稿,但苦於沒有備份,也不想枯燥地重寫,故推延至今,請你們見諒。

還有,許多朋友說(二)(三)寫得十分不明白,這也是我意料到的地方。我寫文章,不多回頭看一下,有時本身也不知道本身寫了什麼。有許多朋友把修改後的版本給我,雖然修改了一點點,甚至僅僅修改了一下錯別字,但讀起來容易理解不少。對此,我心裏十分感激。若是有更多的時間,我會修改一下前面的文章,放到個人網站上去! 

能夠看出DECLARE_MESSAGE_MAP()定義了咱們熟悉的兩個結構和一個函數,顯而易見,這個宏爲每一個須要實現消息映射的類提供了相關變量和函數。

相關文章
相關標籤/搜索