现在的位置: 首页 > 技术文章 > 基础知识 > 正文

MFC 消息映射机制剖析

2014年07月13日 基础知识 ⁄ 共 10301字 ⁄ 字号 暂无评论 ⁄ 阅读 28 次

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() 从而实现消息的自定义处理。

给我留言

留言无头像?



×