GetMessage和PeekMessage的內部機制 2010-12-15 19:18:02| 分類: C/C++/VC程式設計 | 標籤: |字號大中小 訂閱 .
譯者的話
該文重點講述了Windows處理事件、訊息的具體過程和步驟。尤其是在繫系處理滑鼠鍵盤事件的過程上做了詳解。通過這篇文章,你將對Windows的訊息處理機制有一個較全面的瞭解。
概念
這篇文章解釋了GetMessage和PeekMessage的內部運作方式,同時也是一類與“訊息及訊息在16位 MS-DOS®/Microsoft® Windows™環境之下的影響”相關文章的基礎。我們將討論下面這些主題:
?系統和應用程式佇列(譯者注:以下簡稱為“程式佇列”)
?GetMessage和PeekMessage函式
?訊息過濾
?WM_QUIT訊息
?讓步和休眠
?讓步的問題
?WaitMessage
16位MS-DOS/Windows環境和32位Win32™/Windows NT™環境有些很重要的不同之處。雖然這些不同之處在這兒無法被忽視,但我們還是把它們做為遺留問題,由以後的文章去解釋吧。
佇列
要理解GetMessage和PeekMessage的運作,必須首先明白Microsoft® Windows™作業系統是如何儲存事件和訊息的。在Windows中有兩種型別的佇列為此目的工作,它們分別是系統佇列和訊息佇列。
硬體輸入:系統佇列
Windows有一些驅動程式,它們負責響應來自於鍵盤和滑鼠等硬體的中斷服務。在中斷時間中,鍵盤和滑鼠驅動程式會呼叫USER.EXE中指定的一些入口點去報告一個事件的發生。在Windows中服務於光筆計算的光筆驅動程式,同樣會在原始的光筆事件中呼叫這些入口點。
在Windows3.1中,系統佇列是一個有著120個入口空間的定長的佇列。在一般情形下這些“小房間”是足夠了,但如果應用程式掛起了或者在一段長的時間裡沒有及時處理任何訊息就可能導致系統佇列被填滿。如果真的發生了,任何嘗試新增到系統佇列的新事件都將會引起系統蜂鳴。(譯者注:在DOS中,如果一個程式在一段時間內佔用了所有的系統資源,使機器無法響應,這時如果你按住一個鍵不放,你就會聽到機箱喇叭嘀嘀作響)
傳送的訊息和程式佇列
當一個應用程式開始時,一個佇列將會因此而被建立。程式佇列(有時會稱為任務佇列)常常用於儲存“正在被髮往應用程式的一個視窗” 的訊息。唯一常駐程式佇列的訊息是那些由PostMessage或PostAppMessage明確傳送的訊息。(SendMessage從不使用系統佇列)PostQuitMessage函式不會傳送一個訊息到程式佇列。(WM_QUIT訊息將在下文中論討)
預設的,每個程式佇列可以保持八個訊息。一般情況下這是相當足夠的,因為PostMessage極少被使用。但是如果一個應用程式試圖強制呼叫很多的PostMessage到某個應用程式時,那麼這類應用程式將會用使用SetMessageQueue函式來增加訊息佇列的長度。你必須小心的使用SetMessageQueue函式,因為它無論何時都會先刪掉當前的程式佇列,並建立一個預期大小的新佇列,此時任何在舊佇列中的訊息都會被銷燬。因此,它必須在你的WinMain例程中在所有其它的應用程式程式設計介面(API)之前呼叫或在應用程式用PeekMessage明確的清除佇列之後呼叫。
GetMessage和PeekMessage是怎樣工作的
在Windows的內部,GetMessage和PeekMessage執行著相同的程式碼。而兩者最大的不同之處則體現在沒有任何訊息返回到應用程式的情況下。在此種情況下,PeekMessage會返回一個空值到應用程式,GetMessage會在此時讓應用程式休眠。在它們之間還有一些其它的不同,我們將會在下面討論,但它們相當次要。
GetMessage和PeekMessage邏輯
下面一步步的講述了在Windows3.1版的GetMessage和PeekMessage公用程式碼。
提示:下面所示步驟按照訊息型別的優先權進行排序。舉個例子,傳送的訊息總在鍵盤和滑鼠訊息之前被返回,而鍵盤和滑鼠的訊息又會在繪圖(paint)訊息之前反回,以此類推。
1. 檢視在為“活動中任務”服務的程式佇列中是否有訊息的存在。如果是,首先在隊首刪除此訊息並將其返回到應用程式。然後,應用程式中的GetMessage和PeekMessage會呼叫一些程式碼,用以從程式佇列中接收此訊息,這些程式碼是由該應用程式呼叫的動態連結庫(DLL)生成的。記住,只有由PostMessage傳送的訊息會常駐於此佇列中。
2. 與所有訊息和窗體控制程式碼過濾器進行對照,核查此訊息。如果此訊息不匹配指定的過濾器,就會把此訊息留在程式佇列中。如果佇列中在此訊息的後面還有其它訊息,則會轉向對下一個訊息的處理。
3. 如果在程式佇列中沒有訊息了,就掃描系統佇列中的事件。這個過程相當複雜,並且我們將在下面的“掃描系統佇列”小節中XX。一般來講,在系統佇列首部的事件是供這個應用程式所使用的,系統會將其轉化為訊息,並將訊息返回到這個應用程式中(它不會首先被置於應用佇列中)。注意,這個掃描系統佇列的過程可能導致當前活動的應用程式將控制權讓給其它的應用程式。
4. 如果在系統佇列中沒有等待處理的事件,則核查所有與當前應用程式(任務)相關的窗體以確定更新區域。當一個窗體的一部分需要被重繪時,一個更新區域就被建立在那個窗體部分之上。這個區域將與此窗體中現存的所有更新區域相結合,並儲存在內部窗體結構體中。如果GetMessage或PeekMessage在這個任務中發現某些窗體有一些未處理的更新區域,將產生一個WM_PAINT訊息,併為那個窗體返回到應用程式中。WM_PAINT從不駐留在任何佇列中。此時,一個應用程式將為某個窗體不斷的接收WM_PATIN訊息,直到更新區域由BeginPaint/EndPaint,ValidateRect,或ValidateRgn所清除。
5. 如果這個任務中沒有任何窗體需要被更新,GetMessage和PeekMessage就會在這一點讓出控制權,除非PeekMessage呼叫被設定為PM_NOYIELD屬性。
6. 當讓步返回時,檢視在當前任務中是否有計時器到期。如果是,建立一個WM_TIMER訊息並返回。它不但發生在“返回一個WM_TIMER訊息到窗體”的計時器上,同樣也發生在“呼叫一個計時器處理過程”的計時器上。如要了解更多資訊,請看在微軟開發者網路(MSDN)光碟(包括技術文章、Windows文章、核心和驅動程式文章)中的文章“Timers and Timing in Microsoft Windows”(譯者注:如果讀者能夠認可我的工作,我會不遺餘力地翻譯這篇關於計時器的文章)。
7. 如果這個應用程式沒有計時器事件服務,並且一個應用程式正在被終止,程式碼將嘗試去縮小圖形裝置介面(GDI)的本地記憶體堆。一些應用程式,比如繪圖應用程式(像Paintbrush™),為GDI分配了大量的堆記憶體。當應用程式終止時釋放這些物件時,會使GDI本地記憶體堆被空閒空間填滿而膨脹。為了恢復這些空閒的空間, 在GetMessage/PeekMessage處理中,LocalShrink將在這一點被呼叫於GDI的記憶體堆。這個被完成一次,(每次)一個應用程式將終止。
8. 在這一時刻,程式碼將分叉為兩條路,一是程式碼任意的返回一個有效的訊息,另一個是完全沒有這個應用程式去處理的訊息、事件,而程式碼最終會走哪條路決定於PeekMessage和GetMessage中的哪一個被呼叫。
PeekMessage.
如果PeekMessage被呼叫,並設定了PM_NOYIELD標記,PeekMessage在此刻返回一個空值,這個空返回值指出已經沒有要處理的訊息了。如果沒有設定PM_NOYIELD標記,PeekMessage就在此刻讓出控制權。它不會休眠,但會單一的交給其它已準備好的應用程式一個執行的機會。(請參閱下面的“讓步與休眠的不同)當讓步返回,PeekMessage直接將控制權返回到應用程式,並返回一個空值,它指出這個應用程式沒有要處理的訊息了。
GetMessage.
在此刻,GetMessage會讓應用程式休眠、等待,直到一些事件發生需要喚醒應用程式。控制權不會返回到呼叫GetMessage的應用程式,直到有應用程式必須去處理的訊息出現。一旦這個應用程式從被置入休眠狀態中醍來,GetMessage內部的迴圈將回到最開始(步驟1)。
WH_GETMESSAGE鉤子
在GetMessage和PeekMessage將一個訊息返回到呼叫的應用程式之前,會做一個驗證是否存在一個WH_GETMESSAGE鉤子的測試。如果有一個已經被安裝了,那這個鉤子會被呼叫。如果PeekMessage沒有發現可用的訊息並返回一個空值時,這個鉤子將不會被呼叫。在鉤子處理過程中,你不可能得知是到底是GetMessage被呼叫還是PeekMessage被呼叫。
掃描系統佇列
綜上所述,在系統佇列中的事件僅僅是硬體事件的記錄。那些程式碼掃描系統佇列的主要任務是,從這些事件中建立訊息,並確定哪一個窗體將接收這個訊息。
程式碼第一次在系統佇列首部找到事件時,並不會馬上將其刪除。因為滑鼠和鍵盤事件只是佇列中的兩種事件,而程式碼會分枝(譯者注:類似於C語言中的switch語句)並單獨處理每一種型別的事件。
處理系統佇列中的滑鼠事件
下面是處理滑鼠事件的步驟。
1. 首先,將計算該事件螢幕座標的相應窗體。此計算(呼叫窗體點選測試)以桌面窗體開始,從頭至尾的掃描細統中的每一個窗體(包括子窗體),直到找到一個包含這個滑鼠座標點的窗體,並且這個窗體沒有任何同樣包含這個座標點的子窗體。
例如:如果圖2中的箭頭代表當前的滑鼠位置,任何的滑鼠行為,像單擊滑鼠鍵,將生成一個會在B窗體中產生訊息的事件。
2. 如果一個窗體使用SetCapture捕獲滑鼠,那麼“系統佇列掃描”程式碼將通過普通的點選測試,並將所有的滑鼠訊息返回到捕獲的窗體。例如:如果在圖2 中的A窗體呼叫了SetCapture,則在箭頭所指位置的所有滑鼠行為,將產生窗體A中的訊息,而不是窗體B。
3. 如果這個被處理的事件是一個“滑鼠鍵按下”事件(任何一個滑鼠鍵),程式碼會檢測這個事件是否會轉化為雙擊事件。你可以在微軟開發者網路(譯者注:MSDN)CD(技術文章,Ask Dr. GUI)中的“Ask Dr. GUI #5”中找到關於雙擊轉化的描述。實質上,如果在兩次滑鼠鍵按下事件中,時間和距離的增量在允許的範圍之中,該事件將會生成一個雙擊訊息,否則它將生成一個標準的“按下”事件。所有的滑鼠事件都將生成標準的滑鼠訊息,而雙擊測試只在滑鼠事件指定的,包含CS_DBLCLKS型別的窗體中進行。
4. 一個訊息從滑鼠事件中構造出來。
5. 如果滑鼠點選測試確定該事件發生在一個窗體的非客戶區,如邊框或標題欄,那麼該構造出的訊息對映到它相應的非客戶區訊息中。例如:一個WM_MOUSEMOVE事件會被映謝為WM_NCMOUSEMOVE訊息。
6. 與所有指定的訊息過濾器進行對照,核查此訊息。(請參閱下面的“訊息範圍過濾和窗體控制程式碼過濾”)如果該訊息不匹配過濾器,則重新從頭開始“系統佇列掃描”程式碼,檢視佇列中的下一個訊息。
7. 如果滑鼠訊息需要前往與當前任務不同的另一個任務的相關窗體,事件會被留在系統佇列中,並且如果那個將會處理這個訊息的任務在休眠之中,會被喚醒。這個新近被喚醒的任務不會在此刻立即執行,只會標記為準備執行。如果訊息前往了其它任務,並且在系統佇列中沒有要處理的事件被發現,“系統佇列掃描”會程式碼返回到GetMessage/PeekMessage主程式碼。請參閱下面的“讓步與休眠的不同”以獲得更多的資訊。
8. 如果安裝了滑鼠鉤子,它將在此刻被呼叫。如果滑鼠鉤子返回了一個非零值,那麼該滑鼠事件被忽略,並從系統佇列中被刪除,然後重新從頭開始“系統佇列掃描”程式碼。如果鉤子返回零,則繼續處理。
9. 如果訊息是一個“滑鼠鍵按下”訊息,“系統佇列掃描”則會在返回此訊息之前,按照下面的方法啟用窗體。
它沿著父鏈一直向上尋找該窗體的“最終頂層父窗體”,直到相遇。
它用SendMessage向該窗體的“最終頂層父窗體”傳送一個WM_MOUSEACTIVATE訊息。
從WM_MOUSEACTVATE返回的值將在下面被測試:
a) 如果返回的值為空、MA_ACTIVATE或者MA_ACTIVATEANDEAT,ActivateWindow函式將被呼叫去啟用那個“最終頂層父窗體”。
b) 如果返回的值是MA_NOACTIVATE或者MA_NOACTIVATEANDEAT,窗體則不被啟用。
注意:MA_ACTIVATEANDEAT和MA_NOACTIVATEANDEAT會導致“滑鼠鍵按下”事件從系統佇列中被刪除,而不會生成一個滑鼠按下訊息。
c) 最終,一個WM_SETCURSOR訊息被髮送到窗體,充許窗體設定指標的輪廓。
10. 如果滑鼠鉤子被呼叫,並且當前的滑鼠事件從系統佇列中被刪除了,則檢查“基於計算機訓練”(CBT)的鉤子。如果安裝有有一個CBT鉤子,將會攜帶HCBT_CLICKSKIPPED鉤子碼代呼叫它。
11. 按鍵狀態表包含了三個用於跟蹤滑鼠按鍵狀態的入口。這些按鍵被分配予虛擬鍵程式碼(VK_LBUTTON,VK_RUTTON和VC_MBUTTON),它們和GetKeyState一起始用去確事實上滑鼠鍵是彈起還是按下。在返回滑鼠訊息之前,“系統佇列掃描”程式碼會(為彈起或按下訊息)設定按鍵狀態表並且從系統佇列中刪除訊息。如果PeekMessage被呼叫時攜帶PM_NOREMOVE,則按鍵狀態表不會被修改。
處理系統佇列中的鍵盤事件
1. 檢查是否Ctrl鍵被按下和當前的事件是否ESC鍵按。如果是,該使用者――直接窗體――會顯示工作管理員窗體。一個WM_SYSCOMMAND訊息將被髮送到啟用的窗體,並且引數wParam為SC_TASKLIT。然後鍵盤按下事件從系統佇列中被刪除,“系統佇列掃描”程式碼又將重新從頭開始。如果此啟用的窗體是一個系統模組或者是一個被顯示出來的“硬”系統模組訊息框(比如一個“INT”24小時系統錯誤訊息框,或一個使用MB_ICONHAND和MB_SYSTEMMODAL引數的MessageBox函式)的事件,將會被拋棄。
2. 下一步,試著去檢視當前的事件是不是一個Print Screen鍵的按下事件。如果是,任意一個啟用的窗體或整個桌面將被做為一個點陣圖快照,儲存到剪貼簿中。如果Alt鍵被按下,一幅啟用窗體的影像被複制到剪貼簿中;如果沒有,則是整個桌面被複制。然後Print Screen鍵按下事件從系統佇列中被刪除,“系統佇列掃描”程式碼又將重新從頭開始。如果顯示了一個“硬”系統模組訊息框,則此操作被忽略。
3. 下一步檢測熱鍵。使用程式管理器,使用者可以定義用來執行一個應用程式的擊鍵事件。這些擊鍵被稱為熱鍵。如果當前的事件是一個按鍵事件,將會被測試是否與定義過的熱鍵匹配。如果發現匹配,一個WM_SYSCOMMAND訊息將被髮送到啟用的窗體,並且引數wParam為SC_HOTKEY。然後鍵盤按下事件從系統佇列中被刪除,“系統佇列掃描”程式碼又將重新從頭開始。如果此啟用的窗體是一個系統模組或者是一個被顯示出來的“硬”系統模組訊息框,該測試被跳過。
4. 一般情況下,所有的鍵盤訊息(如WM_KEYDOWN、WM_CHAR等等)前往具有輸入焦點的窗體。如果這個具有輸入焦點的窗體與另一個當前執行的任務相關聯,那麼該事件會被留在系統佇列中,並且那個擁有“有焦點的窗體”的任務會被喚醒(如果休眠了)。“系統佇列掃描”程式碼會像沒要發現任何要處理的事件一樣,返回到主GetMessage/PeekMessage程式碼。請參閱下面的“讓步與休眠的不同”和“應用程式如何被喚醒”以獲得更多的資訊。
5. 如果遇到了沒有任何一個窗體具有輸入焦點的情形,鍵盤訊息會直接前往當前啟用的窗體,而不會被翻譯成為系統鍵訊息(如WM_SYSKEYDOW,WM_SYSCHAR,等等)。
6. 與所有指定的訊息過濾器進行對照,核查此訊息。(請參閱下面的“訊息範圍過濾和窗體控制程式碼過濾”)如果該訊息不匹配過濾器,則重新從頭開始“系統佇列掃描”程式碼,檢視佇列中的下一個訊息。
7. 如果事件被返到了當前的任務,它將從系統佇列中被刪除掉,除非PeekMessage被指定為PM_NOREMOVE標記。請參閱下面的“PeekMessage的PM_NOREMOVE標記”以瞭解更多的關於不從佇列中刪除事件的資訊。
8. 如果安裝有鍵盤鉤子,將在此刻被呼叫。如果事件從系統佇列中被刪除了,鉤子的呼叫將伴隨HC_ACTION屬性;如果事件未被從系統佇列中刪除,鉤子的呼叫將具有HC_NOREM屬性。
9. 如果鍵盤鉤子被呼叫,並且當前的按鍵事件從系統佇列中被刪除了,則檢查現存的CBT鉤子。如果安裝有CBT鉤子,將呼叫它並攜帶HCBT_KEYSKIPPED鉤子碼。
10. 最後,訊息被返加到主GetMessage/PeekMessage程式碼。
PeekMessage與PM_NOREMOVE
預設情況下,每一個訊息被返回到應用程式後,PeekMessage和 GetMessage都會把訊息和事件從系統佇列中刪除。然而有些時候,某個應用程式可能需要掃描佇列中現存的訊息而並不刪除它們。例如,某個應用程式在做一些處理過程,這些處理過程期望“一但發現有可用的訊息,就儘快終止”。
深入探討MFC訊息迴圈和訊息泵
首先,應該清楚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(萬一他是某個加速鍵訊息呢?),這樣能夠保證在有非模式對話方塊的情況下還能保證主視窗的加速鍵好使。
我做了一個小例子,在對話方塊類的PreTranslateMessage中,返回FALSE。在主視窗顯示這個非模式對話方塊,在對話方塊擁有焦點的時候,仍然能夠啟用主視窗的快捷鍵。
總之,整個框架就是讓每個訊息的目標視窗(包括他的父視窗)都有機會參與訊息到來之前的處理。呵呵~
至此,非對話方塊的訊息迴圈和訊息泵的機制就差不多了。這個機制在一個無限迴圈中,不斷地從訊息佇列中獲取訊息,並且保證了程式的執行緒訊息能夠得到機會處理,視窗訊息在預處理之後被髮送到相應的視窗處理過程。那麼,還有一點疑問,為什麼要一會兒呼叫::PeekMessage,一會兒呼叫:: GetMessage呢,他們有什麼區別?
NOTE:一般來說,GetMessage被設計用來高效地從訊息佇列獲取訊息。如果佇列中沒有訊息,那麼函式GetMessage將導致執行緒休眠(讓出CPU時間)。而PeekMessage是判斷訊息佇列中如果沒有訊息,它馬上返回0,不會導致執行緒處於睡眠狀態。
在上面的phase1第一個內迴圈中用到了PeekMessage,它的引數PM_NOREMOVE表示並不從訊息佇列中移走訊息,而是一個檢測查詢,如果訊息佇列中沒有訊息他立刻返回0,如果這時執行緒空閒的話將會引起訊息迴圈呼叫OnIdle處理過程(上面講到了這個函式的重要性)。如果將:: PeekMessage改成::GetMessage(***),那麼如果訊息佇列中沒有訊息,執行緒將休眠,直到執行緒下一次獲得CPU時間並且有訊息出現才可能繼續執行,這樣,訊息迴圈的空閒時間沒有得到應用,OnIdle也將得不到執行。這就是為什麼既要用::PeekMessage(查詢),又要用::GetMessage(做實際的工作)的緣故。
第二部分: 對話方塊程式的訊息迴圈機制
基於對話方塊的MFC工程和上面的訊息迴圈機制不一樣。實際上MFC的對話方塊工程程式就是模式對話方塊。他和上面講到的非對話方塊程式的不同之處,主要在於應用程式物件的InitInstance()不一樣。
//dlg_5Dlg.cpp
BOOL CDlg_5App::InitInstance()
{
AfxEnableControlContainer();
#ifdef _AFXDLL
Enable3dControls(); // Call this when using MFC in a shared DLL
#else
Enable3dControlsStatic(); // Call this when linking to MFC statically
#endif
CDlg_5Dlg 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;
}
NOTE: InitInstance函式返回FALSE,由最上面程式啟動流程可以看出,CWinThread::Run是不會得到執行的。也就是說,上面第一部分說的訊息迴圈在對話方塊中是不能執行的。實際上,對話方塊也有訊息迴圈,她的訊息迴圈在CDialog::DoModal()虛擬函式中的一個 RunModalLoop函式中。
這個函式的實現體在CWnd類中:
int CWnd::RunModalLoop(DWORD dwFlags)
{
ASSERT(::IsWindow(m_hWnd)); // window must be created
ASSERT(!(m_nFlags & WF_MODALLOOP)); // window must not already be in modal state
// for tracking the idle time state
BOOL bIdle = TRUE;
LONG lIdleCount = 0;
BOOL bShowIdle = (dwFlags & MLF_SHOWONIDLE) && !(GetStyle() & WS_VISIBLE);
HWND hWndParent = ::GetParent(m_hWnd);
m_nFlags |= (WF_MODALLOOP|WF_CONTINUEMODAL);
MSG* pMsg = &AfxGetThread()->m_msgCur;
// acquire and dispatch messages until the modal state is done
for (;;)
{
ASSERT(ContinueModal());
// phase1: check to see if we can do idle work
while (bIdle &&
!::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE))
{
ASSERT(ContinueModal());
// show the dialog when the message queue goes idle
if (bShowIdle)
{
ShowWindow(SW_SHOWNORMAL);
UpdateWindow();
bShowIdle = FALSE;
}
// call OnIdle while in bIdle state
if (!(dwFlags & MLF_NOIDLEMSG) && hWndParent != NULL && lIdleCount == 0)
{
// send WM_ENTERIDLE to the parent
::SendMessage(hWndParent, WM_ENTERIDLE, MSGF_DIALOGBOX, (LPARAM)m_hWnd);
}
if ((dwFlags & MLF_NOKICKIDLE) ||
!SendMessage(WM_KICKIDLE, MSGF_DIALOGBOX, lIdleCount++))
{
// stop idle processing next time
bIdle = FALSE;
}
}
// phase2: pump messages while available
do
{
ASSERT(ContinueModal());
// pump message, but quit on WM_QUIT
//PumpMessage(訊息泵)的實現和上面講的差不多。都是派送訊息到視窗。
if (!AfxGetThread()->PumpMessage())
{
AfxPostQuitMessage(0);
return -1;
}
// show the window when certain special messages rec'd
if (bShowIdle &&
(pMsg->message == 0x118 || pMsg->message == WM_SYSKEYDOWN))
{
ShowWindow(SW_SHOWNORMAL);
UpdateWindow();
bShowIdle = FALSE;
}
if (!ContinueModal())
goto ExitModal;
// reset "no idle" state after pumping "normal" message
if (AfxGetThread()->IsIdleMessage(pMsg))
{
bIdle = TRUE;
lIdleCount = 0;
}
} while (::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE));
} //無限迴圈
ExitModal:
m_nFlags &= ~(WF_MODALLOOP|WF_CONTINUEMODAL);
return m_nModalResult;
}
先說說怎麼退出這個無限迴圈,在程式碼中:
if (!ContinueModal())
goto ExitModal;
決定是否退出迴圈,訊息迴圈函式返回也就是快要結束結束程式了。
BOOL CWnd::ContinueModal()
{
return m_nFlags & WF_CONTINUEMODAL;
}
NOTE: CWnd::ContinueModal()函式檢查對話方塊是否繼續模式。返回TRUE,表示現在是模式的;返回FALSE,表示對話方塊已經不是模式(將要結束)。
如 果要結束對話方塊,在內部最終會呼叫函式CWnd::EndModalLoop,它取消m_nFlags的模式標誌(訊息迴圈中的 ContinueModal函式將返回FALSE,訊息迴圈將結束,程式將退出);然後激發訊息迴圈讀取訊息。也就是說,結束模式對話方塊是一個標誌,改變這個標誌就可以了。他的程式碼是:
//wincore.cpp
void CWnd::EndModalLoop(int nResult)
{
ASSERT(::IsWindow(m_hWnd));
// this result will be returned from CWnd::RunModalLoop
m_nModalResult = nResult;
// make sure a message goes through to exit the modal loop
if (m_nFlags & WF_CONTINUEMODAL)
{
m_nFlags &= ~WF_CONTINUEMODAL;
PostMessage(WM_NULL);
}
}
NOTE: PostMessage(NULL)是有用的。如果訊息佇列中沒有訊息的話,可能訊息迴圈中的ContinueModal()不會馬上執行,傳送一個空訊息是激發訊息迴圈馬上工作。
下面說一下CWnd::RunModalLoop函式中的訊息迴圈究竟幹了些什麼事情:
1, 第一個內迴圈。首先從訊息佇列中查詢訊息,如果對話方塊空閒,而且訊息佇列中沒有訊息,他做三件事情,大家應到都能從字面上明白什麼意思。最重要的是傳送 WM_KICKIDLE訊息。為什麼呢?第一部分講到了,非對話方塊程式用OnIdle來更新使用者介面(UI),比如工具欄,狀態列。那麼,如果對話方塊中也有工具欄和狀態列呢,在哪裡更新(網上有很多這樣的程式)?可以處理WM_KICKIDLE訊息:
LRESULT CDlg_5Dlg::OnKickIdle(WPARAM w,LPARAM l)
{
//呼叫CWnd::UpdateDialogControls更新使用者介面
UpdateDialogControls(this, TRUE);
return 0;
}
NOTE: CWnd::UpdateDialog函式傳送CN_UPDATE_COMMAND_UI訊息給所有的使用者介面對話方塊控制元件。
2, 第二個內迴圈。最重要的還是PumpMessage派送訊息到目標視窗。其他的,像第二個if語句,0x118訊息好像是WM_SYSTIMER訊息(系統用來通知游標跳動的一個訊息)。也就是說,如果訊息為WM_SYSTIMER或者WM_SYSKEYDOWN,並且空閒顯示標誌為真的話,就顯示視窗並通知視窗立刻重繪。
總之,對話方塊的訊息迴圈機制和非對話方塊(比如SDI,MDI)還是類似的,僅僅側重點不同。模式對話方塊是模式顯示,自然有他的特點。下面部分討論一下模式對話方塊和非模式對話方塊的區別。因為模式對話方塊有自己的特殊訊息迴圈;而非模式對話方塊,共用程式的訊息迴圈,和普通的視窗已經沒有什麼大的區別了。
第三部分:模式對話方塊和非模式對話方塊的區別
這個話題已經有很多人討論,我說說我所理解的意思。
在MFC 框架中,一個對話方塊物件DoModal一下就能產生一個模式對話方塊,Create一下就能產生一個非模式對話方塊。實際上,無論是模式對話方塊還是非模式對話方塊,在MFC內部都是呼叫::CreateDialogIndirect(***)函式來建立非模式對話方塊。只是模式對話方塊作了更多的工作,包括使父視窗無效,然後進入自己的訊息迴圈等等。::CreateDialogIndirect(***)函式最終呼叫CreateWindowEx函式通知系統建立窗體並返回控制程式碼,他內部沒有實現自己的訊息迴圈。
非模式對話方塊建立之後立即返回,並且和主程式共用一個訊息迴圈。非模式對話方塊要等對話方塊結束之後才返回,自己有訊息迴圈。比如下面的程式碼:
CMyDlg* pdlg = new CMyDlg;
pdlg ->Create(IDD_DIALOG1);
pdlg->ShowWindow(SW_SHOW);
MessageBox("abc");
非模式對話方塊和訊息框MessageBox幾乎是同時彈出來。而如果將Create改成DoModal,那麼,只能彈出模式對話方塊,在關閉了對話方塊之後(模式對話方塊自己的訊息迴圈結束),訊息框才彈出來。
NOTE:可以在模式對話方塊中呼叫GetParent()->EnableWindow(true);這樣,主視窗的選單,工具欄又啟用了,能用了。MFC使用非模式對話方塊來模擬模式對話方塊,而在win32 SDK程式中,模式對話方塊激發他的父視窗Enable操作是沒有效果的。
關於訊息迴圈總結:
1,我們站在一個什麼高度看訊息迴圈?訊息迴圈其實沒有什麼深奧的道理。如果一個郵遞員要不斷在一個城市中送信,我們要求他做什麼?要求他來回跑,但他一次只能在一個地方出現。如果我們的應用程式只有一個執行緒的話,我們要他不斷地為視窗傳遞訊息,我們怎麼做?在一個迴圈中不斷的檢測訊息,並將他傳送到適當的視窗。視窗可以有很多個,但訊息迴圈只有一個,而且每時每刻最多隻有一個地方在執行程式碼。為什麼? 看第二點。
2,因為是單執行緒的(程式程式啟動的時候,只有而且有一個執行緒,我們稱他為主執行緒),所以就像郵遞員一樣,每次只能在某一個地方幹活。什麼意思呢?舉個例子,用:: DiapatchMessage派送訊息,在視窗處理過程(WinProc,視窗函式)返回之前,他是阻塞的,不會立即返回,也就是訊息迴圈此時不能再從訊息佇列中讀取訊息,直到::DispatchMessage返回。如果你在視窗函式中執行一個死迴圈操作,就算你用PostQuitMessage函式退出,程式也會down掉。
while(1)
{
PostQuitMessage(0); //程式照樣down.
}
所以,當視窗函式處理沒有返回的時候,訊息迴圈是不會從訊息佇列中讀取訊息的。這也是為什麼在模式對話方塊中要自己用無限迴圈來繼續訊息迴圈,因為這個無限迴圈阻塞了原來的訊息迴圈,所以,在這個無限迴圈中要用GetMessage,PeekMessage,DispatchMessage來從訊息佇列中讀取訊息並派送訊息了。要不然程式就不會響應了,這不是我們所希望的。
所以說,訊息迴圈放在程式的什麼的地方都基本上是過的去的,比如放在DLL裡面。但是,最好在任何時候,只有一個訊息迴圈在工作(其他的都被阻塞了)。然後,我們要作好的一件事情,就是怎麼從訊息迴圈中退出!當然用WM_QUIT 是可以拉~(PostThreadMessage也是個好主意),這個訊息迴圈退出後,可能程式退出,也可能會啟用另外一個被阻塞的訊息迴圈,程式繼續執行。這要看你怎麼想,怎麼去做。最後一個訊息迴圈結束的時候,也許就是程式快結束的時候,因為主執行緒的執行程式碼也快要完了(除非BT的再作個死迴圈)。
NOTE: 讓windows系統知道建立一個執行緒的唯一方法是呼叫API CreatThread函式(__beginthreadex之類的都要在內部呼叫他建立新執行緒)。好像windows核心程式設計說,在win2000下,系統用CreateRemoteThread函式來建立執行緒,CreateThread在內部呼叫CreateRemoteThread。不過這不是爭論的焦點,至少win98下CreateRemoteThread並不能正常工作,還是CreateThread主持大局。
3,在整個訊息迴圈的機制中,還必須談到視窗函式的可重入性。什麼意思?就是視窗函式(他是個回撥函式)的程式碼什麼時候都可以被系統(呼叫者一般是user32模組)呼叫。比如在視窗過程中,向自己的視窗SendMessage(***);那麼執行過程是怎樣的?
我們知道,SendMessage是要等到訊息傳送並被目標視窗執行完之後才返回的。那麼視窗在處理訊息,然後又等待剛才傳送到本視窗的訊息被處理後之後(SendMessage返回)才繼續往下執行,程式不就互相死鎖了嗎?
其實是不會的。windows設計一套適合SendMessage的演算法,他判斷如果傳送的訊息是屬於本執行緒建立的視窗的,那麼直接由user32模組呼叫視窗函式(可能就有視窗重入),並將訊息的處理結果結果返回。這樣做體現了視窗重入。上面的例子,我們呼叫SendMessage(***)傳送訊息到本視窗,那麼視窗過程再次被呼叫,處理完訊息之後將結果返回,然後SendMessage之後的程式接著執行。對於非佇列訊息,如果沒有視窗重入,不知道會是什麼樣子。
NOTE: 由於視窗的可重入性。在win32 SDK程式中應儘量少用全域性變數和靜態變數,因為在視窗函式執行過程中可能視窗重入,如果重入後將這些變數改了,但你的程式在視窗重入返回之後繼續執行,可能就是使用已經改變的全域性或靜態變數。在MFC中(所有視窗的視窗函式基本上都是AfxWndProc),按照類的思想進行了組織,一般變數都是類中的,好管理的多。
4,MFC中視窗類(比如C**View,CFrameWnd等)中的MessageBox函式,以及 AfxMessageBox函式都是阻塞原有的訊息迴圈的。由訊息框內部的一個訊息迴圈來從訊息佇列中讀取訊息,並派送訊息(和模式對話方塊類似)。實際上,這些訊息函式最終呼叫的是::MessageBox,它在訊息框內部實現了一個訊息迴圈(原有的主程式訊息迴圈被阻塞了)。論壇中碰到過幾次關於計時器和訊息框的問題,看下面的程式碼:
void CTest_recalclayoutView::OnTimer(UINT nIDEvent)
{
// TODO: Add your message handler code here and/or call default
MessageBox("abc");
while(1); //設計一個死迴圈
CView::OnTimer(nIDEvent);
}
我們讓OnTimer大約5秒鐘彈出一個訊息框。那麼,訊息框不斷的被彈出來,只要訊息框不被關閉,那麼程式就不會進入死迴圈。實際上,每次彈出對話方塊,都是最上層的那個訊息框掌握著訊息迴圈,其他的訊息迴圈被阻塞了。只要不關閉最上面的訊息框,while(1);就得不到執行。如果點了關閉,程式就進入了死迴圈,只能用ctrl+alt+del來解決問題了。
5,訊息迴圈在很多地方都有應用。比如應用線上程池中。一個執行緒的執行週期一般線上程函式返回之後結束,那麼怎麼延長執行緒的生命週期呢?一種方法就是按照訊息迴圈的思想,線上程中加入訊息迴圈,不斷地從執行緒佇列讀取訊息,並處理訊息,執行緒的生命週期就保持著直到這個訊息迴圈的退出。
NOTE:只要執行緒有介面元素或者呼叫GetMessage,或者有執行緒訊息傳送過來,系統就會為執行緒建立一個訊息佇列。
6, 在單執行緒程式中,如果要執行一個長時間的複雜操作而且介面要有相應的話,可以考慮用自己的訊息泵。比如,可以將一個阻塞等待操作放在一個迴圈中,並將超時值設定得比較小,然後每個等待的片段中用訊息泵繼續訊息迴圈,使介面能夠響應使用者操作。等等之類,都可以應用訊息泵(呼叫一個類似這樣的函式):
BOOL CChildView::PeekAndPump()
{
MSG msg;
while(::PeekMessage(&msg,NULL,0,0,PM_NOREMOVE))
{
if(!AfxGetApp()->PumpMessage())
{
::PostQuitMessage(0);
return false;
}
}
return true;
}
其實,用多執行緒也能解決複雜運算時的介面問題,但是沒有這麼方便,而且一般要加入執行緒通訊和同步,考慮的事情更多一點。
綜上所述,MFC訊息迴圈就那麼回事,主要思想還是和SDK中差不多。這種思想主要的特點表現在迎合MFC整個框架上,為整個框架服務,為應用和功能服務。這是我的理解
|