來自:http://blog.csdn.net/liyi268/article/details/623391
作為C++程序員,我們總是希望自己程序的所有代碼都是自己寫出來的,如果使用了其他的一些庫洞渤,也總是千方百計想弄清楚其中的類和函數(shù)的原理藐握,否則就會感覺不踏實。所以,我們對于在進行MFC視窗程序設計時經(jīng)常要用到的消息機制也不滿足于會使用驯用,而是希望能理解個中道理液兽。本文就為大家剖析MFC消息映射和命令傳遞的原理骂删。
理解MFC消息機制的必要性
說到消息,在MFC中四啰,“最熟悉的神秘”可以說是消息映射了宁玫,那是我們剛開始接觸MFC時就要面對的東西。有過SDK編程經(jīng)驗的朋友轉到MFC編程的時候柑晒,一下子覺得什么都變了樣欧瘪。特別是窗口消息及對消息的處理跟以前相比,更是風馬牛不相及的匙赞。如文檔不是窗口垂寥,是怎樣響應命令消息的呢严望?
初次用MFC編程,我們只會用MFC ClassWizard為我們做大量的東西,最主要的是添加消息響應疚宇。記憶中搞疗,如果是自已添加消息響應箍镜,我們應何等的小心翼翼贤姆,對BEGIN_MESSAGE_MAP()……END_MESSAGE_MAP()更要奉若神靈。它就是一個魔盒子猛拴,把我們的咒語放入恰當?shù)牡胤礁Γ蜁l(fā)生神奇的力量,放錯了愉昆,自己的程序就連“命”都沒有职员。
據(jù)說,知道得太多未必是好事跛溉。我也曾經(jīng)打算不去理解這神秘的區(qū)域焊切,覺得編程的時候知道自己想做什么就行了扮授。MFC外表上給我們提供了東西,直觀地說专肪,不但給了我個一個程序的外殼刹勃,更給我們許多方便。微軟的出發(fā)點可能是希望達到“傻瓜編程”的結果嚎尤,試想荔仁,誰不會用ClassWizard?大家知道芽死,Windows是基于消息的乏梁,有了ClassWizard,你又會添加類关贵,又會添加消息遇骑,那么你所學的東西似乎學到頭了。于是許多程序員認為“我們沒有必要走SDK的老路揖曾,直接用MFC編程落萎,新的東西通常是簡單、直觀翩肌、易學……”模暗。
到你真正想用MFC編程的時候,你會發(fā)覺光會ClassWizard的你是多么的愚蠢念祭。MFC不是一個普通的類庫,普通的類庫我們完全可以不理解里面的細節(jié)碍侦,只要知道這些類庫能干什么粱坤,接口參數(shù)如何就萬事大吉。如string類瓷产,操作順序是定義一個string對象站玄,然后修改屬性,調用方法濒旦。但對于MFC株旷,并不是在你的程序中寫上一句“#include <MFC.h>”,然后就使用MFC類庫的尔邓。
MFC是一塊包著糖衣的牛骨頭晾剖。你很輕松地寫出一個單文檔窗口,在窗口中間打印一句“I love MFC!”梯嗽,然后齿尽,惡夢開始了……想逃避,打算永遠不去理解MFC內幕灯节?門都沒有循头!在MFC這個黑暗神秘的洞中绵估,即使你打算摸著石頭前行,也注定找不到出口卡骂。對著MFC這塊牛骨頭国裳,微軟溫和、民主地告訴你“你當然可以選擇不啃掉它全跨,咳咳……但你必然會因此而餓死躏救!”
MFC消息機制與SDK的不同
消息映射與命令傳遞體現(xiàn)了MFC與SDK的不同。在SDK編程中螟蒸,沒有消息映射的概念盒使,它有明確的回調函數(shù),通過一個switch語句去判斷收到了何種消息七嫌,然后對這個消息進行處理少办。所以,在SDK編程中诵原,會發(fā)送消息和在回調函數(shù)中處理消息就差不多可以寫SDK程序了英妓。
在MFC中,看上去發(fā)送消息和處理消息比SDK更簡單绍赛、直接蔓纠,但可惜不直觀。舉個簡單的例子吗蚌,如果我們想自定義一個消息腿倚,SDK是非常簡單直觀的,用一條語句:SendMessage(hwnd,message/一個大于或等于WM_USER的數(shù)字/,wparam,lparam)蚯妇,之后就可以在回調函數(shù)中處理了敷燎。但MFC就不同了,因為你通常不直接去改寫窗口的回調函數(shù)箩言,所以只能亦步亦趨對照原來的MFC代碼硬贯,把消息放到恰當?shù)牡胤健_@確實是一樣很痛苦的勞動陨收。
要了解MFC消息映射原理并不是一件輕松的事情饭豹。我們可以逆向思維,想象一下消息映射為我們做了什么工作务漩。MFC在自動化給我們提供了很大的方便拄衰,比如,所有的MFC窗口都使用同一窗口過程菲饼,即所有的MFC窗口都有一個默認的窗口過程肾砂。不像在SDK編程中,要為每個窗口類寫一個窗口過程宏悦。
消息隊列
對于每個用MFC開發(fā)的GUI程序镐确,他們都有一個CMy***App,該類繼承自CWinApp包吝,而CWinApp繼承自CWinThread, CWinApp是一個GUI線程,系統(tǒng)會為其維護一個THREADINFO結構源葫,
消息隊列包含在一個叫THREADINFO的結構中诗越,有四個隊列:
`Sent Message Queue 發(fā)送消息隊列 該隊列保存其他程序通過SendMessage給該線程發(fā)送的消息`
`Posted Message Queue 登記消息隊列 該隊列保存其他隊列通過PostMessage給該線程發(fā)送的消息 `
`Visualized Input Queue 輸入消息隊列 保存系統(tǒng)隊列分發(fā)過來的消息,比如鼠標或者鍵盤的消息`
`Reply Message Queue 應答消息隊列 保存向窗體發(fā)送消息后的結果息堂,
比如sendMessage操作結束后嚷狞,接收消息方會發(fā)送一個Reply消息
給發(fā)送方的Reply隊列中,以喚醒發(fā)送隊列荣堰。`
MFC消息映射原理
對于消息映射床未,最直截了當?shù)夭孪胧牵合⒂成渚褪怯靡粋€數(shù)據(jù)結構把“消息”與“響應消息函數(shù)名”串聯(lián)起來。這樣振坚,當窗口感知消息發(fā)生時薇搁,就對結構查找,找到相應的消息響應函數(shù)執(zhí)行渡八。其實這個想法也不能簡單地實現(xiàn):我們每個不同的MFC窗口類啃洋,對同一種消息,有不同的響應方式屎鳍。即是說宏娄,對同一種消息,不同的MFC窗口會有不同的消息響應函數(shù)逮壁。
這時孵坚,大家又想了一個可行的方法。我們設計窗口基類(CWnd)時貌踏,我們讓它對每種不同的消息都來一個消息響應十饥,并把這個消息響應函數(shù)定義為虛函數(shù)。這樣祖乳,從CWnd派生的窗口類對所有消息都有了一個空響應,我們要響應一個特定的消息就重載這個消息響應函數(shù)就可以了秉氧。但這樣做的結果眷昆,一個幾乎什么也不做的CWnd類要有幾百個“多余”的函數(shù),哪怕這些消息響應函數(shù)都為純虛函數(shù)汁咏,每個CWnd對象也要背負著一個巨大的虛擬表亚斋,這也是得不償失的。
許多朋友在學習消息映射時苦無突破攘滩,其原因是一開始就認為MFC的消息映射的目的是為了替代SDK窗口過程的編寫——這本來沒有理解錯帅刊。但他們還有多一層的理解,認為既然是替代“舊”的東西漂问,那么MFC消息映身應該是更高層次的抽象赖瞒、更簡單女揭、更容易認識。但結果是栏饮,如果我們不通過ClassWizard工具吧兔,手動添加消息是相當迷茫的一件事。
所以袍嬉,我們在學習MFC消息映射時境蔼,首先要弄清楚:消息映射的目的,不是為是更加快捷地向窗口過程添加代碼伺通,而是一種機制的改變箍土。如果不想改變窗口過程函數(shù),那么應該在哪里進行消息響應呢罐监?許多朋友一知半解地認為:我們可以用HOOK技術吴藻,搶在消息隊列前把消息抓取,把消息響應提到窗口過程的外面笑诅。再者调缨,不同的窗口,會有不同的感興趣的消息吆你,所以每個MFC窗口都應該有一個表把感興趣的消息和相應消息響應函數(shù)連系起來弦叶。然后得出——消息映射機制執(zhí)行步驟是:當消息發(fā)生,我們用HOOK技術把本來要發(fā)送到窗口過程的消息抓獲妇多,然后對照一下MFC窗口的消息映射表伤哺,如果是表里面有的消息,就執(zhí)行其對應的函數(shù)者祖。
當然立莉,用HOOK技術,我們理論上可以在不改變窗口過程函數(shù)的情況下七问,可以完成消息響應蜓耻。MFC確實是這樣做的,但實際操作起來可能跟你的想象差別很大械巡。
現(xiàn)在我們來編寫消息映射表刹淌,我們先定義一個結構,這個結構至少有兩個項:一是消息ID讥耗,二是響應該消息的函數(shù)有勾。如下:
struct AFX_MSGMAP_ENTRY
{
UINT nMessage; //感興趣的消息
AFX_PMSG pfn; //響應以上消息的函數(shù)指針
}
當然,只有兩個成員的結構連接起來的消息映射表是不成熟的古程。Windows消息分為標準消息蔼卡、控件消息和命令消息,每類型的消息都是包含數(shù)百不同ID挣磨、不同意義雇逞、不同參數(shù)的消息荤懂。我們要準確地判別發(fā)生了何種消息,必須再增加幾個成員喝峦。還有势誊,對于AFX_PMSG pfn,實際上等于作以下聲明:
void (CCmdTarget::*pfn)(); // 提示:AFX_PMSG為類型標識谣蠢,
具體聲明是:
typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);
pfn是一個不帶參數(shù)和返回值的CCmdTarget類型成員函數(shù)指針粟耻,只能指向CCmdTarget類中不帶參數(shù)和返回值的成員函數(shù),這樣pfn更為通用眉踱,但我們響應消息的函數(shù)許多需要傳入?yún)?shù)的挤忙。為了解決這個矛盾,我們還要增加一個表示參數(shù)類型的成員谈喳。當然册烈,還有其它……
最后,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類型的數(shù)組赏僧,用來容納消息映射項。定義如下:
AFX_MSGMAP_ENTRY _messageEntries[];
但這樣還不夠扭倾,每個AFX_MSGMAP_ENTRY數(shù)組淀零,只能保存著當前類感興趣的消息,而這僅僅是我們想處理的消息中的一部分膛壹。對于一個MFC程序驾中,一般有多個窗口類,里面都應該有一個AFX_MSGMAP_ENTRY數(shù)組模聋。
我們知道肩民,MFC還有一個消息傳遞機制,可以把自己不處理的消息傳送給別的類進行處理链方。為了能查找各下MFC對象的消息映射表持痰,我們還要增加一個結構,把所有的AFX_MSGMAP_ENTRY數(shù)組串聯(lián)起來祟蚀。于是共啃,我們定義了一個新結構體:
struct AFX_MSGMAP
{
const AFX_MSGMAP* pBaseMap; //指向別的類的AFX_MSGMAP對象
const AFX_MSGMAP_ENTRY* lpEntries; //指向自身的消息表
};
之后,在每個打算響應消息的類中聲明這樣一個變量:AFX_MSGMAP messageMap暂题,讓其中的pBaseMap指向基類或另一個類的messageMap,那么將得到一個AFX_MSGMAP元素的單向鏈表究珊。這樣薪者,所有的消息映射信息形成了一張消息網(wǎng)。
當然剿涮,僅有消息映射表還不夠言津,它只能把各個MFC對象的消息攻人、參數(shù)與相應的消息響應函數(shù)連成一張網(wǎng)。為了方便查找悬槽,MFC在上面的類中插入了兩個函數(shù)(其中theClass代表當前類):
一個是_GetBaseMessageMap()怀吻,用來得到基類消息映射的函數(shù)。函數(shù)原型如下:
const AFX_MSGMAP* PASCAL theClass::_GetBaseMessageMap() /
{ return &baseClass::messageMap; } /
另一個是GetMessageMap() 初婆,用來得到自身消息映射的函數(shù)蓬坡。函數(shù)原型如下:
const AFX_MSGMAP* theClass::GetMessageMap() const /
{ return &theClass::messageMap; } /
有了消息映射表之后,我們得討論到問題的關鍵磅叛,那就是消息發(fā)生以后屑咳,其對應的響應函數(shù)如何被調用。大家知道弊琴,所有的MFC窗口兆龙,都有一個同樣的窗口過程——AfxWndProc(…)。在這里順便要提一下的是敲董,看過MFC源代碼的朋友都得紫皇,從AfxWndProc函數(shù)進去,會遇到一大堆曲折與迷團腋寨,因為對于這個龐大的消息映射機制聪铺,MFC要做的事情很多,如優(yōu)化消息精置,增強兼容性等计寇,這一大量的工作,有些甚至用匯編語言來完成脂倦,對此番宁,我們很難深究它。所以我們要省略大量代碼赖阻,理性地分析它蝶押。
對已定型的AfxWndProc來說,對所有消息火欧,最多只能提供一種默認的處理方式棋电。這當然不是我們想要的。我們想通過AfxWndProc最終執(zhí)行消息映射網(wǎng)中對應的函數(shù)苇侵。那么赶盔,這個執(zhí)行路線是怎么樣的呢?
從AfxWndProc下去榆浓,最終會調用到一個函數(shù)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只是一個全局函數(shù)烘浦,當然不知怎么樣去處理各種windows窗口消息抖坪,所以它聰明地把處理權交給windows窗口所關聯(lián)的MFC窗口對象。
現(xiàn)在闷叉,大家?guī)缀蹩梢韵胂蟮玫紸fxCallWndProc要做的事情擦俐,不錯,它當中有一句:
pWnd->WindowProc(nMsg,wParam,lParam);
到此握侧,MFC窗口過程函數(shù)變成了自己的一個成員函數(shù)蚯瞧。WindowProc是一個虛函數(shù),我們甚至可以通過改寫這個函數(shù)去響應不同的消息藕咏,當然状知,這是題外話。
WindowProc會調用到CWnd對象的另一個成員函數(shù)OnWndMsg孽查,下面看看大概的函數(shù)原型是怎么樣的:
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函數(shù)從消息入口pMessageMap處查找指定消息饥悴,
如果找到,返回指定消息映射表成員的指針給lpEntry盲再。然后執(zhí)行該結構成員的pfn所指向的函數(shù)
*************************************************************/
if((lpEntry=AfxFindMessageEntry(pMessageMap->lpEntries,message,0,0)!=NULL)
{
lpEntry->pfn();
/*************************************************************
注意:真正MFC代碼中沒有用這一條語句西设。
上面提到,不同的消息參數(shù)代表不同的意義和不同的消息響應函數(shù)有不同類型的返回值答朋。
而pfn是一個不帶參數(shù)的函數(shù)指針贷揽,所以真正的MFC代碼中,
要根據(jù)對象lpEntry的消息的動作標識nSig給消息處理函數(shù)傳遞參數(shù)類型梦碗。
這個過程包含很復雜的宏代換禽绪,大家在此知道:找到匹配消息,執(zhí)行相應函數(shù)就行洪规!
*************************************************************/
}
}
MFC命令傳遞
在上面的代碼中印屁,大家看到了OnWndMsg能根據(jù)傳進來的消息參數(shù),查找到匹配的消息和執(zhí)行相應的消息響應斩例。但這還不夠雄人,我們平常響應菜單命令消息的時候,原本屬于框架窗口(CFrameWnd)的WM_COMMAND消息念赶,卻可以放到視對象或文檔對象中去響應础钠。其原理如下:
我們看上面函數(shù)OnWndMsg原型中看到以下代碼:
if(message==WM_COMMAND)
{
OnCommand(wParam,lParam);
……
}
即對于命令消息,實際上是交給OnCommand函數(shù)處理叉谜。而OnCommand是一個虛函數(shù)旗吁,即WM_COMMAND消息發(fā)生時,最終是發(fā)生該消息所對應的MFC對象去執(zhí)行OnCommand停局。比如點框架窗口菜單阵漏,即向CFrameWnd發(fā)送一個WM_COMMAND驻民,將會導致CFrameWnd::OnCommand(wParam,lParam)的執(zhí)行。且看該函數(shù)原型:
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++多態(tài)性很經(jīng)典的問題裆泳。在這里叹洲,雖然是執(zhí)行CWnd類的函數(shù),但由于這個函數(shù)在CFrameWnd:: OnCmdMsg里執(zhí)行工禾,即當前指針是CFrameWnd類指針运提,再有OnCmdMsg是一個虛函數(shù),所以如果CFrameWnd改寫了OnCommand闻葵,程序會執(zhí)行CFrameWnd::OnCmdMsg(…)民泵。
對CFrameWnd::OnCmdMsg(…)函數(shù)的原理扼要分析如下:
BOOL CFrameWnd:: OnCmdMsg(…)
{
CView pView = GetActiveView();//得到活動視指針。
if(pView-> OnCmdMsg(…))
return TRUE; //如果CView類對象或其派生類對象已經(jīng)處理該消息槽畔,則返回栈妆。
…… //否則,同理向下執(zhí)行厢钧,交給文檔鳞尔、框架、及應用程序執(zhí)行自身的OnCmdMsg早直。
}
到此寥假,CFrameWnd:: OnCmdMsg完成了把WM_COMMAND消息傳遞到視對象、文檔對象及應用程序對象實現(xiàn)消息響應霞扬。
寫了這么多糕韧,我們已經(jīng)清楚了MFC消息映射與命令傳遞的大致過程。
一個按鈕點擊事件的過程如下:
CWinThread::PumpMessage -> CWnd::PretranslateMessage -> /
CWnd::WalkPreTranslateMessate -> CD1Dlg::PreTranslateMessage -> /
CDialog::PreTranslateMessage -> CWnd::PreTranslateInput -> /
CWnd::IsDialogMessageA -> USER32內核 - > AfxWndProcBase -> /
AfxWndProc -> AfxCallWndProc -> CWnd::WindowProc -> /
CWnd::OnWndMsg -> CWnd::OnCommand -> CDialog::OnCmdMsg -> /
CCmdTarget::OnCmdMsg -> _AfxDispatchCmdMsg -> CD1Dlg::OnButton1()
MFC消息映射宏
現(xiàn)在喻圃,我們來看MFC“神秘代碼”萤彩,會發(fā)覺好看多了。
先看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()定義了我們熟悉的兩個結構和一個函數(shù)乒疏,顯而易見,這個宏為每個需要實現(xiàn)消息映射的類提供了相關變量和函數(shù)饮焦。
現(xiàn)在集中精力來看一下:
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[] =
{
//加入的ID_TEST消息參數(shù)
{WM_COMMAND, CN_COMMAND, (WORD)ID_TEST, (WORD)ID_TEST, AfxSig_vv, (AFX_PMSG)&OnTest },
//本類的消息映射的結束項
{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 }
};
大家知道煞肾,要完成ID_TEST消息映射咧织,還要定義和實現(xiàn)OnTest函數(shù)。即在頭文件中寫afx_msg void OnTest()并在源文件中實現(xiàn)它籍救。根據(jù)以上所學的東西习绢,我們知道了當ID為ID_TEST的命令消息發(fā)生,最終會執(zhí)行到我們寫的OnTest函數(shù)蝙昙。