剖析MFC六大關鍵技術(五六)--消息映射與命令傳遞
睿豐德科技 專注RFID識別技術和條碼識別技術與管理軟件的集成項目。質量追溯系統、MES系統、金蝶與條碼系統對接、用友與條碼系統對接
說到消息,在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,你并不是在你的程序中寫上一句“#i nclude MFC.h”,然后就在你的程序中用MFC類庫。
MFC是一塊包著糖衣的牛骨頭。你很輕松地寫出一個單文檔窗口,在窗口中間打印一句“I love MFC!”,然后,惡夢開始了……想逃避,打算永遠不去理解MFC內幕?門都沒有!在MFC這個黑暗神秘的洞中,即使你打算摸著石頭前行,也注定找不到出口。對著MFC這塊牛骨頭,微軟溫和、民主地告訴你“你當然可以選擇不啃掉它,咳咳……但你必然會因此而餓死!”
消息映射與命令傳遞體現了MFC與SDK的不同。在SDK編程中,沒有消息映射的概念,它有明確的回調函數中,通過一個switch語句去判斷收到了何種消息,然后對這個消息進行處理。所以,在SDK編程中,會發送消息和在回調函數中處理消息就差不多可以寫SDK程序了。
在MFC中,看上去發送消息和處理消息比SDK更簡單、直接,但可惜不直觀。舉個簡單的例子,如果我們想自定義一個消息,SDK是非常簡單直觀的,用一條語句:SendMessage(hwnd,message,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類型的數組,用來容納消息映射項。定義如下:
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;
if((lpEntry=AfxFindMessageEntry(pMessageMap->lpEntries,message,0,0)!=NULL)
{
lpEntry->pfn();
}
}
以上,大家看到了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; /
可以看出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函數。RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成
初次用MFC編程,我們只會用MFC ClassWizard為我們做大量的東西,最主要的是添加消息響應。記憶中,如果是自已添加消息響應,我們應何等的小心翼翼,對BEGIN_MESSAGE_MAP()……END_MESSAGE_MAP()更要奉若神靈。它就是一個魔盒子,把我們的咒語放入恰當的地方,就會發生神奇的力量,放錯了,自己的程序就連“命”都沒有。
據說,知道得太多未必是好事。我也曾經打算不去理解這神秘的區域,覺得編程的時候知道自己想做什么就行了。MFC外表上給我們提供了東西,直觀地說,不但給了我個一個程序的外殼,更給我們許多方便。微軟的出發點可能是希望達到“傻瓜編程”的結果,試想,誰不會用ClassWizard?大家知道,Windows是基于消息的,有了ClassWizard,你又會添加類,又會添加消息,那么你所學的東西似乎學到頭了。于是許多程序員認為“我們沒有必要走SDK的老路,直接用MFC編程,新的東西通常是簡單、直觀、易學……”
到你真正想用MFC編程的時候,你會發覺光會ClassWizard的你是多么的愚蠢。MFC不是一個普通的類庫,普通的類庫我們完全可以不理解里面的細節,只要知道這些類庫能干什么,接口參數如何就萬事大吉。如string類,操作順序是定義一個string對象,然后修改屬性,調用方法。
但對于MFC,你并不是在你的程序中寫上一句“#i nclude MFC.h”,然后就在你的程序中用MFC類庫。
MFC是一塊包著糖衣的牛骨頭。你很輕松地寫出一個單文檔窗口,在窗口中間打印一句“I love MFC!”,然后,惡夢開始了……想逃避,打算永遠不去理解MFC內幕?門都沒有!在MFC這個黑暗神秘的洞中,即使你打算摸著石頭前行,也注定找不到出口。對著MFC這塊牛骨頭,微軟溫和、民主地告訴你“你當然可以選擇不啃掉它,咳咳……但你必然會因此而餓死!”
消息映射與命令傳遞體現了MFC與SDK的不同。在SDK編程中,沒有消息映射的概念,它有明確的回調函數中,通過一個switch語句去判斷收到了何種消息,然后對這個消息進行處理。所以,在SDK編程中,會發送消息和在回調函數中處理消息就差不多可以寫SDK程序了。
在MFC中,看上去發送消息和處理消息比SDK更簡單、直接,但可惜不直觀。舉個簡單的例子,如果我們想自定義一個消息,SDK是非常簡單直觀的,用一條語句:SendMessage(hwnd,message,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類型的數組,用來容納消息映射項。定義如下:
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;
if((lpEntry=AfxFindMessageEntry(pMessageMap->lpEntries,message,0,0)!=NULL)
{
lpEntry->pfn();
}
}
以上,大家看到了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; /
可以看出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函數。RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成