但是在當前例子中,當前對象的類CTview沒有覆蓋該函數,所以CWnd的WindowProc被調用。
這個函數把下一步的工作交給OnWndMsg函數來處理。如果OnWndMsg沒有處理,則交給DefWindowProc來處理。
OnWndMsg和DefWindowProc都是CWnd類的虛擬函數。
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。
關於Windows消息和消息處理函數的匹配,見下一節。
缺省處理函數DefWindowProc將在討論對話框等的實現時具體分析。
Windows消息的查找和匹配
CWnd或者派生類的對象調用OnWndMsg搜索本對象或者基類的消息映射數組,尋找當前消息的消息處理函數。如果當前對象或者基類處理了當前消息,則必定在其中一個類的消息映射數組中匹配到當前消息的處理函數。
消息匹配是一個比較耗時的任務,為了提高效率,MFC設計了一個消息緩沖池,把要處理的消息和匹配到的消息映射條目(條目包含了消息處理函數的地址)以及進行消息處理的當前類等信息構成一條緩沖信息,放到緩沖池中。如果以後又有同樣的消息需要同一個類處理,則直接從緩沖池查找到對應的消息映射條目就可以了。
MFC用哈希查找來查詢消息映射緩沖池。消息緩沖池相當於一個哈希表,它是應用程序的一個全局變量,可以放512條最新用到的消息映射條目的緩沖信息,每一條緩沖信息是哈希表的一個入口。
采用AFX_MSG_CACHE結構描述每條緩沖信息,其定義如下:
struct AFX_MSG_CACHE
{
UINT nMsg;
const AFX_MSGMAP_ENTRY* lpEntry;
const AFX_MSGMAP* pMessageMap;
};
nMsg存放消息ID,每個哈希表入口有不同的nMsg。
lpEnty存放和消息ID匹配的消息映射條目的地址,它可能是this所指對象的類的映射條目,也可能是這個類的某個基類的映射條目,也可能是空。
pMessageMap存放消息處理函數匹配成功時進行消息處理的當前類(this所指對象的類)的靜態成員變量messageMap的地址,它唯一的標識了一個類(每個類的messageMap變量都不一樣)。
this所指對象是一個CWnd或其派生類的實例,是正在處理消息的MFC窗口對象。
哈希查找:使用消息ID的值作為關鍵值進行哈希查找,如果成功,即可從lpEntry獲得消息映射條目的地址,從而得到消息處理函數及其原型。
如何判斷是否成功匹配呢?有兩條標准:
第一,當前要處理的消息message在哈希表(緩沖池)中有入口;第二,當前窗口對象(this所指對象)的類的靜態變量messageMap的地址應該等於本條緩沖信息的pMessagMap。MFC通過虛擬函數GetMessagMap得到messageMap的地址。
如果在消息緩沖池中沒有找到匹配,則搜索當前對象的消息映射數組,看是否有合適的消息處理函數。
如果匹配到一個消息處理函數,則把匹配結果加入到消息緩沖池中,即填寫該條消息對應的哈希表入口:
nMsg=message;
pMessageMap=this->GetMessageMap;
lpEntry=查找結果
然後,調用匹配到的消息處理函數。否則(沒有找到),使用_GetBaseMessageMap得到基類的消息映射數組,查找和匹配;直到匹配成功或搜尋了所有的基類(到CCmdTarget)為止。
如果最後沒有找到,則也把該條消息的匹配結果加入到緩沖池中。和匹配成功不同的是:指定lpEntry為空。這樣OnWndMsg返回,把控制權返還給AfxCallWndProc函數,AfxCallWndProc將繼續調用DefWndProc進行缺省處理。
消息映射數組的搜索在CCmdTarget::OnCmdMsg函數中也用到了,而且算法相同。為了提高速度,MFC把和消息映射數組條目逐一比較、匹配的函數AfxFindMessageEntry用匯編書寫。
const AFX_MSGMAP_ENTRY* AFXAPI
AfxFindMessageEntry(const AFX_MSGMAP_ENTRY* lpEntry,
UINT nMsg, UINT nCode, UINT nID)
第一個參數是要搜索的映射數組的入口;第二個參數是Windows消息標識;第三個參數是控制通知消息標識;第四個參數是命令消息標識。
對Windows消息來說,nMsg是每條消息不同的,nID和nCode為0。
對命令消息來說,nMsg固定為WM_COMMAND,nID是每條消息不同,nCode都是CN_COMMAND(定義為0)。
對控制通知消息來說,nMsg固定為WM_COMMAND或者WM_NOTIFY,nID和nCode是每條消息不同。
對於Register消息,nMsg指定為0XC000,nID和nCode為0。在使用函數AfxFindMessageEntry得到匹配結果之後,還必須判斷nSig是否等於message,只有相等才調用對應的消息處理函數。
Windows消息處理函數的調用
對一個Windows消息,匹配到了一個消息映射條目之後,將調用映射條目所指示的消息處理函數。
調用處理函數的過程就是轉換映射條目的pfn指針為適當的函數類型並執行它:MFC定義了一個成員函數指針mmf,首先把消息處理函數的地址賦值給該函數指針,然後根據消息映射條目的nSig值轉換指針的類型。但是,要給函數指針mmf賦值,必須使該指針可以指向所有的消息處理函數,為此則該指針的類型是所有類型的消息處理函數指針的聯合體。
對上述過程,MFC的實現大略如下:
union MessageMapFunctions mmf;
mmf.pfn = lpEntry->pfn;
swithc (value_of_nsig){
…
case AfxSig_is: //OnCreate就是該類型
lResult = (this->*mmf.pfn_is)((LPTSTR)lParam);
break;
…
default:
ASSERT(FALSE); break;
}
…
LDispatchRegistered: // 處理registered windows messages
ASSERT(message >= 0xC000);
mmf.pfn = lpEntry->pfn;
lResult = (this->*mmf.pfn_lwl)(wParam, lParam);
…
如果消息處理函數有返回值,則返回該結果,否則,返回TRUE。
對於圖4-1所示的例子,nSig等於AfxSig_is,所以將執行語句
(this->*mmf.pfn_is)((LPTSTR)lParam)
也就是對CTview::OnCreate的調用。
順便指出,對於Registered窗口消息,消息處理函數都是同一原型,所以都被轉換成lwl型(關於Registered窗口消息的映射,見4.4.2節)。
綜上所述,標准Windwos消息和應用程序消息中的Registered消息,由窗口過程直接調用相應的處理函數處理:
如果某個類型的窗口(C++類)處理了某條消息(覆蓋了CWnd或直接基類的處理函數),則對應的HWND窗口(Winodws window)收到該消息時就調用該覆蓋函數來處理;如果該類窗口沒有處理該消息,則調用實現該處理函數最直接的基類(在C++的類層次上接近該類)來處理,上述例子中如果CTview不處理WM_CREATE消息,則調用上一層的CWnd::OnCreate處理;
如果基類都不處理該消息,則調用DefWndProc來處理。
消息映射機制完成虛擬函數功能的原理
綜合對Windows消息的處理來看,MFC使用消息映射機制完成了C++虛擬函數的功能。這主要基於以下幾點:
所有處理消息的類從CCmdTarget派生。
使用靜態成員變量_messageEntries數組存放消息映射條目,使用靜態成員變量messageMap來唯一地區別和得到類的消息映射。
通過GetMessage虛擬函數來獲取當前對象的類的messageMap變量,進而得到消息映射入口。
按照先底層,後基層的順序在類的消息映射數組中搜索消息處理函數。基於這樣的機制,一般在覆蓋基類的消息處理函數時,應該調用基類的同名函數。
以上論斷適合於MFC其他消息處理機制,如對命令消息的處理等。不同的是其他消息處理有一個命令派發/分發的過程。
下一節,討論命令消息的接受和處理。
對命令消息的接收和處理
MFC標准命令消息的發送
在SDI或者MDI應用程序中,命令消息由用戶界面對象(如菜單、工具條等)產生,然後送給主邊框窗口。主邊框窗口使用標准MFC窗口過程處理命令消息。窗口過程把命令傳遞給MFC主邊框窗口對象,開始命令消息的分發。MFC邊框窗口類CFrameWnd提供了消息分發的能力。
下面,還是通過一個例子來說明命令消息的處理過程。
使用AppWizard產生一個單文檔應用程序t。從help菜單選擇“About”,就會彈出一個ABOUT對話框。下面,討論從命令消息的發出到對話框彈出的過程。
首先,選擇“ About”菜單項的動作導致一個Windows命令消息ID_APP_ABOUT的產生。Windows系統發送該命令消息到邊框窗口,導致它的窗口過程AfxWndProc被調用,參數1是邊框窗口的句柄,參數2是消息ID(即WM_COMMAND),參數3、4是消息參數,參數3的值是ID_APP_ABOUT。接著的系列調用如圖4-3所示。
下面分別講述每一層所調用的函數。
前4步同對Windows消息的處理。這裡接受消息的HWND窗口是主邊框窗口,因此,AfxWndProc根據HWND句柄得到的MFC窗口對象是MFC邊框窗口對象。
在4.2.2節談到,如果CWnd::OnWndMsg判斷要處理的消息是命令消息(WM_COMMAND),就調用OnCommand進一步處理。由於OnCommand是虛擬函數,當前MFC窗口對象是邊框窗口對象,它的類從CFrameWnd類導出,沒有覆蓋CWnd的虛擬函數OnCommand,而CFrameWnd覆蓋了CWnd的OnCommand,所以,CFrameWnd的OnCommand被調用。換句話說,CFrameWnd的OnCommand被調用是動態約束的結果。接著介紹的本例子的有關調用,也是通過動態約束而實際發生的函數調用。
接著的有關調用,將不進行為什麼調用某個類的虛擬或者消息處理函數的分析。
(1)CFrameWnd的OnCommand函數
BOOL CFrameWnd::OnCommand(WPARAM wParam, LPARAM lParam)
參數wParam的低階word存放了菜單命令nID或控制子窗口ID;如果消息來自控制窗口,高階word存放了控制通知消息;如果消息來自加速鍵,高階word值為1;如果消息來自菜單,高階word值為0。
如果是通知消息,參數lParam存放了控制窗口的句柄hWndCtrl,其他情況下lParam是0。
在這個例子裡,低階word是ID_APP_ABOUT,高階word是1;lParam是0。
MFC對CFrameWnd的缺省實現主要是獲得一個機會來檢查程序是否運行在HELP狀態,需要執行上下文幫助,如果不需要,則調用基類的CWnd::OnCommand實現正常的命令消息發送。
(2)CWnd的OnCommand函數
BOOL CWnd::OnCommand(WPARAM wParam, LPARAM lParam)
它按一定的順序處理命令或者通知消息,如果發送成功,返回TRUE,否則,FALSE。處理順序如下:
如果是命令消息,則調用OnCmdMsg(nID, CN_UPDATE_COMMAND_UI, &state, NULL)測試nID命令是否已經被禁止,如果這樣,返回FALSE;否則,調用OnCmdMsg進行命令發送。關於CN_UPDATE_COMMAND_UI通知消息,見後面用戶界面狀態的更新處理。
如果是控制通知消息,則先用ReflectLastMsg反射通知消息到子窗口。如果子窗口處理了該消息,則返回TRUE;否則,調用OnCmdMsg進行命令發送。關於通知消息的反射見後面4.4.4.3節。OnCommand給OnCmdMsg傳遞四個參數:nID,即命令消息ID;nCode,如果是通知消息則為通知代碼,如果是命令消息則為NC_COMMAND(即0);其余兩個參數為空。
(3)CFrameWnd的OnCmdMsg函數
BOOL CFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
參數1是命令ID;如果是通知消息(WM_COMMAND或者WM_NOTIFY),則參數2表示通知代碼,如果是命令消息,參數2是0;如果是WM_NOTIFY,參數3包含了一些額外的信息;參數4在正常消息處理中應該是空。
在這個例子裡,參數1是命令ID,參數2為0,參數3空。
OnCmdMsg是虛擬函數,CFrameWnd覆蓋了該函數,當前對象(this所指)是MFC單文檔的邊框窗口對象。故CFrameWnd的OnCmdMsg被調用。CFrameWnd::OnCmdMsg在MFC消息發送中占有非常重要的地位,MFC對該函數的缺省實現確定了MFC的標准命令發送路徑:
送給活動(Active)視處理,調用活動視的OnCmdMsg。由於當前對象是MFC視對象,所以,OnCmdMsg將搜索CTview及其基類的消息映射數組,試圖得到相應的處理函數。
如果視對象自己不處理,則視得到和它關聯的文檔,調用關聯文檔的OnCmdMsg。由於當前對象是MFC視對象,所以,OnCmdMsg將搜索CTdoc及其基類的消息映射數組,試圖得到相應的處理函數。
如果文檔對象不處理,則它得到管理文檔的文檔模板對象,調用文檔模板的OnCmdMsg。由於當前對象是MFC文檔模板對象,所以,OnCmdMsg將搜索文檔模板類及其基類的消息映射數組,試圖得到相應的處理函數。
如果文檔模板不處理,則把沒有處理的信息逐級返回:文檔模板告訴文檔對象,文檔對象告訴視對象,視對象告訴邊框窗口對象。最後,邊框窗口得知,視、文檔、文檔模板都沒有處理消息。
CFrameWnd的OnCmdMsg繼續調用CWnd::OnCmdMsg(斜體表示有類屬限制)來處理消息。由於CWnd沒有覆蓋OnCmdMsg,故實際上調用了函數CCmdTarget::OnCmdMsg。由於當前對象是MFC邊框窗口對象,所以OnCmdMsg函數將搜索CMainFrame類及其所有基類的消息映射數組,試圖得到相應的處理函數。CWnd沒有實現OnCmdMsg卻指定要執行其OnCmdMsg函數,可能是為了以後MFC給CWnd實現了OnCmdMsg之後其他代碼不用改變。
這一步是邊框窗口自己嘗試處理消息。
如果邊框窗口對象不處理,則送給應用程序對象處理。調用CTApp的OnCmdMsg,由於實際上CTApp及其基類CWinApp沒有覆蓋OnCmdMsg,故實際上調用了函數CCmdTarget::OnCmdMsg。由於當前對象是MFC應用程序對象,所以OnCmdMsg函數將搜索CTApp類及其所有基類的的消息映射入口數組,試圖得到相應的處理函數
如果應用程序對象不處理,則返回FALSE,表明沒有命令目標處理當前的命令消息。這樣,函數逐級別返回,OnCmdMsg告訴OnCommand消息沒有被處理,OnCommand告訴OnWndMsg消息沒有被處理,OnWndMsg告訴WindowProc消息沒有被處理,於是WindowProc調用DefWindowProc進行缺省處理。
本例子在第六步中,應用程序對ID_APP_ABOUT消息作了處理。它找到處理函數CTApp::OnAbout,使用DispatchCmdMsg派發消息給該函數處理。
如果是MDI邊框窗口,標准發送路徑還有一個環節,該環節和第二、三、四步所涉及的OnCmdMsg函數,將在下兩節再次具體分析。
命令消息的派發和消息的多次處理
命令消息的派發
如前3.1所述,CCmdTarget的靜態成員函數DispatchCmdMsg用來派發命令消息給指定的命令目標的消息處理函數。
static BOOL DispatchCmdMsg(CCmdTarget* pTarget,
UINT nID, int nCode,
AFX_PMSG pfn, void* pExtra, UINT nSig,
AFX_CMDHANDLERINFO* pHandlerInfo)
前面在講CCmdTarget時,提到了該函數。這裡講述它的實現:
第一個參數指向處理消息的對象;第二個參數是命令ID;第三個是通知消息等;第四個是消息處理函數地址;第五個參數用於存放一些有用的信息,根據nCode的值表示不同的意義,例如當消息是WM_NOFITY,指向一個NMHDR結構(關於WM_NOTIFY,參見4.4.4.2節通知消息的處理);第六個參數標識消息處理函數原型;第七個參數是一個指針,指向AFX_CMDHANDLERINFO結構。前六個參數(除了第五個外)都是向函數傳遞信息,第五個和第七個參數是雙向的,既向函數傳遞信息,也可以向調用者返回信息。
關於AFX_CMDHANDLERINFO結構:
struct AFX_CMDHANDLERINFO
{
CCmdTarget* pTarget;
void (AFX_MSG_CALL CCmdTarget::*pmf)(void);
};
第一個成員是一個指向命令目標對象的指針,第二個成員是一個指向CCmdTarget成員函數的指針。
該函數的實現流程可以如下描述:
首先,它檢查參數pHandlerInfo是否空,如果不空,則用pTarget和pfn填寫其指向的結構,返回TRUE;通常消息處理時傳遞來的pHandlerInfo空,而在使用OnCmdMsg來測試某個對象是否處理某條命令時,傳遞一個非空的pHandlerInfo指針。若返回TRUE,則表示可以處理那條消息。
如果pHandlerInfo空,則進行消息處理函數的調用。它根據參數nSig的值,把參數pfn的類型轉換為要調用的消息處理函數的類型。這種指針轉換技術和前面講述的Windows消息的處理是一樣的。
消息的多次處理
如果消息處理函數不返回值,則DispatchCmdMsg返回TRUE;否則,DispatchCmdMsg返回消息處理函數的返回值。這個返回值沿著消息發送相反的路徑逐級向上傳遞,使得各個環節的OnCmdMsg和OnCommand得到返回的處理結果:TRUE或者FALSE,即成功或者失敗。
這樣就產生了一個問題,如果消息處理函數有意返回一個FALSE,那麼不就傳遞了一個錯誤的信息?例如,OnCmdMsg函數得到FALSE返回值,就認為消息沒有被處理,它將繼續發送消息到下一環節。的確是這樣的,但是這不是MFC的漏洞,而是有意這麼設計的,用來處理一些特別的消息映射宏,實現同一個消息的多次處理。
通常的命令或者通知消息是沒有返回值的(見4.4.2節的消息映射宏),僅僅一些特殊的消息處理函數具有返回值,這類消息的消息處理函數是使用擴展消息映射宏映射的,例如:
ON_COMMAND對應的ON_COMMAND_EX
擴展映射宏和對應的普通映射宏的參數個數相同,含義一樣。但是擴展映射宏的消息處理函數的原型和對應的普通映射宏相比,有兩個不同之處:一是多了一個UINT類型的參數,另外就是有返回值(返回BOOL類型)。回顧4.4.2章節,范圍映射宏ON_COMMAND_RANGE的消息處理函數也有一個這樣的參數,該參數在兩處的含義是一樣的,例如:命令消息擴展映射宏ON_COMMAND_EX定義的消息處理函數解釋該參數是當前要處理的命令消息ID。有返回值的意義在於:如果擴展映射宏的消息處理函數返回FALSE,則導致當前消息被發送給消息路徑上的下一個消息目標處理。
綜合來看,ON_COMMAND_EX宏有兩個功能:
一是可以把多個命令消息指定給一個消息處理函數處理。這類似於ON_COMMAND_RANGE宏的作用。不過,這裡的多條消息的命令ID或者控制子窗口ID可以不連續,每條消息都需要一個ON_COMMAND_EX宏。
二是可以讓幾個消息目標處理同一個命令或者通知或者反射消息。如果消息發送路徑上較前的命令目標不處理消息或者處理消息後返回FALSE,則下一個命令目標將繼續處理該消息。
對於通知消息、反射消息,它們也有擴展映射宏,而且上述論斷也適合於它們。例如:
ON_NOTIFY對應的ON_NOTIFY_EX
ON_CONTROL對應的ON_CONTROL_EX
ON_CONTROL_REFLECT對應的ON_CONTROL_REFLECT_EX
等等。
范圍消息映射宏也有對應的擴展映射宏,例如:
ON_NOTIFY_RANGE對應的ON_NOTIFY_EX_RANGE
ON_COMMAND_RANGE對應的ON_COMMAND_EX_RANGE
使用這些宏的目的在於利用擴展宏的第二個功能:實現消息的多次處理。
關於擴展消息映射宏的例子,參見13.2..4.4節和13.2.4.6節。
一些消息處理類的OnCmdMsg的實現
從以上論述知道,OnCmdMsg虛擬函數在MFC命令消息的發送中扮演了重要的角色,CFrameWnd的OnCmdMsg實現了MFC的標准命令消息發送路徑。
那麼,就產生一個問題:如果命令消息不送給邊框窗口對象,那麼就不會有按標准命令發送路徑發送消息的過程?答案是肯定的。例如一個菜單被一個對話框窗口所擁有,那麼,菜單命令將送給MFC對話框窗口對象處理,而不是MFC邊框窗口處理,當然不會和CFrameWnd的處理流程相同。
但是,有一點需要指出,一般標准的SDI和MDI應用程序,只有主邊框窗口擁有菜單和工具條等用戶接口對象,只有在用戶與用戶接口對象進行交互時,才產生命令,產生的命令必然是送給SDI或者MDI程序的主邊框窗口對象處理。
下面,討論幾個MFC類覆蓋OnCmdMsg虛擬函數時的實現。這些類的OnCmdMsg或者可能是標准MFC命令消息路徑的一個環節,或者可能是一個獨立的處理過程(對於其中的MFC窗口類)。
從分析CView的OnCmdMsg實現開始。
CView的OnCmdMsg
CView::OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
首先,調用CWnd::OnCmdMsg,結果是搜索當前視的類和基類的消息映射數組,搜索順序是從下層到上層。若某一層實現了對命令消息nID的處理,則調用它的實現函數;否則,調用m_pDocument->OnCmdMsg,把命令消息送給文檔類處理。m_pDocument是和當前視關聯的文檔對象指針。如果文檔對象類實現了OnCmdMsg,則調用它的覆蓋函數;否則,調用基類(例如CDocument)的OnCmdMsg。
接著,討論CDocument的實現。
CDocument的 OnCmdMsg
BOOL CDocument::OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
首先,調用CCmdTarget::OnCmdMsg,導致當前對象(this)的類和基類的消息映射數組被搜索,看是否有對應的消息處理函數可用。如果有,就調用它;如果沒有,則調用文檔模板的OnCmdMsg函數(m_pTemplate->OnCmdMsg)把消息送給文檔模板處理。
MFC文檔模板沒有覆蓋OnCmdMsg,導致基類CCmdTarget的OnCmdMsg被調用,看是否有文檔模板類或基類實現了對消息的處理。是的話,調用對應的消息處理函數,否則,返回FALSE。從前面的分析知道,CCmdTarget類的消息映射數組是空的,所以這裡返回FALSE。
CDialog的OnCmdMsg
BOOL CDialog::OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
調用CWnd::OnCmdMsg,讓對話框或其基類處理消息。
如果還沒有處理,而且是控制消息或系統命令或非命令按鈕,則返回FALSE,不作進一步處理。否則,調用父窗口的OnCmdmsg(GetParent()->OnCmdmsg)把消息送給父窗口處理。
如果仍然沒有處理,則調用當前線程的OnCmdMsg(GetThread()->OnCmdMsg)把消息送給線程對象處理。
如果最後沒有處理,返回FALSE。
CMDIFrameWnd的OnCmdMsg
對於MDI應用程序,MDI主邊框窗口首先是把命令消息發送給活動的MDI文檔邊框窗口進行處理。MDI主邊框窗口對OnCmdMsg的實現函數的原型如下:
BOOL CMDIFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
如果有激活的文檔邊框窗口,則調用它的OnCmdMsg(MDIGetActive()->OnCmdMsg)把消息交給它進行處理。MFC的文檔邊框窗口類並沒有覆蓋OnCmdMsg函數,所以基類CFrameWnd的函數被調用,導致文檔邊框窗口的活動視、文檔邊框窗口本身、應用程序對象依次來進行消息處理。
如果文檔邊框窗口沒有處理,調用CFrameWnd::OnCmdMsg把消息按標准路徑發送,重復第一次的步驟,不過對於MDI邊框窗口來說不存在活動視,所以省卻了讓視處理消息的必要;接著讓MDI邊框窗口本身來處理消息,如果它還沒有處理,則讓應用程序對象進行消息處理──雖然這是一個無用的重復。
除了CView、CDocument和CMDIFrameWnd類,還有幾個OLE相關的類覆蓋了OnCmdMsg函數。OLE的處理本書暫不涉及,CDialog::OnCmdMsg將在對話框章節專項討論其具體實現。
一些消息處理類的OnCommand的實現
除了虛擬函數OnCmdMsg,還有一個虛擬函數OnCommand在命令消息的發送中占有重要地位。在處理命令或者通知消息時,OnCommand被MFC窗口過程調用,然後它調用OnCmdMsg按一定路徑傳送消息。除了CWnd類和一些OLE相關類外,MFC裡主要還有MDI邊框窗口實現了OnCommand。
BOOL CMDIFrameWnd::OnCommand(WPARAM wParam, LPARAM lParam)
第一,如果存在活動的文檔邊框窗口,則使用AfxCallWndProc調用它的窗口過程,把消息送給文檔邊框窗口來處理。這將導致文檔邊框窗口的OnCmdMsg作如下的處理:
活動視處理消息→與視關聯的文檔處理消息→本文檔邊框窗口處理消息→應用程序對象處理消息→文檔邊框窗口缺省處理
任何一個環節如果處理消息,則不再向下發送消息,處理終止。如果消息仍然沒有被處理,就只有交給主邊框窗口了。
第二,第一步沒有處理命令,繼續調用CFrameWnd::OnCommand,將導致CMDIFrameWnd的OnCmdMsg被調用。從前面的分析知道,將再次把消息送給MDI邊框窗口的活動文檔邊框窗口,第一步的過程除了文檔邊框窗口缺省處理外都將被重復。具體的處理過程見前文的CMDIFrameWnd::OnCmdMsg函數。
對於MDI消息,如果主邊框窗口還不處理的話,交給CMDIFrameWnd的DefWindowProc作缺省處理。
消息沒有處理,返回FALSE。
上述分析綜合了OnCommand和OnCmdMsg的處理,它們是在MFC內部MDI邊框窗口處理命令消息的完整的流程和標准的步驟。整個處理過程再次表明了邊框窗口在處理命令消息時的中心作用。從程序員的角度來看,可以認為整個標准處理路徑如下:
活動視處理消息→與視關聯的文檔處理消息→本文檔邊框窗口處理消息→應用程序對象處理消息→文檔邊框窗口缺省處理→MDI邊框窗口處理消息→MDI邊框窗口缺省處理
任何一個環節如果處理消息,不再向下發送消息,急處理終止。
對控制通知消息的接收和處理
WM_COMMAND控制通知消息的處理
WM_COMMAND控制通知消息的處理和WM_COMMAND命令消息的處理類似,但是也有不同之處。
首先,分析處理WM_COMMAND控制通知消息和命令消息的相似處。如前所述,命令消息和控制通知消息都是由窗口過程給OnCommand處理(參見CWnd::OnWndMsg的實現),OnCommand通過wParam和lParam參數區分是命令消息或通知消息,然後送給OnCmdMsg處理(參見CWnd::OnCommnd的實現)。
其次,兩者的不同之處是:
命令消息一般是送給主邊框窗口的,這時,邊框窗口的OnCmdMsg被調用;而控制通知消息送給控制子窗口的父窗口,這時,父窗口的OnCmdMsg被調用。
OnCmdMsg處理命令消息時,通過命令分發可以由多種命令目標處理,包括非窗口對象如文檔對象等;而處理控制通知消息時,不會有消息分發的過程,控制通知消息最終肯定是由窗口對象處理的。
不過,在某種程度上可以說,控制通知消息由窗口對象處理是一種習慣和約定。當使用ClassWizard進行消息映射時,它不提供把控制通知消息映射到非窗口對象的機會。但是,手工地添加消息映射,讓非窗口對象處理控制通知消息的可能是存在的。例如,對於CFormView,一方面它具備接受WM_COMMAND通知消息的條件,另一方面,具備把WM_COMMAND消息派發給關聯文檔對象處理的能力,所以給CFormView的通知消息是可以讓文檔對象處理的。
事實上,BN_CLICKED控制通知消息的處理和命令消息的處理完全一樣,因為該消息的通知代碼是0,ON_BN_CLICKED(id,memberfunction)和ON_COMMAND(id,memberfunction)是等同的。
此外,MFC的狀態更新處理機制就是建立在通知消息可以發送給各種命令目標的基礎之上的。關於MFC的狀態更新處理機制,見後面4.4.4.4節的討論。
控制通知消息可以反射給子窗口處理。OnCommand判定當前消息是WM_COMAND通知消息之後,首先它把消息反射給控制子窗口處理,如果子窗口處理了反射消息,OnCommand不會繼續調用OnCmdMsg讓父窗口對象來處理通知消息。
WM_NOTIFY消息及其處理:
(1)WM_NOTIFY消息
還有一種通知消息WM_NOTIFY,在Win32中用來傳遞信息復雜的通知消息。WM_NOTIFY消息怎麼來傳遞復雜的信息呢?WM_NOTIFY的消息參數wParam包含了發送通知消息的控制窗口ID,另一個參數lParam包含了一個指針。該指針指向一個NMHDR結構,或者更大的結構,只要它的第一個結構成員是NMHDR結構。
NMHDR結構:
typedef struct tagNMHDR {
HWND hwndFrom;
UINT idFrom;
UINT code;
} NMHDR;
上述結構有三個成員,分別是發送通知消息的控制窗口的句柄、ID和通知消息代碼。
舉一個更大、更復雜的結構例子:列表控制窗發送LVN_KEYDOWN控制通知消息,則lParam包含了一個指向LV_KEYDOWN結構的指針。其結構如下:
typedef struct tagLV_KEYDOWN {
NMHDR hdr;
WORD wVKey;
UINT flags;
}LV_KEYDOWN;
它的第一個結構成員hdr就是NMHDR類型。其他成員包含了更多的信息:哪個鍵被按下,哪些輔助鍵(SHIFT、CTRL、ALT等)被按下。
(2)WM_NOTIFY消息的處理
在分析CWnd::OnWndMsg函數時,曾指出當消息是WM_NOTIFY時,它把消息傳遞給OnNotify虛擬函數處理。這是一個虛擬函數,類似於OnCommand,CWnd和派生類都可以覆蓋該函數。OnNotify的函數原型如下:
BOOL CWnd::OnNotify(WPARAM, LPARAM lParam, LRESULT* pResult)
參數1是發送通知消息的控制窗口ID,沒有被使用;參數2是一個指針;參數3指向一個long類型的數據,用來返回處理結果。
WM_NOTIFY消息的處理過程如下:
反射消息給控制子窗口處理。
如果子窗口不處理反射消息,則交給OnCmdMsg處理。給OnCmdMsg的四個參數分別如下:第一個是命令消息ID,第四個為空;第二個高階word是WM_NOTIFY,低階word是通知消息;第三個參數是指向AFX_NOTIFY結構的指針。第二、三個參數有別於OnCommand送給OnCmdMsg的參數。
AFX_NOTIFY結構:
struct AFX_NOTIFY
{
LRESULT* pResult;
NMHDR* pNMHDR;
};
pNMHDR的值來源於參數2 lParam,該結構的域pResult用來保存處理結果,域pNMHDR用來傳遞信息。
OnCmdMsg後續的處理和WM_COMMAND通知消息基本相同,只是在派發消息給消息處理函數時,DispatchMsdMsg的第五個參數pExtra指向OnCmdMsg傳遞給它的AFX_NOTIFY類型的參數,而不是空指針。這樣,處理函數就得到了復雜的通知消息信息。
消息反射
(1)消息反射的概念
前面討論控制通知消息時,曾經多次提到了消息反射。MFC提供了兩種消息反射機制,一種用於OLE控件,一種用於Windows控制窗口。這裡只討論後一種消息反射。
Windows控制常常發送通知消息給它們的父窗口,通常控制消息由父窗口處理。但是在MFC裡頭,父窗口在收到這些消息後,或者自己處理,或者反射這些消息給控制窗口自己處理,或者兩者都進行處理。如果程序員在父窗口類覆蓋了通知消息的處理(假定不調用基類的實現),消息將不會反射給控制子窗口。這種反射機制是MFC實現的,便於程序員創建可重用的控制窗口類。
MFC的CWnd類處理以下控制通知消息時,必要或者可能的話,把它們反射給子窗口處理:
WM_CTLCOLOR,
WM_VSCROLL,WM_HSCROLL,
WM_DRAWITEM,WM_MEASUREITEM,
WM_COMPAREITEM,WM_DELETEITEM,
WM_CHARTOITEM,WM_VKEYTOITEM,
WM_COMMAND、WM_NOTIFY。
例如,對WM_VSCROLL、WM_HSCROLL消息的處理,其消息處理函數如下:
void CWnd::OnHScroll(UINT, UINT, CScrollBar* pScrollBar)
{
//如果是一個滾動條控制,首先反射消息給它處理
if (pScrollBar != NULL && pScrollBar->SendChildNotifyLastMsg())
return; //控制窗口成功處理了該消息
Default();
}
又如:在討論OnCommand和OnNofity函數處理通知消息時,都曾經指出,它們首先調用ReflectLastMsg把消息反射給控制窗口處理。
為了利用消息反射的功能,首先需要從適當的MFC窗口派生出一個控制窗口類,然後使用ClassWizard給它添加消息映射條目,指定它處理感興趣的反射消息。下面,討論反射消息映射宏。
上述消息的反射消息映射宏的命名遵循以下格式:“ON”前綴+消息名+“REFLECT”後綴,例如:消息WM_VSCROLL的反射消息映射宏是ON_WM_VSCROLL_REFECT。但是通知消息WM_COMMAND和WM_NOTIFY是例外,分別為ON_CONTROL_REFLECT和ON_NOFITY_REFLECT。狀態更新通知消息的反射消息映射宏是ON_UPDATE_COMMAND_UI_REFLECT。
消息處理函數的名字和去掉“WM_”前綴的消息名相同 ,例如WM_HSCROLL反射消息處理函數是Hscroll。
消息處理函數的原型這裡不一一列舉了。
這些消息映射宏和消息處理函數的原型可以借助於ClassWizard自動地添加到程序中。ClassWizard添加消息處理函數時,可以處理的反射消息前面有一個等號,例如處理WM_HSCROLL的反射消息,選擇映射消息“=EN_HSC ROLL”。ClassWizard自動的添加消息映射宏和處理函數到框架文件。
(2)消息反射的處理過程
如果不考慮有OLE控件的情況,消息反射的處理流程如下圖所示:
首先,調用CWnd的成員函數SendChildNotifyLastMsg,它從線程狀態得到本線程最近一次獲取的消息(關於線程狀態,後面第9章會詳細介紹)和消息參數,並且把這些參數傳遞給函數OnChildNotify。注意,當前的CWnd對象就是MFC控制子窗口對象。
OnChlidNofify是CWnd定義的虛擬函數,不考慮OLE控制的話,它僅僅只調用ReflectChildNotify。OnChlidNofify可以被覆蓋,所以如果程序員希望處理某個控制的通知消息,除了采用消息映射的方法處理通知反射消息以外,還可以覆蓋OnChlidNotify虛擬函數,如果成功地處理了通知消息,則返回TRUE。
ReflectChildNotify是CWnd的成員函數,完成反射消息的派發。對於WM_COMMAND,它直接調用CWnd::OnCmdMsg派發反射消息WM_REFLECT_BASE+WM_COMMAND;對於WM_NOTIFY,它直接調用CWnd::OnCmdMsg派發反射消息WM_REFLECT_BASE+WM_NOFITY;對於其他消息,則直接調用CWnd::OnWndMsg(即CmdTarge::OnWndMsg)派發相應的反射消息,例如WM_REFLECT_BASE+WM_HSCROLL。
注意:ReflectChildNotify直接調用了CWnd的OnCmdMsg或OnWndMsg,這樣反射消息被直接派發給控制子窗口,省卻了消息發送的過程。
接著,控制子窗口如果處理了當前的反射消息,則返回反射消息被成員處理的信息。
(3)一個示例
如果要創建一個編輯框控制,要求它背景使用黃色,其他特性不變,則可以從CEdit派生一個類CYellowEdit,處理通知消息WM_CTLCOLOR的反射消息。CYellowEdit有三個屬性,定義如下:
CYellowEdit::CYellowEdit()
{
m_clrText = RGB( 0, 0, 0 );
m_clrBkgnd = RGB( 255, 255, 0 );
m_brBkgnd.CreateSolidBrush( m_clrBkgnd );
}
使用ClassWizard添加反射消息處理函數:
函數原型:
afx_msg void HScroll();
消息映射宏:
ON_WM_CTLCOLOR_REFLECT()
函數的框架
HBRUSH CYellowEdit::CtlColor(CDC* pDC, UINT nCtlColor)
{
// TODO:添加代碼改變設備描述表的屬性
// TODO: 如果不再調用父窗口的處理,則返回一個非空的刷子句柄
return NULL;
}
添加一些處理到函數CtlColor中,如下:
pDC->SetTextColor( m_clrText );//設置文本顏色
pDC->SetBkColor( m_clrBkgnd );//設置背景顏色
return m_brBkgnd; //返回背景刷
這樣,如果某個地方需要使用黃色背景的編輯框,則可以使用CYellowEdit控制。
對更新命令的接收和處理
用戶接口對象如菜單、工具條有多種狀態,例如:禁止,可用,選中,未選中,等等。這些狀態隨著運行條件的變化,由程序來進行更新。雖然程序員可以自己來完成更新,但是MFC框架為自動更新用戶接口對象提供了一個方便的接口,使用它對程序員來說可能是一個好的選擇。
實現方法
每一個用戶接口對象,如菜單、工具條、控制窗口的子窗口,都由唯一的ID號標識,用戶和它們交互時,產生相應ID號的命令消息。在MFC裡,一個用戶接口對象還可以響應CN_UPDATE_COMMAND_UI通知消息。因此,對每個標號ID的接口對象,可以有兩個處理函數:一個消息處理函數用來處理該對象產生的命令消息ID,另一個狀態更新函數用來處理給該對象的CN_UPDATE_COMMAND_UID的通知消息。
使用ClassWizard可把狀態更新函數加入到某個消息處理類,其結果是:
在類的定義中聲明一個狀態函數;
在消息映射中使用ON_UPDATE_COMMAND_UI宏添加一個映射條目;
在類的實現文件中實現狀態更新函數的定義。
ON_UPDATE_COMMAND_UI給指定ID的用戶對象指定狀態更新函數,例如:
ON_UPDATE_COMMAND_UI(ID_EDIT_COPY, OnUpdateEditCopy)
映射標識號ID為ID_EDIT_COPY菜單的通知消息CN_UPDATE_COMMAND_UI到函數OnUpdateEditCopy。用於給EDIT(編輯菜單)的菜單項ID_EDIT_COPY(復制)添加一個狀態處理函數OnUpdateEditCopy,通過處理通知消息CN_UPDATE_COMMAND_UI實現該菜單項的狀態更新。
狀態處理函數的原型如下:
afxmsg void ClassName::OnUpdateEditPaste(CCmdUI* pCmdUI)
CCmdUI對象由MFC自動地構造。在完善函數的實現時,使用pCmdUI對象和CmdUI的成員函數實現菜單項ID_EDIT_COPY的狀態更新,讓它變灰或者變亮,也就是禁止或者允許用戶使用該菜單項。
狀態更新命令消息
要討論MFC的狀態更新處理,先得了解一條特殊的消息。MFC的消息映射機制除了處理各種Windows消息、控制通知消息、命令消息、反射消息外,還處理一種特別的“通知命令消息”,並通過它來更新菜單、工具欄(包括對話框工具欄)等命令目標的狀態。
這種“通知命令消息”是MFC內部定義的,消息ID是WM_COMMAND,通知代碼是CN_UPDATE_COMMAND_UI(0XFFFFFFFF)。
它不是一個真正意義上的通知消息,因為沒有控制窗口產生這樣的通知消息,而是MFC自己主動產生,用於送給工具條窗口或者主邊框窗口,通知它們更新用戶接口對象的狀態。
它和標准WM_COMMAND命令消息也不相同,因為它有特定的通知代碼,而命令消息通知代碼是0。
但是,從消息的處理角度,可以把它看作是一條通知消息。如果是工具條窗口接收該消息,則在發送機制上它和WM_COMMAND控制通知消息是相同的,相當於讓工具條窗口處理一條通知消息。如果是邊框窗口接收該消息,則在消息的發送機制上它和WM_COMMAND命令消息是相同的,可以讓任意命令目標處理該消息,也就是說邊框窗口可以把該條通知消息發送給任意命令目標處理。
從程序員的角度,可以把它看作一條“狀態更新命令消息”,像處理命令消息那樣處理該消息。每條命令消息都可以對應有一條“狀態更新命令消息”。ClassWizard也支持讓任意消息目標處理“狀態更新命令消息”(包括非窗口命令目標),實現用戶接口狀態的更新。
在這條消息發送時,通過OnCmdMsg的第三個參數pExtra傳遞一些信息,表示要更新的用戶接口對象。pExtra指向一個CCmdUI對象。這些信息將傳遞給狀態更新命令消息的處理函數。
下面討論用於更新用戶接口對象狀態的類CCmdUI。
類CCmdUI
CCmdUI不是從CObject派生,沒有基類。
成員變量
m_nID 用戶接口對象的ID
m_nIndex 用戶接口對象的index
m_pMenu 指向CCmdUI對象表示的菜單
m_pSubMenu 指向CCmdUI對象表示的子菜單
m_pOther 指向其他發送通知消息的窗口對象
m_pParentMenu 指向CCmdUI對象表示的子菜單
成員函數
Enable(BOOL bOn = TRUE ) 禁止用戶接口對象或者使之可用
SetCheck( int nCheck = 1) 標記用戶接口對象選中或未選中
SetRadio(BOOL bOn = TRUE)
SetText(LPCTSTR lpszText)
ContinueRouting()
還有一個MFC內部使用的成員函數:
DoUpdate(CCmdTarget* pTarget, BOOL bDisableIfNoHndler)
其中,參數1指向處理接收更新通知的命令目標,一般是邊框窗口;參數2指示如果沒有提供處理函數(例如某個菜單沒有對應的命令處理函數),是否禁止用戶對象。
DoUpdate作以下事情:
首先,發送狀態更新命令消息給參數1表示的命令目標:調用pTarget->OnCmdMsg(m_nID, CN_UPDATE_COMMAND_UI, this, NULL)發送m_nID對象的通知消息CN_UPDATE_COMMAND_UI。OnCmdMsg的參數3取值this,包含了當前要更新的用戶接口對象的信息。
然後,如果參數2為TRUE,調用pTarget->OnCmdMsg(m_nID, CN_COMMAND, this, &info)測試命令消息m_nID是否被處理。這時,OnCmdMsg的第四個參數非空,表示僅僅是測試,不是真的要派發消息。如果沒有提供命令消息m_nID的處理函數,則禁止用戶對象m_nID,否則使之可用。
從上面的討論可以知道:通過其結構,一個CCmdUI對象標識它表示了哪一個用戶接口對象,如果是菜單接口對象,pMenu表示了要更新的菜單對象;如果是工具條,pOther表示了要更新的工具條窗口對象,nID表示了工具條按鈕ID。
所以,由參數上狀態更新消息的消息處理函數就知道要更新什麼接口對象的狀態。例如,第1節的函數OnUpdateEditPaste,函數參數pCmdUI表示一個菜單對象,需要更新該菜單對象的狀態。
通過其成員函數,一個CCmdUI可以更新、改變用戶接口對象的狀態。例如,CCmdUI可以管理菜單和對話框控制的狀態,調用Enable禁止或者允許菜單或者控制子窗口,等等。
所以,函數OnUpdateEditPaste可以直接調用參數的成員函數(如pCmdUI->Enable)實現菜單對象的狀態更新。
由於接口對象的多樣性,其他接口對象將從CCmdUI派生出管理自己的類來,覆蓋基類的有關成員函數如Enable等,提供對自身狀態更新的功能。例如管理狀態條和工具欄更新的CStatusCmdUI類和CToolCmdUI類。
自動更新用戶接口對象狀態的機制
MFC提供了分別用於更新菜單和工具條的兩種途徑。
更新菜單狀態
當用戶對菜單如File單擊鼠標時,就產生一條WM_INITMENUPOPUP消息,邊框窗口在菜單下拉之前響應該消息,從而更新該菜單所有項的狀態。
在應用程序開始運行時,邊框也會收到WM_INITMENUPOPUP消息。
更新工具條等狀態
當應用程序進入空閒處理狀態時,將發送WM_IDLEUPDATECMDUI消息,導致所有的工具條用戶對象的狀態處理函數被調用,從而改變其狀態。WM_IDLEUPDATECMDUI是MFC自己定義和使用的消息。
在窗口初始化時,工具條也會收到WM_IDLEUPDATECMDUI消息。
菜單狀態更新的實現
MFC讓邊框窗口來響應WM_INITMENUPOPUP消息,消息處理函數是OnInitMenuPopup,其原型如下:
afx_msg void CFrameWnd::OnInitMenuPopup( CMenu* pPopupMenu,
UINT nIndex, BOOL bSysMenu );
第一個參數指向一個CMenu對象,是當前按擊的菜單;第二個參數是菜單索引;第三個參數表示子菜單是否是系統控制菜單。
函數的處理:
如果是系統控制菜單,不作處理;否則,創建CCmdUI對象state,給它的各個成員如m_pMenu,m_pParentMenu,m_pOther等賦值。
對該菜單的各個菜單項,調函數state.DoUpdate,用CCmdUI的DoUpdate來更新狀態。DoUpdate的第一個參數是this,表示命令目標是邊框窗口;在CFrameWnd的成員變量m_bAutoMenuEnable為TRUE時(表示如果菜單m_nID沒有對應的消息處理函數或狀態更新函數,則禁止它),把DoUpdate的第二個參數bDisableIfNoHndler置為TRUE。
順便指出,m_bAutoMenuEnable缺省時為TRUE,所以,應用程序啟動時菜單經過初始化處理,沒有提供消息處理函數或狀態更新函數的菜單項被禁止。
工具條等狀態更新的實現
圖4-5表示了消息空閒時MFC更新用戶對象狀態的流程:
MFC提供的缺省空閒處理向頂層窗口(框架窗口)的所有子窗口發送消息WM_IDLEUPDATECMDUI;MFC的控制窗口(工具條、狀態欄等)實現了對該消息的處理,導致用戶對象狀態處理函數的調用。
雖然兩種途徑調用了同一狀態處理函數,但是傳遞的 CCmdUI參數從內部構成上是不一樣的:第一種傳遞的CCmdUI對象表示了一菜單對象,(pMenu域被賦值);第二種傳遞了一個窗口對象(pOther域被賦值)。同樣的狀態改變動作,如禁止、允許狀態的改變,前者調用了CMenu的成員函數EnableMenuItem,後者使用了CWnd的成員函數EnabelWindow。但是,這些不同由CCmdUI對象內部區分、處理,對用戶是透明的:不論菜單還是對應的工具條,用戶都用同一個狀態處理函數使用同樣的形式來處理。
這一節分析了用戶界面更新的原理和機制。在後面第13章討論工具條和狀態欄時,將詳細的分析這種機制的具體實現。
消息的預處理
到現在為止,詳細的討論了MFC的消息映射機制。但是,為了提高效率和簡化處理,MFC提供了一種消息預處理機制,如果一條消息在預處理時被過濾掉了(被處理),則不會被派發給目的窗口的窗口過程,更不會進入消息循環了。
顯然,能夠進行預處理的消息只可能是隊列消息,而且必須在消息派發之前進行預處理。因此,MFC在實現消息循環時,對於得到的每一條消息,首先送給目的窗口、其父窗口、其祖父窗口乃至最頂層父窗口,依次進行預處理,如果沒有被處理,則進行消息轉換和消息派發,如果某個窗口實現了預處理,則終止。有關實現見後面關於CWinThread線程類的章節,CWinThread的Run函數和PreTranslateMessage函數以及CWnd的函數WalkPreTranslateTree實現了上述要求和功能。這裡要討論的是MFC窗口類如何進行消息預處理。
CWnd提供了虛擬函數PreTranslateMessage來進行消息預處理。CWnd的派生類可以覆蓋該函數,實現自己的預處理。下面,討論幾個典型的預處理。
首先,是CWnd的預處理:
預處理函數的原型為:
BOOL CWnd::PreTranslateMessage(MSG* pMsg)
CWnd類主要是處理和過濾Tooltips消息。關於該函數的實現和Tooltips消息,見後面第13章關於工具欄的討論。
然後,是CFrameWnd的預處理:
CFrameWnd除了調用基類CWnd的實現過濾Tooltips消息之外,還要判斷當前消息是否是鍵盤快捷鍵被按下,如果是,則調用函數::TranslateAccelerator(m_hWnd, hAccel, pMsg)處理快捷鍵。
接著,是CMDIChildWnd的預處理:
CMDIChildWnd的預處理過程和CFrameWnd的一樣,但是不能依靠基類CFrameWnd的實現,必須覆蓋它。因為MDI子窗口沒有菜單,所以它必須在MDI邊框窗口的上下文中來處理快捷鍵,它調用了函數::TranslateAccelerator(GetMDIFrame()->m_hWnd, hAccel, pMsg)。
討論了MDI子窗口的預處理後,還要討論MDI邊框窗口:
CMDIFrameWnd的實現除了CFrameWnd的實現的功能外,它還要處理MDI快捷鍵(標准MDI界面統一使用的系統快捷鍵)。
在後面,還會討論CDialog、CFormView、CToolBar等的消息預處理及其實現。
至於CWnd::WalkPreTranslateTree函數,它從接受消息的窗口開始,逐級向父窗回溯,逐一對各層窗口調用PreTranslateMessage函數,直到消息被處理或者到最頂層窗口為止。
MFC消息映射的回顧
從處理命令消息的過程可以看出,Windows消息和控制消息的處理要比命令消息的處理簡單,因為查找消息處理函數時,後者只要搜索當前窗口對象(this所指)的類或其基類的消息映射入口表。但是,命令消息就要復雜多了,它沿一定的順序鏈查找鏈上的各個命令目標,每一個被查找的命令目標都要搜索它的類或基類的消息映射入口表。
MFC通過消息映射的手段,以一種類似C++虛擬函數的概念向程序員提供了一種處理消息的方式。但是,若使用C++虛擬函數實現眾多的消息,將導致虛擬函數表極其龐大;而使用消息映射,則僅僅感興趣的消息才加入映射表,這樣就要節省資源、提高效率。這套消息映射機制的基礎包括以下幾個方面:
消息映射入口表的實現:采用了C++靜態成員和虛擬函數的方法來表示和得到一個消息映射類(CCmdTarget或派生類)的映射表。
消息查找的實現:從低層到高層搜索消息映射入口表,直至根類CCmdTarget。
消息發送的實現:主要以幾個虛擬函數為基礎來實現標准MFC消息發送路徑:OnComamnd、OnNotify、OnWndMsg和OnCmdMsg。、
OnWndMsg是CWnd類或其派生類的成員函數,由窗口過程調用。它處理標准的Windows消息。
OnCommand是CWnd類或其派生類的成員函數,由OnWndMsg調用來處理WM_COMMAND消息,實現命令消息或者控制通知消息的發送。如果派生類覆蓋該函數,則必須調用基類的實現,否則將不能自動的處理命令消息映射,而且必須使用該函數接受的參數(不是程序員給定值)調用基類的OnCommand。
OnNotify是CWnd類或其派生類的成員函數,由OnWndMsg調用來處理WM_NOTIFY消息,實現控制通知消息的發送。
OnCmdMsg是CCmdTarget類或其派生類的成員函數。被OnCommand調用,用來實現命令消息發送和派發命令消息到命令消息處理函數。
自動更新用戶對象狀態是通過MFC的命令消息發送機制實現的。
控制消息可以反射給控制窗口處理。
隊列消息在發送給窗口過程之前可以進行消息預處理,如果消息被MFC窗口對象預處理了,則不會進入消息發送過程。