MFC的類非常多,繼承關系復雜,如何完成MFC巨大的類層次之間消息的傳遞是一個技術難點,最簡單的就是采用虛函數機制,每繼承一個類,就覆蓋父類的函數,但問題來了,MFC有上百個類,如果使用虛函數,那麼每個派生類都會生成一個巨大的虛函數表,效率低下,內存使用率高,違背了微軟設計MFC的准則。微軟采用了所謂的消息映射機制,來完成不同對象之間消息的傳遞,本文就MFC9.0源碼進行分析,大致講解MFC的消息映射機制。
步入正題,在AfxWinMain() 函數中,當MFC框架初始化完成後,即 pThread->InitInstance() 執行完成,就開始進行消息循環,入口函數是pThread->Run():
// Perform specific initializations if (!pThread->InitInstance())// MFC初始化框架 { if (pThread->m_pMainWnd != NULL) { TRACE(traceAppMsg, 0, "Warning: Destroying non-NULL m_pMainWnd\n"); pThread->m_pMainWnd->DestroyWindow(); } nReturnCode = pThread->ExitInstance(); goto InitFailure; } nReturnCode = pThread->Run();// 進入消息循環
執行CWinApp:: Run():
// Main running routine until application exits int CWinApp::Run() { if (m_pMainWnd == NULL && AfxOleGetUserCtrl()) { // Not launched /Embedding or /Automation, but has no main window! TRACE(traceAppMsg, 0, "Warning: m_pMainWnd is NULL in CWinApp::Run - quitting application.\n"); AfxPostQuitMessage(0); } return CWinThread::Run(); }
執行CWinThread::Run():
// main running routine until thread exits int CWinThread::Run() { ASSERT_VALID(this); _AFX_THREAD_STATE* pState = AfxGetThreadState(); // for tracking the idle time state BOOL bIdle = TRUE; LONG lIdleCount = 0; // acquire and dispatch messages until a WM_QUIT message is received. for (;;) { // phase1: check to see if we can do idle work while (bIdle && !::PeekMessage(&(pState->m_msgCur), NULL, NULL, NULL, PM_NOREMOVE)) { // call OnIdle while in bIdle state if (!OnIdle(lIdleCount++)) bIdle = FALSE; // assume "no idle" state } // phase2: pump messages while available do { // pump message, but quit on WM_QUIT if (!PumpMessage()) return ExitInstance(); // reset "no idle" state after pumping "normal" message //if (IsIdleMessage(&m_msgCur)) if (IsIdleMessage(&(pState->m_msgCur))) { bIdle = TRUE; lIdleCount = 0; } } while (::PeekMessage(&(pState->m_msgCur), NULL, NULL, NULL, PM_NOREMOVE)); } }
在 do-while 循環中進行消息的路由,主要函數就是 PumpMessage(),我們跟著進入這個函數看看做了什麼:
BOOL CWinThread::PumpMessage() { return AfxInternalPumpMessage(); }
繼續跟蹤:
BOOL AFXAPI AfxInternalPumpMessage() { _AFX_THREAD_STATE *pState = AfxGetThreadState(); if (!::GetMessage(&(pState->m_msgCur), NULL, NULL, NULL))// 從消息隊列獲取消息 { #ifdef _DEBUG TRACE(traceAppMsg, 1, "CWinThread::PumpMessage - Received WM_QUIT.\n"); pState->m_nDisablePumpCount++; // application must die #endif // Note: prevents calling message loop things in 'ExitInstance' // will never be decremented return FALSE; } #ifdef _DEBUG if (pState->m_nDisablePumpCount != 0) { TRACE(traceAppMsg, 0, "Error: CWinThread::PumpMessage called when not permitted.\n"); ASSERT(FALSE); } #endif #ifdef _DEBUG _AfxTraceMsg(_T("PumpMessage"), &(pState->m_msgCur)); #endif // process this message if (pState->m_msgCur.message != WM_KICKIDLE && !AfxPreTranslateMessage(&(pState->m_msgCur))) { ::TranslateMessage(&(pState->m_msgCur)); ::DispatchMessage(&(pState->m_msgCur)); } return TRUE; }
從上面的代碼我們可以看到 MFC是通過 GetMessage() 來獲取消息,然後再看下面這幾句代碼:
if (pState->m_msgCur.message != WM_KICKIDLE && !AfxPreTranslateMessage(&(pState->m_msgCur))) { ::TranslateMessage(&(pState->m_msgCur)); ::DispatchMessage(&(pState->m_msgCur)); }
也就是說當系統獲取消息後,先調用 AfxPreTranslateMessage() 這個看起來跟我們經常看到的PreTranslateMessage() 很像!我們來看看到底發生了什麼:
BOOL __cdecl AfxPreTranslateMessage(MSG* pMsg) { CWinThread *pThread = AfxGetThread(); if( pThread ) return pThread->PreTranslateMessage( pMsg ); else return AfxInternalPreTranslateMessage( pMsg ); }
執行 pThread->PreTranslateMessage():
BOOL CWinThread::PreTranslateMessage(MSG* pMsg) { ASSERT_VALID(this); return AfxInternalPreTranslateMessage( pMsg ); }
執行AfxInternalPreTranslateMessage():
BOOL AfxInternalPreTranslateMessage(MSG* pMsg) { // ASSERT_VALID(this); CWinThread *pThread = AfxGetThread(); if( pThread ) { // if this is a thread-message, short-circuit this function if (pMsg->hwnd == NULL && pThread->DispatchThreadMessageEx(pMsg)) return TRUE; } // walk from target to main window CWnd* pMainWnd = AfxGetMainWnd(); if (CWnd::WalkPreTranslateTree(pMainWnd->GetSafeHwnd(), pMsg))// 注意這個函數 return TRUE; // in case of modeless dialogs, last chance route through main // window's accelerator table if (pMainWnd != NULL) { CWnd* pWnd = CWnd::FromHandle(pMsg->hwnd); if (pWnd->GetTopLevelParent() != pMainWnd) return pMainWnd->PreTranslateMessage(pMsg); } return FALSE; // no special processing }
執行 CWnd::WalkPreTranslateTree(pMainWnd->GetSafeHwnd(), pMsg)
BOOL PASCAL CWnd::WalkPreTranslateTree(HWND hWndStop, MSG* pMsg) { ASSERT(hWndStop == NULL || ::IsWindow(hWndStop)); ASSERT(pMsg != NULL); // walk from the target window up to the hWndStop window checking // if any window wants to translate this message for (HWND hWnd = pMsg->hwnd; hWnd != NULL; hWnd = ::GetParent(hWnd)) { CWnd* pWnd = CWnd::FromHandlePermanent(hWnd); if (pWnd != NULL) { // target window is a C++ window if (pWnd->PreTranslateMessage(pMsg)) return TRUE; // trapped by target window (eg: accelerators) } // got to hWndStop window without interest if (hWnd == hWndStop) break; } return FALSE; // no special processing }
MFC在後台維護了一個句柄和C++對象指針的映射表,一旦有消息產生時,我們知道這個消息的結構體中包含了該消息所屬窗口的句柄,那麼通過這個句柄我們可以找到相對於的C++對象的指針,在for循環裡面遍歷當前窗口的所有父窗口,查找是否有消息重寫,一旦有子類重寫父類消息的,則通過當前消息所屬窗口的句柄來調用 CWnd::FromHandlePermanent(hWnd) 函數,從而得到當前C++對象的指針,最後調用 PreTranslateMessage(),因為 PreTranslateMessage()是虛函數,所以調用的是子類的 PreTranslateMessage(),該函數可以自定義一部分消息的處理方式。
需要注意的是,PreTranslateMessage() 返回為 FALSE 時,說明沒有發生消息的重寫,則把消息直接給 TranslateMessage() 和DispatchMessage() 進行處理,當返回是TRUE時,則去消息隊列獲取下一條消息。
另外,SendMessage() 是直接發送給WindowProc進行處理(該函數是一個DispatchMessage() 中的一個回調函數,用於處理默認的一些系統消息),沒有進入消息隊列,所以不會被GetMessage() 抓取到,所以也就不會PreTranslateMessage() 抓到了,但是PostMessage() 是進入消息隊列的,是可以被GetMessage() 抓到的。
所以從上面的代碼跟蹤可見,當我們需要重寫一些系統消息時,比如給程序設置一些快捷鍵等,可以在PreTranslateMessage() 中進行操作,當然,不是PreTranslateMessage() 並不是可以重寫所有消息,有一些消息也是無法處理的,這時可以交給WindowProc 進行處理,WindowProc () 也是虛函數。
LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) { // OnWndMsg does most of the work, except for DefWindowProc call LRESULT lResult = 0; if (!OnWndMsg(message, wParam, lParam, &lResult)) lResult = DefWindowProc(message, wParam, lParam); return lResult; }
消息的映射主要是通過 OnWndMsg() 進行處理,考慮到該函數代碼很多,就不全貼了,我們分析其中一小段:
for (/* pMessageMap already init'ed */; pMessageMap->pfnGetBaseMap != NULL; pMessageMap = (*pMessageMap->pfnGetBaseMap)()) { // Note: catch not so common but fatal mistake!! // BEGIN_MESSAGE_MAP(CMyWnd, CMyWnd) ASSERT(pMessageMap != (*pMessageMap->pfnGetBaseMap)()); if (message < 0xC000) { // constant window message if ((lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries, message, 0, 0)) != NULL) { pMsgCache->lpEntry = lpEntry; winMsgLock.Unlock(); goto LDispatch; } } else { // registered windows message lpEntry = pMessageMap->lpEntries; while ((lpEntry = AfxFindMessageEntry(lpEntry, 0xC000, 0, 0)) != NULL) { UINT* pnID = (UINT*)(lpEntry->nSig); ASSERT(*pnID >= 0xC000 || *pnID == 0); // must be successfully registered if (*pnID == message) { pMsgCache->lpEntry = lpEntry; winMsgLock.Unlock(); goto LDispatchRegistered; } lpEntry++; // keep looking past this one } } }
在for循環裡面有這樣一個數據: AFX_MSGMAP* pMessageMap() ,它代表當前C++對象的消息映射表指針,那麼這裡的消息映射表到底是個什麼樣的數據結構,我們來一探究竟。我們打開一個MFC工程,然後再打開一個類,比如我們打開CMainFrm 這個類,在其頭文件中,我們看到有一個宏:DECLARE_MESSAGE_MAP(),進入定義看看:
#define DECLARE_MESSAGE_MAP() \ protected: \ static const AFX_MSGMAP* PASCAL GetThisMessageMap(); \ virtual const AFX_MSGMAP* GetMessageMap() const; \
我們可以發現 這個宏只是聲明了兩個函數原型。再打開CMainFrm.cpp ,我們看到有兩個宏:BEGIN_MESSAGE_MAP() 和 END_MESSAGE_MAP(),看代碼:
#define BEGIN_MESSAGE_MAP(theClass, baseClass) \ PTM_WARNING_DISABLE \ const AFX_MSGMAP* theClass::GetMessageMap() const \ { return GetThisMessageMap(); } \ const AFX_MSGMAP* PASCAL theClass::GetThisMessageMap() \ { \ typedef theClass ThisClass; \ typedef baseClass TheBaseClass; \ static const AFX_MSGMAP_ENTRY _messageEntries[] = \ { #define END_MESSAGE_MAP() \ {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \ }; \ static const AFX_MSGMAP messageMap = \ { &TheBaseClass::GetThisMessageMap, &_messageEntries[0] }; \ return &messageMap; \ } \ PTM_WARNING_RESTORE
從代碼中可見其實這兩個宏定義了一個變量:AFX_MSGMAP messageMap 和兩個函數 GetMessageMap() 、GetThisMessageMap()。那麼 AFX_MSGMAP 是什麼數據結構呢,我們繼續看代碼:
struct AFX_MSGMAP { const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)(); const AFX_MSGMAP_ENTRY* lpEntries; };
又出現了一個數據結構:AFX_MSGMAP_ENTRY,不怕,我們繼續看這個數據的定義:
struct AFX_MSGMAP_ENTRY { UINT nMessage; // windows message UINT nCode; // control code or WM_NOTIFY code UINT nID; // control ID (or 0 for windows messages) UINT nLastID; // used for entries specifying a range of control id's UINT_PTR nSig; // signature type (action) or pointer to message # AFX_PMSG pfn; // routine to call (or special value) };
到此,主要的兩個數據結構都已經出來了,一個是AFX_MSGMAP,另一個是AFX_MSGMAP_ENTRY ,其中後者包含在前者當中,看代碼注釋我們可以大致猜出來 AFX_MSGMAP_ENTRY 代表一條消息的所有信息,所以AFX_MSGMAP的作用就是兩個了,一個是保存當前對象的消息映射表,另一個是獲取基類的消息映射表!所以我們可以回想CMainFrm類中頭文件裡的 DECLARE_MESSAGE_MAP() 宏就是函數原型聲明,而.cpp 文件中的兩個宏之間則定義了一個數組,該數組保存當前類的消息映射表項,每一項表示某一條消息對應的處理函數等信息。
回到OnWndMsg() 中的for循環裡面, 程序從當前對象開始先上遍歷,獲取對象的消息映射表指針,然後執行 AfxFindMessageEntry() 函數,該函數什麼作用呢?我們還是看代碼:
wincore.cpp ///////////////////////////////////////////////////////////////////////////// // Routines for fast search of message maps const AFX_MSGMAP_ENTRY* AFXAPI AfxFindMessageEntry(const AFX_MSGMAP_ENTRY* lpEntry, UINT nMsg, UINT nCode, UINT nID) { #if defined(_M_IX86) && !defined(_AFX_PORTABLE) // 32-bit Intel 386/486 version. ASSERT(offsetof(AFX_MSGMAP_ENTRY, nMessage) == 0); ASSERT(offsetof(AFX_MSGMAP_ENTRY, nCode) == 4); ASSERT(offsetof(AFX_MSGMAP_ENTRY, nID) == 8); ASSERT(offsetof(AFX_MSGMAP_ENTRY, nLastID) == 12); ASSERT(offsetof(AFX_MSGMAP_ENTRY, nSig) == 16); _asm { MOV EBX,lpEntry // 獲取消息映射入口地址,即第一條消息映射地址 MOV EAX,nMsg// 獲取當前消息 MOV EDX,nCode// 獲取當前消息Code MOV ECX,nID// 獲取當前消息ID號 __loop: CMP DWORD PTR [EBX+16],0 ; nSig (0 => end)<span style="white-space:pre"> </span> JZ __failed CMP EAX,DWORD PTR [EBX] ; nMessage// 判斷當前消息與當前消息映射表項的消息是否相等 JE __found_message<span style="white-space:pre"> </span>// 相等則跳至_found_message __next: ADD EBX,SIZE AFX_MSGMAP_ENTRY// 否則跳至下一條消息映射表項 JMP short __loop __found_message: CMP EDX,DWORD PTR [EBX+4] ; nCode// 判斷code號是否相等 JNE __next // message and code good so far // check the ID CMP ECX,DWORD PTR [EBX+8] ; nID<span style="white-space:pre"> </span> JB __next CMP ECX,DWORD PTR [EBX+12] ; nLastID JA __next // found a match MOV lpEntry,EBX ; return EBX// 找到對應的消息映射表項,結束 JMP short __end __failed: XOR EAX,EAX ; return NULL MOV lpEntry,EAX __end: } return lpEntry; #else // _AFX_PORTABLE // C version of search routine while (lpEntry->nSig != AfxSig_end) { if (lpEntry->nMessage == nMsg && lpEntry->nCode == nCode && nID >= lpEntry->nID && nID <= lpEntry->nLastID) { return lpEntry; } lpEntry++; } return NULL; // not found// 沒有發現消息表項,返回NULL #endif // _AFX_PORTABLE }
分析上面的匯編,我們發現它把當前消息與傳入的C++對象消息映射表做一一比較,當找到消息時,就返回當前AFX_MSGMAP_ENTRY 地址,進一步做處理,當找不到時,則返回NULL。所以OnWndMsg() 函數的作用就是從用戶定義的消息映射表中尋找是否有重寫的消息項,如果找到則返回TRUE,否則返回FALSE, 此時由 WindowProc() 來執行系統默認的消息處理函數 DefWindowProc(), 所以我們也可以在這個函數裡重寫一些消息響應函數。
總結
從上面的分析可以大致看出MFC的消息處理流程,即程序從GetMessage() 從消息隊列中取消息,然後用 PreTranslateMessage() 函數判斷是否發生用戶的消息重寫,如果重寫了,則取下一條消息,否則將消息交給WindowProc() 處理,WIndowProc() 首先在OnWndMsg() 判斷當前消息是否在當前對象的消息映射表中,如果存在,則執行用戶定義的該消息對應的響應函數,否則將消息交給 DefWindowProc() 處理,我們也可以重寫 DefWIndowProc() 從而實現消息的自定義處理。