深入剖析MFC中Windows訊息處理機制
本人對Windows系統、MFC談不上有深入的瞭解,但對MFC本身封裝API的機制很有興趣,特別是讀了候老師的《深入淺出MFC》後,感覺到Visual C++的Application FrameWork十分精製。在以前,我對SDI結構處理訊息有一定的認識,但對於強制回應對話方塊的訊息機制不瞭解,讀了《深入》一書也沒能得到解決,近日,通過在網友的協助和查閱MSDN,自認為已經瞭解。一時興起,寫下這些文字,沒有其它目的,只是希望讓後來者少走彎路,也希望和我一樣喜歡“鑽牛角尖”的人共同討論、學習。如果你是牛人,那麼你現在要謹慎考慮有沒有充足的時間讀這些幼稚文字。
本文:
Windows程式和DOS程式的主要不同點之一是:Windows程式是以事件為驅動、訊息機製為基礎。如何理解?
舉了例子,當你CLICK Windows “開始”BUTTON時,為什麼就會彈出一個菜單呢?
當你單擊滑鼠左鍵時,作業系統中與MOUSE相關的驅動程式在第一時間內得到這個訊號[LBUTTONDOWN],然後它通知作業系統―――“嗨,滑鼠左鍵被單擊了!”,作業系統得到這一訊號後,馬上要判斷――使用者單擊滑鼠左鍵,這是針對哪個視窗呢?如何判斷?這很簡單!目前狀態中,具有焦點的視窗[或控制項]就是了[這裡當然是“開始”BUTTON了]。然後作業系統馬上向這個視窗發送一條訊息到這個視窗所在進程的訊息佇列,訊息內容應是訊息本身的代號、附加參數、視窗控制代碼…等等了。那麼,只有作業系統才有資格發送訊息至某一視窗的訊息佇列嗎?不然,其它程式也有資格。你可以在你的程式中調用:SendMessage 、PostMessage。這樣,被單擊的視窗得到了一條由作業系統發送的包含CLICK的訊息,作業系統已經暫時不再管視窗的任何事,因為它還要忙於處理其它事務。你的程式得到一條訊息後如何做呢?Windows對於你在“開始”BUTTON上的單擊事件做出如下反映:彈出一菜單。可是,得到訊息到做出反映這一過程是如何?的呢?這就是本文討論的主要內容[當然只是針對MFC了]。
我首先簡要談一下SDI,然後會花更多文字描述強制回應對話方塊。
對於SDI視窗,你的應用程式類的InitInstance()大約如下:
BOOL CEx06aApp::InitInstance() { …………… CSingleDocTemplate* pDocTemplate; pDocTemplate = new CSingleDocTemplate( IDR_MAINFRAME, RUNTIME_CLASS(CEx06aDoc), RUNTIME_CLASS(CMainFrame), // main SDI frame window RUNTIME_CLASS(CEx06aView)); AddDocTemplate(pDocTemplate); CCommandLineInfo cmdInfo; ParseCommandLine(cmdInfo); if (!ProcessShellCommand(cmdInfo)) return FALSE; m_pMainWnd->ShowWindow(SW_SHOW); m_pMainWnd->UpdateWindow(); return TRUE; } |
完成一些如動態產生相關文檔、視,顯示主架構視窗、處理參數行資訊等工作。這些都是顯示在你工程中的“明碼”。我們現在把斷點設定到return TRUE;一句,跟入MFC源碼中,看看到底MFC內部做了什麼。
程式進入SRC/WinMain.cpp,下一個大動作應是:
nReturnCode = pThread->Run(); |
注意了,重點來了。F11進入
int CWinApp::Run() { if (m_pMainWnd == NULL && AfxOleGetUserCtrl()) { // Not launched /Embedding or /Automation, but has no main window! TRACE0("Warning: m_pMainWnd is NULL in CWinApp::Run - quitting application./n"); AfxPostQuitMessage(0); } return CWinThread::Run(); } |
再次F11進入:
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)); } ASSERT(FALSE); // not reachable } BOOL CWinThread::IsIdleMessage(MSG* pMsg) { // Return FALSE if the message just dispatched should _not_ // cause OnIdle to be run. Messages which do not usually // affect the state of the user interface and happen very // often are checked for. // redundant WM_MOUSEMOVE and WM_NCMOUSEMOVE if (pMsg->message == WM_MOUSEMOVE || pMsg->message == WM_NCMOUSEMOVE) { // mouse move at same position as last mouse move? if (m_ptCursorLast == pMsg->pt && pMsg->message == m_nMsgLast) return FALSE; m_ptCursorLast = pMsg->pt; // remember for next time m_nMsgLast = pMsg->message; return TRUE; } // WM_PAINT and WM_SYSTIMER (caret blink) return pMsg->message != WM_PAINT && pMsg->message != 0x0118; } |
這是SDI處理訊息的中心機構,但請注意,它覺對不是核心!
分析一下,在無限迴圈FOR內部又出現一個WHILE迴圈
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 } |
這段代碼是當你程式進程的訊息佇列中沒有訊息時,會調用OnIdle做一些後備工作,臨時對象在這裡被刪除。當然它是虛函數。其中的PeekMessage,是查看訊息佇列,如果有訊息返回TRUE,如果沒有訊息返回FALSE,這裡指定PM_NOREMOVE,是指查看過後不移走訊息佇列中剛剛被查看到的訊息,也就是說這裡的PeekMessage只起到一個檢測作用,顯然返回FALSE時[即沒有訊息],才會進入迴圈內部,執行OnIdle,當然了,你的OnIdle返回FLASE,會讓程式不再執行OnIdle。你可能要問:
當bidle=0或訊息隊例中有訊息時,程式又執行到哪了呢?
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)); |
看啊,又進入一個迴圈!
其中有個重要的函數,PumpMessage,內容如下:
BOOL CWinThread::PumpMessage() { ASSERT_VALID(this); 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; } |
如你所想,這才是MFC訊息處理的核心基地[也是我個人認為的]。
GetMessage不同於PeekMessae,它是不得到訊息不罷體,PeekMessage如果發現訊息佇列中沒有訊息會返回0,而GetMessage如果發現沒有訊息,等,直到有了訊息,而且,GetMessage不同於PeekMessage,它會將訊息移走[當然,PeekMessage也可以做到這點]。我想當你讀了這個函數後,你應明白PreTranslateMessage函數的用法了吧[我比較喜歡在程式中充分利用這個函數]。
::TranslateMessage(&m_msgCur); ::DispatchMessage(&m_msgCur); |
將訊息發送到視窗的處理函數[它是由視窗類別指定的],之後的動作一直到你的程式做出反映的過程,你可以在《深入》一書中得到完美的解釋。我們還是通過reurn TRUE;回到CWinThread::Run()中的Do{}while;迴圈。然後還是對IDLE的處理,即便剛才你的ONIDLE返回了FALSE,在這裡你看到,你的程式還是有機會執行它的。然後又是利用PeekMessage檢測訊息佇列:
如果有訊息[這個訊息不被移動的原因是因為它要為PumpMessage內的GetMessage所利用。]再次進入PumpMessage[叫它“訊息泵”吧]。
如果沒有訊息,退出DO迴圈,但它還在FOR內部,所以又執行第一個While迴圈。
這是CwinThread::Run的一個執行過程。
不用擔心退不出for(;;)如果你的訊息佇列中有一條WM_QUIT,會使GetMessage返回0,然後PumpMessage返回0而RUN()內部:
if (!PumpMessage()) return ExitInstance(); |
SDI就說到這,下面我來談一下強制回應對話方塊。我分2種情況討論:
一當你的工程以強制回應對話方塊為基礎時[沒父視窗,或為案頭]。
與SDI不同處在於,在應用程式類的InItInstance內部:
BOOL CComboBoxApp::InitInstance() { AfxEnableControlContainer(); // Standard initialization // If you are not using these features and wish to reduce the size // of your final executable, you should remove from the following // the specific initialization routines you do not need. #ifdef _AFXDLL Enable3dControls(); // Call this when using MFC in a shared DLL #else Enable3dControlsStatic(); // Call this when linking to MFC statically #endif this->m_nCmdShow = SW_HIDE; CComboBoxDlg dlg; m_pMainWnd = &dlg; int nResponse = dlg.DoModal(); if (nResponse == IDOK) { // TODO: Place code here to handle when the dialog is // dismissed with OK } else if (nResponse == IDCANCEL) { // TODO: Place code here to handle when the dialog is // dismissed with Cancel } // Since the dialog has been closed, return FALSE so that we exit the // application, rather than start the application’s message pump. return FALSE; } |
int nResponse = dlg.DoModal();一句使你的整個程式都在DoModal()內部進行。而且,你退出DoMal()時[你一定結束了你的對話方塊],InitInstance返回的是False,我們知道,這樣,CwinThread::Run是不會執行的。
但對話方塊程式是在哪裡進行訊息處理的呢。
原來,dlg.DoModal()內部會調用CwinThread::RunModalLoop,它起到的作用和RUN()是一樣的[當然內部有細小差別,請參考MSDN]!!!
第二種情況,你是在SDI[或其它]程式中調用Dlg.DoModal() 產生了一強制回應對話方塊[有父視窗].
這又是如何運作的呢?
建了這樣一個工程做為例子。
SDI,在View中處理LBUTTONDOWN:
MyDLg內有按鈕,以憊後用.
沒有顯示模式對話方塊前,訊息處理一直在Cthread::Run()中進行.
你單擊後,程式執行點進入DoModal()內部的RunModalLoop,這又是一個訊息處理機制.
不過DoModal()中調用RunModalLoop,前會Disable掉它的父視窗.從RunModalLoop中出來後,再 Enable它.
強制回應對話方塊和非強制回應對話方塊都是通過調用CreateDialogIndirect()產生建立對話方塊.那它和非強制回應對話方塊區別是什麼造成的呢?
1 強制回應對話方塊將父視窗DISABLE掉.
我原以為被Disable的視窗是不接收訊息的.但後來我馬上發現我是錯的.但,為什麼你對被Disable的視窗進行KeyBorad,Mouse動作時,視窗沒反映呢,我想,這可能是作業系統從中搞的鬼.我在本文一開始,就寫出作業系統向視窗發送訊息的過程,我想當它發現目標視窗處理Disabled狀態時,不會將訊息發送給它,但這不能說視窗不接收訊息,其它程式[或它本身]發送給它的訊息還是可以接收並處理的.
2 強制回應對話方塊本身有訊息處理機制 RunModalLoop.
對以上兩點加以實驗.
我在我的剛才建的項目中的強制回應對話方塊中加上一個BUTTON,其中加入如下代碼:
OnButton1() { GetParaent()->EnableWindow(1); } |
單擊,後我們發現,此時它已經不再表現為”模態”,我試著點擊菜單,還是會作出正常反映.
我想這此訊息[對於父視窗的,如:菜單動作]的處理也應是在強制回應對話方塊中的RunModalLoop中進行處理的吧[這點我不能確定].