也許題目有些誇張,但是Windows訊息方面確實存在一些不去探究就摸不著頭腦的事情,這種問題不是明顯錯誤,不會拋出異常,但卻是最棘手的問題,給調試帶來很大麻煩,所以我將實際遇到的問題整理如下,以供參考。
一、Windows 訊息以及訊息處理演算法Windows以訊息驅動的方式,使得線程能夠通過處理訊息來響應外界。Windows 為每個需要接受訊息和處理訊息的線程建立訊息佇列(包括髮送訊息佇列,登記訊息佇列,輸入訊息佇列,響應訊息佇列),其中發送訊息佇列儲存其他線程通過SendMessage發送給該線程建立視窗的訊息,登記訊息佇列儲存通過PostMessage發送給該線程或者該線程建立視窗的訊息,輸入訊息佇列儲存系統的輸入(包括鍵盤,滑鼠輸入),響應訊息佇列包含該線程調用SendMessage給指定視窗的視窗函數處理完後通知該線程的資訊。 Windows通過QS_SENDMESSAGE、QS_POSTMESSAGE、QS_QUIT、QS_INPUT、QS_PAINT、QS_TIMER表示是否有發送訊息、登記訊息、退出訊息、輸入訊息、重繪訊息、定時訊息。訊息的優先順序是QS_SENDMESSAGE > QS_POSTMESSAGE > QS_QUIT > QS_INPUT > QS_PAINT > QS_TIMER。 Windows處理訊息的方式大概是這樣的:訊息迴圈偽演算法:BOOL bRet = FALSE;MSG msg;while ((bRet = GetMessage(&msg, NULL, 0, 0))) { if (bRet == -1) break; // On Error exit the loop TranslateMessage(&msg); //轉換訊息 DispatchMessage(&msg); //發送訊息,其實就是調用指定視窗的視窗函數} GetMessage偽演算法如下:BOOL GetMessage(MSG *lpMsg, HWND hWnd , UINT wMsgFilterMin, UINT wMsgFilterMax) { //查看QS_SENDMESSAGE標誌,如果有的話迴圈處理,直到沒有訊息位置 DWORD dwRetVal = 0; ThreadInfo threadInfo; FLAG_SENDPROCLOOP: GetThreadInfo(GetCurrentThreadId(), &threadInfo); while (threadInfo.QS_SENDMESSAGE == QS_SIGNALSET) { //從發送訊息佇列中擷取訊息 dwReturnVal = GetMsgFromQueue(QUEUE_SEND, lpMsg, hWnd,wMsgFilterMin, wMsgFilterMax); //判斷是否取到訊息,有則調用視窗函數,無則複為QS_SENDMESSAGE標誌 If (dwReturnVal == GETMESSAGE_HASMESSAGE) { //調用指定視窗的視窗函數 CallWindowProc(hWnd, &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_PAINT if (threadInfo.QS_PAINT == QS_SIGNALSET) { //填充MSG,如果沒有視窗過程確認視窗,則複位QS_PAINT標誌 //... //返回TRUE threadInfo.QS_PAINT = QS_SIGNALRESET; return TRUE; } if (threadInfo.QS_TIMER == QS_SIGNALSET) { //填充MSG,如果沒有定時器報時,則複位QS_TIMER標誌 //... //返回TRUE return TRUE; } //等待有訊息到達 dwRetVal = MsgWaitForMultipleObjectsEx(...); if (...) goto FLAG_SENDPROCLOOP; //等待失敗 return FALSE;} 上面要注意的是各種訊息被處理的優先順序順序,在發送隊列中有發送訊息時,GetMessage不返回,直到將發送隊列中訊息處理完畢為止,然後複位QS_SENDMESSAGE,沒有發送訊息時,GetMessage才查看登記訊息,如果沒有登記訊息,則依著優先順序從高到低的順序依次處理各種訊息。 如果此過程中發現了優先順序低的訊息,則GetMessage填充一個MSG,然後返回。如果是QS_QUIT被置位,則GetMessage返回FALSE,否則返回TRUE。 當GetMessage返回FALSE時,訊息迴圈也就結束了。看訊息迴圈可知,當訊息迴圈再次調用GetMessage時,依然按照優先順序順序依次處理各種訊息。請注意SendMessage發送到目標線程訊息佇列的訊息在目標線程調用GetMessage時被處理掉,直到沒有發送訊息為止GetMessage才回去查詢其他訊息,如果有訊息GetMessage取到訊息返回,否則GetMessage使得線程陷入IDLE狀態,被掛起,當有訊息到達線程時GetMessage被喚醒,擷取訊息返回。二、Windows 訊息之WM_TIMERWM_TIMER訊息的優先順序最低,所以在有其他訊息的情況下,WM_TIMER訊息得不到處理,這也是我以前使用SetTimer註冊一個回呼函數,而回呼函數一直未被調用的原因。因為我在UI環境中使用,處理WM_PAINT訊息時又觸發了介面的重繪,導致了始終有WM_PAINT訊息要處理,WM_TIMER於是得不到處理的機會。處理WM_PAINT訊息時要小心,不然程式就可能消耗很高的cpu,並且使得低於WM_PAINT優先順序的WM_TIMER得不到處理。三、Windows 訊息相關函數之SendMessageTimeOutSendMessageTimeOut是發送訊息,在訊息被處理或者逾時的情況下會返回。但是查閱了MSDN和Windows核心編程,都沒有發現這個逾時值設為0時有什麼效果。直到最近一次在服務中對外廣播訊息,將此值設為0,服務啟動後在沒有將服務狀態設為RUNNING時調用SendMessageTimeOut對外廣播訊息,逾時值設為0,原本以為該函數會立刻返回,但是調用導致了線程的掛起。由於處理廣播訊息的另外線程一直在等待RUNNING狀態,而服務又等待外界處理完該訊息然後繼續,這就產生了一個死結。 這都是逾時值設定為0引起的後果。現在看來逾時值設為0就等同於調用SendMessage了。上面沒有分析線程訊息(即通過PostThreadMessage發送的訊息),關於Windows訊息更詳細的解釋,以及訊息處理機制請參考《Windows 核心編程第26章》