回爐重造之重讀Windows核心程式設計-026- 視窗訊息
26 視窗訊息
本章介紹Microsoft的訊息系統是如何支援帶有圖形介面的應用程式的。首先設計Win2K
以後的訊息系統時,有兩個主要目標:
- 儘可能保持與過去16位Windows相容,偏於移植現有的16位Windows程式。
- 使視窗系統強壯,一個執行緒不會對系統的其他執行緒產生不利的影響。
只不過往往事非所願。16位系統中,向視窗傳送一個訊息總是同步執行的,傳送訊息的程式要等接收訊息的程式完全處理訊息後才能繼續執行。但是如果接受訊息的視窗要花很長的時間來處理訊息,或者直接掛起了,那麼傳送程式就不能繼續執行。這是不強壯的。那隻能折衷一下了。
首先是一些基本原則。由於Windows允許一個程序最多建立10 000個不同型別的使用者物件(影象、游標、視窗類、選單、加速鍵等等),這些使用者物件歸建立這些物件的執行緒的程序
hook
) 這兩種使用者物件,就分別由建立視窗和安裝掛鉤的執行緒所擁有,如果執行緒結束,作業系統就會自動刪除視窗或解除安裝掛鉤。
所以,建立視窗的執行緒必須是為視窗處理所有訊息的執行緒
。這意味著:
- 如果執行緒建立了一個視窗,然後就結束了,那它不會收到
WM_DESTORY
或者WM_NCDESTROY
訊息。 - 每個執行緒,如果建立了至少一個視窗,都由系統對它分配一個訊息佇列,用於視窗訊息的派送。而為了接收這些訊息,執行緒又要有自己的訊息迴圈。
26.1 執行緒的訊息佇列
在成功建立程序之後,執行緒就有了執行的環境,並且每一個執行緒都相信自己是唯一的執行緒,有一個獨立的環境,用來維持自己的鍵盤焦點、視窗啟用、滑鼠捕獲等概念。
而執行緒被成功建立後,系統會假定執行緒不會使用者和使用者的任何任務,減少系統資源的耗費。但是一旦和圖形使用者介面關聯(檢查一個訊息佇列或者建立一個視窗),系統就會為執行緒分配一個重要的結構THREADINFO
。這個結構如下所示:
26.2 將訊息傳送到執行緒的訊息佇列中
當上面的操作完成,執行緒就有了自己的訊息佇列集合,程序中有幾個執行緒而且它們都呼叫CreateWindow
就有幾個訊息佇列集合。訊息被放置線上程的登記訊息佇列
中,這要通過呼叫PostMessage
函式來完成:
BOOL PostMessage( HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam);
訊息的接收視窗就用hwnd
控制代碼來標識。然後這個訊息就被放在一塊系統分配的記憶體,新增到執行緒的登記訊息佇列中。並且函式還給訊息設定QS_POSTMESSGEAGE
喚醒位,一旦被登記立即返回,不過也因為這樣呼叫這個函式的執行緒就沒有辦法知道訊息何時會被處理,甚至會不會被處理。
可用PostThreadMessage
函式可以將訊息放置線上程的登記訊息佇列中。
BOOL PostThreadMessage(
DWORD dwThreadId,LPARAM lParam);
可用GetWindowsThreadProcessId
函式確定是哪個執行緒建立了一個視窗。
DWORD GetWindowsThreadProcessId(
HWND hwnd,PDWORD pdwProcessId);
這個函式返回執行緒的ID,這個執行緒建立了hwnd
引數標識的視窗。如果傳遞引數pdwProcessId
還可以獲取擁有執行緒的程序的ID,也可以不傳遞。
PostThreadMessage
接收訊息的執行緒由第一個引數標記。當訊息被設定到佇列中後,MSG結構的hwnd
。如果程式中需要執行一些特殊的處理就要呼叫這個函式。
在主訊息迴圈中處理訊息時,要檢查hwnd
是否為NULL, 並進程MSG結構的msg程式來執行特別的處理,如果沒有訊息被處理就呼叫DispatchMessage
。再之後再迴圈處理下一個訊息。
PostThreadMessage
和PostMessage
一樣,都是登記了訊息就返回。
向執行緒傳送訊息的還有PostQuitMessage
可以終止執行緒的訊息迴圈,但是它並不實際登記一個訊息到任何一個THREADINFO
結構的佇列,只是在內部設定QS_QUIT
喚醒標誌和結構體裡的nExitCode
成員。這個函式不會失敗,所以返回值是VOID。
26.3 向視窗傳送訊息
使用SendMessage
函式可以把視窗訊息直接發給一個視窗過程:
LRESULT SendMessage(
HWND hwnd,LPARAM wParam);
只有當訊息被處理之後,SendMessage
才能夠返回呼叫它的程式,呼叫這行程式碼的執行緒在執行下一行程式碼前就知道這個資訊已經被處理了,並得到一個來自目標視窗過程的返回值。
當訊息是傳送到另一個執行緒的視窗過程的時候更復雜,因為傳送訊息的執行緒並非執行在接收訊息的程序的地址空間中,只能掛起等待,由系統執行以下的動作。
- 傳送的訊息被追加到接收執行緒的傳送訊息佇列,併為接收執行緒設定
QS_SENDMESSGE
標誌,並處於idle
狀態,等待一個訊息出現在它的應答訊息佇列中。- 如果接收訊息的執行緒沒有處理訊息的過程(如呼叫
GetMessage
),傳送的訊息不會被處理,系統不會中斷執行緒來立即處理訊息。 - 如果執行緒在等待訊息,就掃描
QS_SENDMESSGE
標誌:- 如果是,系統掃描傳送訊息佇列中的列表,並找到第一個資訊,用合適的視窗過程處理訊息。
- 如果沒有傳送訊息佇列中沒有訊息了,
QS_SENDMESSGE
標誌則被關閉。
- 如果接收訊息的執行緒沒有處理訊息的過程(如呼叫
- 當傳送的訊息被處理後,視窗過程的返回值被登記到傳送執行緒的應答訊息佇列,傳送執行緒被喚醒,並處理這個返回值,傳送執行緒迴歸正常執行。
當然,如果傳送執行緒即便被掛起,也是可以執行一些任務的,如果它也有視窗過程,等待處理訊息的話。
使用SendMessage
會造成執行緒掛起,這是有可能引起bug的。如果處理訊息的過程含有錯誤進入了死迴圈,那麼傳送訊息的執行緒就有可能永遠掛起了。這個時候,就要使用SendMessageTimeout
、SendMessageCallback
、SendNotifyTimeout
和ReplyMessage
就可以防止這種情況。
LRESULT SendMessageTimeout(
HWND hwnd,LPARAM lParam,UINT fuFlags,UINT uTimeout,PDWORD_PTR pdwResult);
- 對於
fuFlags
引數,可以是以下的值:SMTO_NORMAL
如果不想使用以下的標誌,就使用這個。SMTO_ABORTIFHUNG
,告訴函式去檢視接收訊息的執行緒是否處理掛起狀態,是就立即返回。SMTO_NOTIMEOUTIFNOTHUNG
使得接收訊息的執行緒不考慮等待時間的限定值。SMTO_BLOCK
使得執行緒在函式返回前不處理任何收到的資訊。- 前面說過執行緒即使因為呼叫了
SendMessage
而掛起也是有可能處理其他的任務的,現在可以使用SMTO_BLOCK
遮蔽這種可能。當然也會產生死鎖,知道timeout指定的時間期限結束。
uTimeout
等待應答時間的毫秒數。pdwResult
儲存返回值。- 這個函式的返回值應該是
BOOL
而不是LRESULTE
型別,這會引起一些問題。- 如果發生錯誤,返回值是FALSE,而
GetLastError
為0(ERROR_SUCCESS)。 - 如果對引數傳遞了一個無效的控制代碼,
GetLastError
為1400(ERROR_INVALID_WINDOW_HANDLE)。
- 如果發生錯誤,返回值是FALSE,而
BOOL SendMessageCallback(
HWND hwnd,SENDASYNCPROC pfnResultCallback,ULONG_PTR dwData);
接收執行緒得到的訊息放在傳送訊息佇列,傳送執行緒就可以立即返回。當訊息的處理完成時,一個訊息被登記到傳送執行緒的應答訊息佇列中,系統也通過呼叫一個函式將這個應答傳送給執行緒,函式的原型如下:
VOID CALLBACK ResultCallback(
HWND hwnd,ULONG dwData,LRESULT pdwResult);
這個函式的地址就當作SendMessageCallback
的引數。這個函式的第一個引數是視窗的控制代碼,第二個引數是訊息,第三個訊息dwData
是SendMessageCallback
函式中的dwData
引數。最後一個引數pdwResult
處理訊息的視窗過程返回的結果。
要注意回撥函式的時機並不是在SendMessageCallback
函式返回後就執行,而是先把訊息登記到一個傳送執行緒的應答訊息佇列。傳送執行緒呼叫處理訊息的函式時,訊息從應答訊息佇列中取出,並執行ResultCallback
函式。
SendMessageCallback
還可以實現廣播的效果。它通過向每一個重疊(overlapped)視窗廣播一個訊息,並檢視每一個結果。對每個處理訊息的返回結果都要呼叫ResultCallback
函式。
如果SendMessageCallback
把訊息傳送給一個有視窗的執行緒,系統立即呼叫視窗過程,並在訊息被處理後呼叫ResultCallback
函式。ResultCallback
執行完之後,SendMessageCallback
之後的程式碼才開始執行。(變回同步了?)
BOOL SendNotifyMessage(
HWND hwnd,LPARAM lParam);
函式將一個訊息至於接收執行緒的傳送訊息佇列中,並立即返回到呼叫執行緒,這一點和PostMessage一樣
但也有不同:
SendNotifyMessage
是向其他執行緒建立的視窗傳送資訊,傳送的訊息比起接收執行緒訊息佇列中的登記訊息有更高的優先權。- 如果向一個建立了視窗的執行緒傳送訊息,
SendNotifyMessage
在訊息處理完後才能返回。
訊息的目的是通知,是讓接收方知道某個狀態已經發生變化,而在這之前有做某些處理的機會。
第四個用於向執行緒傳送訊息的函式是ReplyMessage
:
BOOL ReplyMessage(LRESULTE lResult);
這個函式略微的不同,是為了接收視窗訊息。當這個函式被呼叫的時候,執行緒是想讓系統知道它已經完成了足夠的工作,結果應該包裝起來並登記到傳送執行緒的應答訊息佇列中。強迫傳送執行緒獲得結果,恢復執行。
唯一的引數指出訊息處理的結果。在呼叫ReplyMessage
後,傳送訊息的執行緒恢復執行,而處理訊息的執行緒繼續處理訊息,兩個執行緒都不會被掛起,可以正常地執行。
這裡唯一的問題是,在ReplyMessage
函式前面介紹的三個函式都不適合用來實現一些保護性的程式碼,而是推薦使用ReplyMessage
。另外如果在傳送訊息的執行緒在呼叫這個函式,它其實什麼也不做。如果你在處理執行緒間的訊息的時候呼叫了ReplyMessage
,則它返回TRUE;如果你在處理執行緒內的資訊傳送時呼叫了ReplyMessage
,它返回FALSE。如果你想要知道是前者還是後者,可以呼叫InMessage
。這個函式在處理執行緒間傳送的訊息時,返回TRUE;而執行緒處理執行緒內傳送的或登記的訊息時,返回FALSE。另外ReplyMessageEx
也可以做同樣的事,只是唯一的引數要填NULL。 ReplyMessageEx
的返回值是DWORD
,代表正在處理的訊息的型別,如果是0(ISMEX_NOSEND
),表示處理的訊息是執行緒內傳送或者登記的訊息;否則就是ISMEX_SEND
、ISMEX_NOTIFY
、ISMEX_CALLBACK
、ISMEX_REPLIED
的組合。
ISMEX_SEND
:處理執行緒間的訊息,使用SendMessage
或者SendMessageTimeout
傳送,如果沒有ISMEX_REPLIED
標誌,傳送執行緒被阻塞,等待應答。ISMEX_NOTIFY
:處理執行緒間的訊息,使用SendNotifyMessage
傳送,傳送執行緒不阻塞,也不應答。ISMEX_CALLBACK
:處理執行緒間的訊息,使用SendMessageCallback
傳送,傳送執行緒不阻塞,也不應答。ISMEX_REPLIED
:處理執行緒間的訊息,已經呼叫ReplyMessage
,傳送執行緒不阻塞。
26.4 喚醒一個執行緒
當一個執行緒呼叫GetMessage
或WaitMessage
,但沒有對這個執行緒或者這個執行緒建立的視窗的訊息時,系統可以掛起這個執行緒,這樣系統就不再給它分配CPU時間。當一個訊息被登記或傳送到這個執行緒,系統要設定一個喚醒標誌,指出要給這個執行緒分配CPU時間以處理訊息。
26.4.1 佇列狀態標誌
當一個執行緒正在執行,可以通過GetQueueStatus
函式查詢佇列的狀態:
DWORD GetQueueStatus(UINT fuFlags);
fuFlags
引數是由一個或以上的標誌連線起來,可用來測試特定的喚醒位,可以用OR連線,連線得越少查詢得越快。返回值的高字(HIWORD
)是訊息的型別,低字(LOWWORD
)。
記憶體映像檔案 共享 wParam
或lParam
表示一個指向資料結構的指標的時候
WM_GETTEXT
是兩個訊息,先搞到長度,再複製內容
如果是WM_USER+X類的訊息又如何?
用特殊的視窗WM_COPYDATA
訊息 COPYDATASTRUCT
結構體
dwData
指定位元組數 lpData
是要傳送的內容的第一個位元組
CopyData
示例程式。