深入GetMessage和PeekMessage

來源:互聯網
上載者:User
 

深入GetMessage和PeekMessage

該文重點講述了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(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. 滑鼠事件的表單點擊測試

例如:如果圖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都會把訊息和事件從系統隊列中刪除。然而有些時候,某個應用程式可能需要掃描隊列中現存的訊息而並不刪除它們。例如,某個應用程式在做一些處理過程,這些處理過程期望“一但發現有可用的訊息,就儘快終止”。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.