Microsoft Windows的訊息迴圈

微軟視窗作業系統是以事件驅動做為程式設計的基礎。程式的執行緒會從作業系統獲取訊息。應用程式會不斷迴圈呼叫GetMessage函式：MSG msg; //用於儲存一條訊息BOOL bRet;//從UI執行緒訊息佇列中取出一條訊息while( (bRet = GetMessage( &amp;msg, NULL, 0, 0 )) != 0)if (bRet == -1)//錯誤處理程式碼，通常是直接退出程式elseTranslateMessage(&amp;msg); //按鍵訊息轉換為字元訊息DispatchMessage(&amp;msg); //分發訊息給相應的窗體雖然在程式上並沒有很嚴格的規定與要求，但是一般來說，它的事件迴圈通常會呼叫TranslateMessage函式與DispatchMessage函式，這兩個函式會傳遞訊息給回呼函式，以及呼叫相應視窗的訊息處理函式。現在的繪圖介面架構程式設計，例如Visual Basic與Qt基本上是不會要求應用程式直接擁有視窗程式的訊息迴圈，但是會以鍵盤與滑鼠的按鍵動作來作為事件的處理機制。在這些架構底下，訊息迴圈的痕跡還是可以被找到的。注意：在上述的原始碼裡，尤其在while迴圈"大於零"的條件。即使GetMessage函式的傳回值型態是英文字大寫的BOOL，但是在Win32視窗程式裡，它是被定義成int整數型態，它有兩個值，TRUE是整數的1，FALSE是整數的0。整數 -1代表error。歷史.16位Windows系統為非搶先單執行緒模式，應用程式沒有傳送訊息佇列，向視窗傳送一個訊息總是按同步方式執行，也即傳送程式要在接受訊息的視窗完全處理完訊息之後才能繼續執行。這通常是一個所期望的特性。但是，如果接收訊息的視窗花很長的時間來處理訊息或者出現掛起，則傳送程式就不能再執行。這意味著系統是不強壯的。如果應用程式訊息佇列為空，由於沒有虛擬輸入訊息佇列，SendMessage或PeekMessage函式訪問系統事件佇列查取可用的滑鼠或鍵盤輸入訊息。如果系統佇列中沒有需要處理的事件，SendMessage或PeekMessage函式掃描所有視窗以處理需要修改重繪的區域。如果沒有需要重繪的區域，則交出CPU控制權。恢復CPU控制權時，檢視是否有定時器過期。至此如果沒有訊息可返回，SendMessage進入睡眠，直至被輸入事件喚醒；PeekMessage如果沒有設定PM_NOYIELD標記，則會讓出CPU控制權，但不會讓執行緒休眠，重新獲得CPU後PeekMessage將控制權返回到執行緒，並返回一個空值指出這個執行緒沒有要處理的訊息了。本文主要關注Win32系統的訊息處理機制。背景.UI執行緒.Windows系統規定，視窗和鉤子則歸程序所有，程序結束時作業系統會自動刪除這些物件。建立視窗的執行緒必須就是處理視窗所有訊息的執行緒，即UI執行緒，不會為它建立訊息佇列。因此，呼叫PostThreadMessage前，這個執行緒必須是UI執行緒從而有投寄訊息的佇列，通常可在該執行緒中呼叫一次PeekMessage函式以達到這個目的。如果一個UI執行緒結束執行，作業系統會自動回收它所建立的所有窗體。窗體過程.窗體過程是一個函式，每個窗體有一個窗體過程，負責處理該窗體的所有訊息。UI控制元件也是獨立的“Window”，擁有自己的“窗體過程”。訊息佇列.Windows作業系統的核心空間中有一個系統訊息佇列，在核心空間中還為每個UI執行緒分配各自的執行緒訊息佇列(Thread message queue)。在發生輸入事件之後，Windows作業系統的輸入裝置驅動程式將輸入事件轉換為一個「訊息」投寄到系統訊息佇列；作業系統的一個專門執行緒從系統訊息佇列取出訊息，分發到各個UI執行緒的輸入訊息佇列中。每個UI執行緒的執行緒資訊塊TIB分配一個THREADINFO的結構，該結構包含一族成員變數，包括：應用程式的每個UI執行緒中有一段稱之為“訊息迴圈”的程式碼，透過GetMessage系統呼叫訪問系統空間中的對應的UI執行緒的訊息佇列，並依照下述次序處理：需要注意的是，GetMessage如果在應用程式訊息佇列未獲取訊息，則GetMessage呼叫不返回，該執行緒掛起，CPU使用權交給作業系統。即GetMessage為阻塞呼叫。由此可見，Windows的事件驅動模式，並不是作業系統把訊息主動分發給應用程式；而是由應用程式的每個UI執行緒透過“訊息迴圈”程式碼從UI執行緒訊息佇列獲取訊息。Windows訊息類別.鍵盤輸入時需要明確插入符位置，相關API函式為：CreateCaret、SetCaretPos、ShowCaret、HideCaret、DestroyCaret、GetCaretPos、GetCaretBlinkTime、SetCaretBlinkTime。3個獲得鍵狀態的函式：GetKeyState、GetAsyncKeyState、GetKeyboardState。對於自定義的控制元件，當單擊子視窗時，父視窗會得到焦點。但對於標準子視窗控制元件，單擊時會自動獲得焦點。如果一個子視窗擁有輸入焦點，滑鼠單擊另一個兄弟子視窗，則兄弟子視窗獲得輸入焦點。同步與阻塞.Windows API函式SendMessage是個同步呼叫，即它發出的Windows訊息沒被處理完之前這個函式就不返回。但這個函式不是阻塞的。分兩種情形：非同步傳送或投寄訊息的函式，如PostMessage、SendMessageCallback、SendNotifyMessage，訊息引數中不能使用指標，否則函式呼叫失敗。GetMessage偽演算法.BOOL GetMessage(MSG *lpMsg, HWND hWnd , UINT wMsgFilterMin, UINT wMsgFilterMax)//檢視QS_SENDMESSAGE標誌，如果有的話迴圈處理，直到沒有訊息位置DWORD dwRetVal = 0;ThreadInfo threadInfo;FLAG_SENDPROCLOOP:GetThreadInfo(GetCurrentThreadId(), &amp;threadInfo);while (threadInfo.QS_SENDMESSAGE == QS_SIGNALSET) {//從傳送訊息佇列中獲取訊息dwReturnVal = GetMsgFromQueue(QUEUE_SEND, lpMsg, hWnd,wMsgFilterMin, wMsgFilterMax);//判斷是否取到訊息,有則呼叫視窗函式，無則復為QS_SENDMESSAGE標誌If (dwReturnVal == GETMESSAGE_HASMESSAGE) {//呼叫指定視窗的視窗函式CallWindowProc(hWnd, &amp;threadInfo, lpMsg);else {QS_SENDMESSAGE = QS_SIGNALRESET;break;//在繼續處理之前再次檢查傳送訊息佇列if (threadInfo.QS_SENDMESSAGE == QS_SIGNALSET) goto FLAG_SENDPROCLOOP;//檢查傳送訊息佇列, 如果有訊息則取傳送訊息//判斷是否還有傳送訊息，沒有了則復位QS_POSTMESSAGE標誌if (threadInfo.QS_POSTMESSAGE == QS_SIGNALSET) {dwReturnVal = GetMsgFromQueue(QUEUE_POST, lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax);if (dwReturnVal == GETMESSAGE_LASTMESSAGE)threadInfo.QS_POSTMESSAGE = QS_SIGNALRESET;return TRUE;//如果退出標誌被置位if (threadInfo.QS_QUIT == QS_SIGNALSET) {threadInfo.QS_QUIT = QS_SIGNALRESET;FillMessage(lpMsg, MESSAGE_QUIT);return FALSE;//檢查輸入訊息佇列if (threadInfo.QS_INPUT == QS_SIGNALSET) {DWORD dwRetVal = GetMessageFromQueue(QUEUE_INPUT, lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax);//檢查是否有鍵盤，滑鼠訊息if (Test(dwRetVal, QS_KEY) == QS_LASTMOUSEKEYMESSAGE)threadInfo.QS_KEY = QS_SIGNALRESET;if (Test(dwRetVal, QS_MOUSEBUTTON) == QS_LASTMOUSEMESSAGE)threadInfo.QS_MOUSEBUTTON = QS_SIGNALRESET;return TRUE;//測試QS_PAINTif (threadInfo.QS_PAINT == QS_SIGNALSET) {//填充MSG，如果沒有視窗過程確認視窗，則復位QS_PAINT標誌//返回TRUEthreadInfo.QS_PAINT = QS_SIGNALRESET;return TRUE;if (threadInfo.QS_TIMER == QS_SIGNALSET) {//填充MSG，如果沒有定時器報時，則復位QS_TIMER標誌//返回TRUEreturn TRUE;//等待有訊息到達dwRetVal = MsgWaitForMultipleObjectsEx(...);if (...)goto FLAG_SENDPROCLOOP;//等待失敗return FALSE;