首先,應該清楚MFC的消息循環(::GetMessage,::PeekMessage),消息泵(CWinThread::PumpMessage)和MFC的消息在窗口之間的路由是兩件不同的事情。在MFC的應用程序中(應用程序類基于CWinThread繼承),必須要有一個消息循環,他的作用是從應用程序的消息隊列中讀取消息,并把它派送出去(::DispatchMessage)。而消息路由是指消息派送出去之后,系統(USER32.DLL)把消息投遞到哪個窗口,以及以后消息在窗口之間的傳遞是怎樣的。
消息分為隊列消息(進入線程的消息隊列)和非隊列消息(不進入線程的消息隊列)。對于隊列消息,最常見的是鼠標和鍵盤觸發的消息,例如WM_MOUSERMOVE,WM_CHAR等消息;還有例如:WM_PAINT、WM_TIMER和WM_QUIT。當鼠標、鍵盤事件被觸發后,相應的鼠標或鍵盤驅動程序就會把這些事件轉換成相應的消息,然后輸送到系統消息隊列,由Windows系統負責把消息加入到相應線程的消息隊列中,于是就有了消息循環(從消息隊列中讀取并派送消息)。還有一種是非隊列消息,他繞過系統隊列和消息隊列,直接將消息發送到窗口過程。例如,當用戶激活一個窗口系統發送WM_ACTIVATE, WM_SETFOCUS, and WM_SETCURSOR。創建窗口時發送WM_CREATE消息。在后面你將看到,MS這么設計是很有道理的,以及他的整套實現機制。
這里講述MFC的消息循環,消息泵。先看看程序啟動時,怎么進入消息循環的:
_tWinMain ->AfxWinMain ->AfxWinInit ->CWinThread::InitApplication ->CWinThread::InitInstance ->CWinThread::Run
非對話框程序的消息循環的事情都從這CWinThread的一Run開始...
第一部分:非對話框程序的消息循環機制。
//thrdcore.cpp
// main running routine until thread exits
int CWinThread::Run()
{
ASSERT_VALID(this);
// 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(&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))
{
bIdle = TRUE;
lIdleCount = 0;
}
} while (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE));
} //無限循環,退出條件是收到WM_QUIT消息。
ASSERT(FALSE); // not reachable
}
這是一個無限循環,他的退出條件是收到WM_QUIT消息:
if (!PumpMessage())
return ExitInstance();
在PumpMessage中,如果收到WM_QUIT消息,那么返回FALSE,所以ExitInstance()函數執行,跳出循環,返回程序的退出代碼。所以,一個程序要退出,只用在代碼中調用函數
VOID PostQuitMessage( int nExitCode )。指定退出代碼nExitCode就可以退出程序。
下面討論一下這個函數Run的流程,分兩步:
1,第一個內循環phase1。bIdle代表程序是否空閑。他的意思就是,如果程序是空閑并且消息隊列中沒有要處理的消息,那么調用虛函數OnIdle進行空閑處理。在這個處理中將更新UI界面(比如工具欄按鈕的enable和disable狀態),刪除臨時對象(比如用FromHandle得到的對象指針。由于這個原因,在函數之間傳遞由FromHandle得到的對象指針是不安全的,因為他沒有持久性)。OnIdle是可以重載的,你可以重載他并返回TRUE使消息循環繼續處于空閑狀態。
NOTE:MS用臨時對象是出于效率上的考慮,使內存有效利用,并能夠在空閑時自動撤銷資源。關于由句柄轉換成對象,可以有若干種方法。一般是先申明一個對象obj,然后使用obj.Attatch來和一個句柄綁定。這樣產生的對象是永久的,你必須用obj.Detach來釋放對象。
2,第二個內循環phase2。在這個循環內先啟動消息泵(PumpMessage),如果不是WM_QUIT消息,消息泵將消息發送出去(::DispatchMessage)。消息的目的地是消息結構中的hwnd字段所對應的窗口。
//thrdcore.cpp
BOOL CWinThread::PumpMessage()
{
ASSERT_VALID(this);
//如果是WM_QUIT就退出函數(return FALSE),這將導致程序結束.
if (!::GetMessage(&m_msgCur, NULL, NULL, NULL)) {
#ifdef _DEBUG
if (afxTraceFlags & traceAppMsg)
TRACE0("CWinThread::PumpMessage - Received WM_QUIT.\n");
m_nDisablePumpCount++; // application must die
// Note: prevents calling message loop things in 'ExitInstance'
// will never be decremented
#endif
return FALSE;
}
#ifdef _DEBUG
if (m_nDisablePumpCount != 0)
{
TRACE0("Error: CWinThread::PumpMessage called when not permitted.\n");
ASSERT(FALSE);
}
#endif
#ifdef _DEBUG
if (afxTraceFlags & traceAppMsg)
_AfxTraceMsg(_T("PumpMessage"), &m_msgCur);
#endif
// process this message
if (m_msgCur.message != WM_KICKIDLE && !PreTranslateMessage(&m_msgCur))
{
::TranslateMessage(&m_msgCur); //鍵轉換
::DispatchMessage(&m_msgCur); //派送消息
}
return TRUE;
}
?
在這一步有一個特別重要的函數大家一定認識:PreTranslateMessage。這個函數在::DispatchMessage發送消息到窗口之前,進行對消息的預處理。PreTranslateMessage函數是CWinThread的成員函數,大家重載的時候都是在View類或者主窗口類中,那么,它是怎么進入別的類的呢?代碼如下:
//thrdcore.cpp
BOOL CWinThread::PreTranslateMessage(MSG* pMsg)
{
ASSERT_VALID(this);
// 如果是線程消息,那么將會調用線程消息的處理函數
if (pMsg->hwnd == NULL && 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
}
由上面這個函數可以看出:
第一,如果(pMsg->hwnd == NULL),說明這是一個線程消息。調用CWinThread::DispatchThreadMessageEx到消息映射表找到消息入口,然后調用消息處理函數。
NOTE: 一般用PostThreadMessage函數發送線程之間的消息,他和窗口消息不同,需要指定線程id,消息激被系統放入到目標線程的消息隊列中;用ON_THREAD_MESSAGE( message, memberFxn )宏可以映射線程消息和他的處理函數。這個宏必須在應用程序類(從CWinThread繼承)中,因為只有應用程序類才處理線程消息。如果你在別的類(比如視圖類)中用這個宏,線程消息的消息處理函數將得不到線程消息。
第二,消息的目標窗口的PreTranslateMessage函數首先得到消息處理權,如果函數返回FALSE,那么他的父窗口將得到消息的處理權,直到主窗口;如果函數返回TRUE(表示消息已經被處理了),那么就不需要調用父類的PreTranslateMessage函數。這樣,保證了消息的目標窗口以及他的父窗口都可以有機會調用PreTranslateMessage--在消息發送到窗口之前進行預處理(如果自己處理完然后返回FALSE的話 -_-b),如果你想要消息不傳遞給父類進行處理的話,返回TRUE就行了。
第三,如果消息的目標窗口和主窗口沒有父子關系,那么再調用主窗口的PreTranslateMessage函數。為什么這樣?由第二步知道,一個窗口的父窗口不是主窗口的話,盡管它的PreTranslateMessage返回FALSE,主窗口也沒有機會調用PreTranslateMessage函數。我們知道,加速鍵的轉換一般在框架窗口的PreTranslateMessage函數中。
我找遍了MFC中關于加速鍵轉換的處理,只有CFrameWnd,CMDIFrameWnd,CMDIChildWnd等窗口類有。所以,第三步的意思是,如果消息的目標窗口(他的父窗口不是主窗口,比如一個這樣的非模式對話框)使消息的預處理繼續漫游的話(他的PreTranslateMessage返回FALSE),那么給一次機會給主窗口調用PreTranslateMessage(萬一他是某個加速鍵消息呢?),這樣能夠保證在有非模式對話框的情況下還能保證主窗口的加速鍵好使。
http://dev.csdn.net/article/51/51474.shtm