天天看點

Windows核心程式設計 第26章 視窗消 息

窗 口 消 息

允許一個程序至多建立10 000個不同類型的使用者對象(User object):圖符、光标、視窗類、菜單、加速鍵表等等。當一個線程調用一個函數來建立某個對象時,則該對象就歸這個線程的程序所擁有。這樣,當程序結束時,如果沒有明确删除這個對象,則作業系統會自動删除這個對象。對視窗和挂鈎( h o o k )這兩種U s e r對象,它們分别由建立視窗和安裝挂鈎的線程所擁有。如果一個線程建立一個視窗或安裝一個挂鈎,然後線程結束,作業系統會自動删除視窗或解除安裝挂鈎。

26.1 線程的消息隊列

T H R E A D I N F O結構,并将這個資料結構與線程聯系起來。

這個T H R E A D I N F O結構包含一組成員變量,利用這組成員,線程可以認為它是在自己獨占的環境中運作。T H R E A D I N F O是一個内部的、未公開的資料結構,用來指定線程的登記消息隊列(posted-message queue)、發送消息隊列( send-message queue)、應答消息隊列( r e p l y -message queue)、虛拟輸入隊列(virtualized-input queue)、喚醒标志 (wake flag)、以及用來描述線程局部輸入狀态的若幹變量。

Windows核心程式設計 第26章 視窗消 息

26.2 将消息發送到線程的消息隊列中

(1)BOOL WINAPI PostMessageW(

    _In_opt_ HWND hWnd,

    _In_ UINT Msg,

    _In_ WPARAM wParam,

    _In_ LPARAM lParam);.

h w n d參數辨別的視窗。然後系統配置設定一塊記憶體,将這個消息參數存儲在這塊記憶體中,并将這塊記憶體增加到相應線程的登記消息隊列中。并且,這個函數還設定 Q S _ P O S T M E S S A G E喚醒位(後面會簡單讨論)。函數P o s t M e s s a g e在登記了消息之後立即傳回,調用該函數的線程不知道登記的消息是否被指定視窗的視窗過程所處理。實際上,有可能這個指定的視窗永遠不會收到登記的消息。如果建立這個特定視窗的線程在處理完它的消息隊列中的所有消息之前就結束了,就會發生這種事。

還可以通過調用P o s t T h r e a d M e s s a g e将消息放置線上程的登記消息隊列中。

(2)BOOL WINAPI PostThreadMessageW(

    _In_ DWORD idThread,

    _In_ UINT Msg,

    _In_ WPARAM wParam,

    _In_ LPARAM lParam);

函數所期望的線程由第一個參數d w T h r e a d I d所标記。當消息被設定到隊列中時,M S G結構的h w n d成員将設定成N U L L。當程式要在主消息循環中執行一些特殊處理時要調用這個函數。

要對線程編寫主消息循環以便在 G e t M e s s a g e或P e e k M e s s a g e取出一個消息時,主消息循環代碼檢查h w n d是否為N U L L,并檢查M S G結構的m s g成員來執行特殊的處理。如果線程确定了該消息不被指派給一個視窗,則不調用D i s p a t c h M e s s a g e,消息循環繼續取下一個消息。

P o s t M e s s a g e函數一樣,P o s t T h r e a d M e s s a g e在向線程的隊列登記了消息之後就立即傳回。調用該函數的線程不知道消息是否被處理。

向線程的隊列發送消息的函數還有P o s t Q u i t M e s s a g e:

(3)VOID PostQuitMessage(int nExitCode);

P o s t Q u i t M e s s a g e類似于調用:

PostThreadMessage(GetCurrentThreadid(),WM_QUIT ,nExitCode ,0);

但是,P o s t Q u i t M e s s a g e并不實際登記一個消息到任何一個 T H R E A D I N F O結構的隊列。隻是在内部,P o s t Q u i t M e s s a g e設定Q S_Q U I T喚醒标志(後面将要讨論),并設定T H R E A D I N F O結構的n E x i t C o d e成員。因為這些操作永遠不會失敗,是以 P o s t Q u i t M e s s a g e的原型被定義成傳回V O I D。

G e t Wi n d o w s T h r e a d P r o c e s s I d來确定是哪個線程建立了一個視窗。

    DWORD GetWindowThreadProcessId(HWND hwnd ,PDWORD pdwProcessId);

I D ,這個線程建立了h w n d參數所辨別的視窗。線程I D 在全系統範圍内是唯一的。還可以通過對p d w P r o c e s s I d參數傳遞一個D W O R D 位址來擷取擁有該線程的程序I D ,這個程序I D 在全系統範圍内也是唯一的。通常,我們不需要程序I D ,隻須對這個參數傳遞一個N U L L。

26.3 向視窗發送消息

S e n d M e s s a g e函數可以将視窗消息直接發送給一個視窗過程:

LRESULT WINAPI SendMessageW(

    _In_ HWND hWnd,

    _In_ UINT Msg,

    _Pre_maybenull_ _Post_valid_ WPARAM wParam,

    _Pre_maybenull_ _Post_valid_ LPARAM lParam);

S e n d M e s s a g e才能傳回到調用程式。由于具有這種同步特性,比之P o s t M e s s a g e或P o s t T h r e a d M e s s a g e,S e n d M e s s a g e用得更頻繁。調用這個函數的線程在下一行代碼執行之前就知道視窗消息已經被完全處理。

是如何工作的呢?如果調用S e n d M e s s a g e的線程向該線程所建立的一個視窗發送一個消息,S e n d M e s s a g e就很簡單:它隻是調用指定視窗的視窗過程,将其作為一個子例程。當視窗過程完成對消息的處理時,它向 S e n d M e s s a g e傳回一個值。S e n d M e s s a g e再将這個值傳回給調用線程。

S e n d M e s s a g e的内部工作就複雜得多(即使兩個線程在同一程序中也是如此)。Wi n d o w s要求建立視窗的線程處理視窗的消息。是以當一個線程用S e n d M e s s a g e向一個由其他程序所建立的視窗發送一個消息,也就是向其他的線程發送消息,發送線程不可能處理視窗消息,因為發送線程不是運作在接收程序的位址空間中,是以不能通路相應視窗過程的代碼和資料。實際上,發送線程要挂起,而由另外的線程處理消息。是以為了向其他線程建立的視窗發送一個視窗消息,系統必須執行下面将讨論的動作。

Q S _ S E N D M E S S A G E标志(後面将讨論)。其次,如果接收線程已經在執行代碼并且沒有等待消息(如調用G e t M e s s a g e、P e e k M e s s a g e或Wa i t M e s s a g e),發送的消息不會被處理,系統不能中斷線程來立即處理消息。當接收程序在等待消息時,系統首先檢查 Q S _ S E N D M E S S A G E喚醒标志是否被設定,如果是,系統掃描發送消息隊列中消息的清單,并找到第一個發送的消息。有可能在這個隊列中有幾個發送的消息。例如,幾個線程可以同時向一個視窗分别發送消息。當發生這樣的事時,系統隻是将這些消息追加到接收線程的發送消息隊列中。

Q S _ S E N D M E S S A G E喚醒标志被關閉。當接收線程處理消息的時候,調用 S e n d M e s s a g e的線程被設定成空閑狀态(i d l e),等待一個消息出現在它的應答消息隊列中。在發送的消息處理之後,視窗過程的傳回值被登記到發送線程的應答消息隊列中。發送線程現在被喚醒,取出包含在應答消息隊列中的傳回值。這個傳回值就是調用S e n d M e s s a g e的傳回值。這時,發送線程繼續正常執行。

S e n d M e s s a g e傳回時,它基本上是處于空閑狀态。但它可以執行一個任務:如果系統中另外一個線程向一個視窗發送消息,這個視窗是由這個等待 S e n d M e s s a g e傳回的線程所建立的,則系統要立即處理發送的消息。在這種情況下,系統不必等待線程去調用G e t M e s s a g e、Peek Message或Wa i t M e s s a g e。

Wi n d o w s使用上述方法處理線程之間發送的消息,是以有可能造成線程挂起( h a n g)。例如,當處理發送消息的線程含有錯誤時,會導緻進入死循環。那麼對于調用 S e n d M e s s a g e的線程會發生什麼事呢?它會恢複執行嗎?這是否意味着一個程式中的 b u g會導緻另一個程式挂起?答案是确實有這種可能。

4個函數——S e n d M e s s a g e Ti m e o u t、S e n d M e s s a g e C a l l b a c k、S e n d N o t i f y M e s s a g e和R e p l y M e s s a g e,可以編寫保護性代碼防止出現這種情況。第一個函數是 S e n d M e s s a g e Ti m e o u t:

LRESULT WINAPI SendMessageTimeoutW(

    _In_ HWND hWnd,

    _In_ UINT Msg,

    _In_ WPARAM wParam,

    _In_ LPARAM lParam,

    _In_ UINT fuFlags,

    _In_ UINT uTimeout,

    _Out_opt_ PDWORD_PTR lpdwResult);

S e n d M e s s a g e Ti m e o u t函數,可以規定等待其他線程答回你消息的時間最大值。前 4個參數與傳遞給S e n d M e s s a g e的參數相同。對f u F l a g s參數,可以傳遞值S M TO _ N O R M A L (定義為0 )、S M TO _ A B O RT I F H U N G、S M TO _ B L O C K、S M TO _ N O T I M E O U T I F N O T H U N G或這些标志的組合。

标志是告訴S e n d M e s s a g e Ti m e o u t去檢視接收消息的線程是否處于挂起狀态,如果是,就立即傳回。S M TO _ N O T I M E O U T I F N O T H U N G标志使函數在接收消息的線 程 沒 有 挂 起 時 不 考 慮 等 待 時 間 限 定 值 。 S M T O _ B L O C K 标 志 使 調 用 線 程 在S e n d M e s s a g e Ti m e o u t傳回之前,不再處理任何其他發送來的消息。 S M TO _ N O R M A L标志在Wi n u s e r. h中定義成0,如果不想指定任何其他标志及組合,就使用這個标志。

S M TO _ B L O C K标志阻止系統允許這種中斷。僅當線程在等待處理發送的消息的時候 (不能處理别的發送消息),才使用這個标志。使用S M TO _ B L O C K可能會産生死鎖情況,直到等待時間期滿。例如,如果你的線程向另外一個線程發送一個消息,而這個線程又需要向你的線程發送消息。在這種情況下,兩個線程都不能繼續執行,并且都将永遠挂起。

函數中的u Ti m e o u t參數指定等待應答消息時間的毫秒數。如果這個函數執行成功,傳回T R U E,消息的結果複制到一個緩沖區中,該緩沖區的位址由 p d w R e s u l t參數指定。

用來線上程間發送消息的第二個函數是S e n d M e s s a g e C a l l b a c k:

BOOL WINAPI SendMessageCallbackW(

    _In_ HWND hWnd,

    _In_ UINT Msg,

    _In_ WPARAM wParam,

    _In_ LPARAM lParam,

    _In_ SENDASYNCPROC lpResultCallBack,

    _In_ ULONG_PTR dwData);

4個參數同S e n d M e s s a g e中使用的一樣。當一個線程調用S e n d M e s s a g e C a l l b a c k時,該函數發送消息到接收線程的發送消息隊列,并立即傳回使發送線程可以繼續執行。當接收線程完成對消息的處理時,一個消息被登記到發送線程的應答消息隊列中。然後,系統通過調用一個函數将這個應答通知給發送線程,該函數是使用下面的原型編寫的。

VOID CALLBACK ResultCallBack(

    HWND hwnd,

    UINT uMsg,

    ULONG_PTR dwData,

    LRESULT lResult);

S e n d M e s s a g e C a l l b a c k在執行線程間發送時會立即傳回,是以在接收線程完成對消息的處理時不是立即調用這個回調函數。而是由接收線程先将一個消息登記到發送線程的應答消息隊列。發送線程在下一次調用G e t M e s s a g e、P e e k M e s s a g e、Wa i t M e s s a g e或某個S e n d M e s s a g e*函數時,消息從應答消息隊列中取出,并執行R e s u l t C a l l B a c k函數。

函數還有另外一個用處。Wi n d o w s提供了一種廣播消息的方法,用這種方法你可以向系統中所有現存的重疊( o v e r l a p p e d)視窗廣播一個消息。這可以通過調用S e n d M e s s a g e函數,對參

數h w n d傳遞H W N D _ B R O A D C A S T(定義為-1)。使用這種方法廣播的消息,其傳回值我們并不感興趣,因為 S e n d M e s s a g e函數隻能傳回一個 L R E S U LT。但使用S e n d M e s s a g e C a l l b a c k,就可以向每一個重疊視窗廣播消息,并檢視每一個傳回結果。對每一個處理消息的視窗的傳回結果都要調用R e s u l t C a l l b a c k函數。

S e n d M e s s a g e C a l l b a c k向一個由調用線程所建立的視窗發送一個消息,系統立即調用視窗過程,并且在消息被處理之後,系統調用 R e s u l t C a l l B a c k函數。在R e s u l t C a l l B a c k函數傳回之後,系統從調用S e n d M e s s a g e C a l l b a c k的後面的代碼行開始執行。

S e n d N o t i f y M e s s a g e

BOOL WINAPI SendNotifyMessageW(

    _In_ HWND hWnd,

    _In_ UINT Msg,

    _In_ WPARAM wParam,

    _In_ LPARAM lParam);

将一個消息置于接收線程的發送消息隊列中,并立即傳回到調用線程。這一點與P o s t M e s s a g e函數一樣,但S e n d N o t i f y M e s s a g e在兩方面與P o s t M e s s a g e不同。

S e n d N o t i f y M e s s a g e是向另外的線程所建立的視窗發送消息,發送的消息比起接收線程消息隊列中存放的登記消息有更高的優先級。換句話說,由 S e n d N o t i f y M e s s a g e函數存放在隊列中的消息總是在P o s t M e s s a g e函數登記到隊列中的消息之前取出。

S e n d N o t i f y M e s s a g e同S e n d M e s s a g e函數完全一樣:S e n d N o t i f y M e s s a g e在消息被處理完之後才能傳回。

R e p l y M e s s a g e:

BOOL ReplyMessage(LRESULT lResult);

S e n d M e s s a g e Ti m e o u t、S e n d M e s s a g eC a l l b a c k和S e n d N o t i f y M e s s a g e發送消息,是為了保護自己以免被挂起。而線程調用 R e p l yM e s s a g e是為了接收視窗消息。當一個線程調用 R e p l y M e s s a g e時,它是要告訴系統,為了知道消息結果,它已經完成了足夠的工作,結果應該包裝起來并登記到發送線程的應答消息隊列中。這将使發送線程醒來,獲得結果,并繼續執行。

    有時候,你可能想知道究竟是在處理線程間的消息發送,還是在處理線程内的消息發送。

為了搞清楚這一點,可以調用I n S e n d M e s s a g e:

BOOL InSendMessage();

    還可以調用另外一個函數來确定視窗過程正在處理的消息類型:

DWORD InSendMessageEx(PVOID pvReserved);

26.4 喚醒一個線程

G e t M e s s a g e或Wa i t M e s s a g e,但沒有對這個線程或這個線程所建立視窗的消息時,系統可以挂起這個線程,這樣就不再給它配置設定 C P U時間。當有一個消息登記或發送到這個線程,系統要設定一個喚醒标志,指出現在要給這個線程配置設定 C P U時間,以便處理消息。正常情況下,如果使用者不按鍵或移動滑鼠,就沒有消息發送給任何視窗。這意味着系統中大多數線程沒有被配置設定給C P U時間。

26.4.1 隊列狀态标志

G e t Q u e u e S t a t u s函數來查詢隊列的狀态:

DWORD GetQueueStatus(UNIT fuFlags)

Windows核心程式設計 第26章 視窗消 息

26.4.2 從線程的隊列中提取消息的算法

G e t M e s s a g e或P e e k M e s s a g e時,系統必須檢查線程的隊列狀态标志的情況,并确定應該處理哪個消息。圖2 6 - 2和下面叙述的步驟說明了系統是如何确定線程應該處理的下一個消息的情況。

Windows核心程式設計 第26章 視窗消 息

26.4.3 利用核心對象或隊列狀态标志喚醒線程

或P e e k M e s s a g e函數導緻一個線程睡眠,直到該線程需要處理一個與使用者界(U I)相關的任務。有時候,若能讓線程被喚醒去處理一個與 U I有關的任務或其他任務,就會帶來許多友善。例如,一個線程可能啟動一個長時間運作的操作,并可以讓使用者取消這個操作。這個線程需要知道何時操作結束(與 U I無關的任務),或使用者是否按了C a n c e l按鈕(與U I相關的任務)來結束操作。

M s g Wa i t F o r M u l t i p l e O b j e c t s或M s g Wa i t F o r M u l t i p l e O b j e c t s E x函數,使線程等待它自已的消息。

DWORD MsgWaitForMultipleObjectsEx(

    DWORD nCount,

    PHANDLE phObject,

    DWORD dwMilliseconds,

    DWORD dwWakeMask,

    DWORD dwFlags);

是M s g Wa i t F o r M u l t i p l e O b j e c t s的一個超集( s u p e r s e t)。新的特性是通過d w F l a g s參數引進的。對這個參數,可以指定下面标志的任意組合(見表 2 6 - 3)。

Windows核心程式設計 第26章 視窗消 息

d w F l a g s傳遞零(0)。

下面是有關M s g Wa i t F o r M u l t i p l e O b j e c t s(E x)的一些重要内容:

• 由于這個函數隻是向核心句柄的數組增加一個内部事件核心對象, n C o u n t參數的最大值是M A X I M U M _ WA I T _ O B J E C T減1或6 3。

• 當對f Wa i t A l l參數傳遞FA L S E時,那麼當一個核心對象是有信号的(s i g n a l e d),或當指定的消息類型出現線上程的隊列時,函數傳回。

• 當對f Wa i t A l l參數傳遞T R U E時,那麼當所有核心對象成為有信号狀态,并且指定的消息類型出現線上程的隊列中時,函數傳回。這種行為似乎使許多開發人員感到驚訝。開發人員希望有一種辦法,當所有核心對象變成有信号的或者當指定的消息類型出現線上程

的隊列中時,可以喚醒線程。但沒有函數能夠這樣。

• 當調用這兩個函數時,實際是檢視是否有指定類型的新消息被放入調用線程的隊列。注意,上述最後一條會使許多開發人員吃驚。這裡有一個例子。假定一個線程的隊列目前包含有兩個按鍵消息。如果這個線程現在要調用 M s g Wa i t F o r M u l t i p l e O b j e c t s(E x),其中d w Wa k e M a s k參數設定成Q S _ I N P U T,線程将被喚醒,從隊列中取出第一個按鍵消息,并處理這個消息。現在,如果這個線程要再調用 M s g Wa i t F o r M u l t i p l e O b j e c t s(E x),線程将不會被喚醒,因為線程的隊列中沒有“新”的消息。

M W M O _ I N P U TAVA I LA B L E标志,這隻用于M s g Wa i t F o r M u l t i p l e O b j e c t s E x,而不用于M s g Wa i t F o r M u l t i p l e O b j e c t s。

這裡是一個例子,講述如何适當地編碼一個使用M s g Wa i t F o r M u l t i p l e O b j e c ts E x的消息循 

Windows核心程式設計 第26章 視窗消 息

26.5 通過消息發送資料

l P a r a m參數中指出了一個記憶體塊的位址。例如, W M _ S E T T E X T消息使用l P a r a m參數作為指向一個以零結尾的字元串的指針,這個字元串為視窗規定了新的文本标題串。考慮下面的調用:

SendMessage(FindWindow(NULL ,” C a l c u l a t o r”) ,WM_SETTEXT ,

0 ,(LPARAM)”XXXXXXX”)

這個調用看起來不會有害。它确定 C a l c u l a t o r程式視窗的視窗句柄,并試圖将視窗的标題改成“A Test Caption”。

微軟建立了一個特殊的視窗消息,W M _ C O P Y D ATA以解決這個(中間資料共享)問題:

COPYDATASTRUCT cds;

SendMessage(hwndReceicer,WM_COPYDATA ,(WPARAM)hwndSender,(LPARAM) &cds);

C O P Y D ATA S T R U C T是一個結構,定義在Wi n U s e r. h檔案中,形式如下面的樣子:

typedef struct tagCOPYDATASTRUCT {

    ULONG_PTR dwData;

    DWORD cbData;

    _Field_size_bytes_(cbData) PVOID lpData;

} COPYDATASTRUCT, *PCOPYDATASTRUCT;

C O P Y D ATA S T R U C T結構。資料成員d w D a t a是一個備用的資料項,可以存放任何值。例如,你有可能向另外的程序發送不同類型或不同類别的資料。可以用這個資料來指出要發送資料的内容。

資料成員規定了向另外的程序發送的位元組數, l p D a t a資料成員指向要發送的第一個位元組。l p D a t a所指向的位址,當然在發送程序的位址空間中。

繼續閱讀