窗口消息——Windows核心编程学习手札之二十六
窗口消息
——Windows核心編程學習手札之二十六
Windows允許一個進程至多建立10000個不同類型的用戶對象(user object):圖符、光標、窗口類、菜單、加速鍵表等,當一個線程調用一個函數來建立某個對象時,則該對象就歸屬這個線程的進程所擁有。這種線程擁有關系的概念對窗口有重要意義:建立窗口的線程必須為窗口處理所有消息。一個線程創建了一個窗口,系統為其分配一個消息隊列,用于窗口消息的派送(dispatch),為了使窗口接收這些消息,線程必須有自己的消息循環。一旦線程調用一個與圖形界面有關的函數(如檢查它的消息隊列或建立一個窗口)系統就會為該線程分配一些另外的資源,以便它能夠執行與用戶界面有關的任務,特別的是,系統分配一個THREADINFO結構,并將這個數據結構和線程聯系起來。
THREADINFO結構是一個內部的、未公開的數據結構,用來指定線程的登記消息隊列(posted-message queue)、發送消息隊列(send-message queue)、應答消息隊列(reply-message queue)、虛擬輸入隊列(virtualized-input queue)、喚醒標志(wake flag)、以及用來描述線程局部輸入狀態的若干變量。當線程有了與之相聯系的THREADINFO結構時,線程就有了自己的消息隊列集合。
將消息發送到線程的消息隊列中
發送到線程的消息被放置在接收線程的登記消息隊列中,發送消息通過函數PostMessage完成:
?????? BOOL PostMessage(HWND hwnd,
?????????????????????????????????? ?UINT uMsg,
???????????????????? WPARAM wParam,
???????????????????? LPARAM lParam);
也可以通過調用PostThreadMessage將消息放置在接收線程的登記消息隊列中:
?????? BOOL PostThreadMessage(DWORD dwThreadID,
???????????????????????????????????????????????? UINT uMsg,
??????????????????????????? WPARAM wParam,
??????????????????????????? LPARAM lParam);
可以通過調用GetWindowsThreadPorcessID來確定是哪個線程建立了一個窗口:
?????? DWORD GetWindowThreadProcessID(HWND hwnd,
????????????????????????????????????? PDWORD pdwProcessID);
傳遞窗口DWORD地址返回擁有該線程的進程ID。
為線程編寫主消息循環以便在GetMessage或PeekMessage取出一個消息時,主消息循環代碼檢查hwnd是否為null,并檢查MSG結構的msg成員來執行特殊的處理。如線程確定該消息不被指派給一個窗口,則不調用DispatchMessage,消息循環進行下一個消息。
終止線程的消息循環,可以調用函數PostQuitMessage:
?????? VOID PostQuitMessage(int nExitCode);
類似于調用PostThreadMessage(GetCurrentThreadId(),WM_QUIT,nExitCode,0);
不過,PostQuitMessage并不實際登記一個消息到任何一個THREADINFO結構的隊列,只是在內部,PostQuitMessage設定QS_QUIT喚醒標志,并設置THREADINFO結構的nExitCode成員。
向窗口發送消息
使用SendMessage函數可將窗口消息直接發送給一個窗口過程:
?????? LRESULT SendMessage(HWND hwnd,
????????????????????????????????????????? ?UINT uMsg,
???????????????????????? WPARAM wParam,
???????????????????????? LPARAM lParam);
窗口過程將處理這個消息,只有當消息被處理之后,SendMessage才能返回到調用程序。由于具有這個同步特性,比之PostMessage或PostThreadMessage,SendMessage函數用得頻繁,調用這個函數的線程在下一行代碼執行之前就知道窗口消息已經被完全處理。
如果調用SendMessage的線程向自己創建的窗口發送一個消息:調用指定窗口的窗口過程,將其作為一個子例程,當窗口過程完成對消息的處理,向SendMessage返回一個值,SendMessage再將這個值返回給調用線程。
如果一個線程通過SendMessage向其他線程創建的窗口發送消息:發送線程掛起,由另外線程處理消息并返回。由于可能造成線程掛起(hang):接收線程的bug導致發送線程掛起,可利用4個函數:SendMessageTimeout、SendMessageCallback、SendNotifyMessage、ReplyMessage編寫保護性代碼防止出現這個情況:
?????? LRESULT SendMessageTimeout(
????????????????????????????????????????? HWND hwnd,
????????????????????????????????????????? UINT uMsg,
????????????????????????????????????????? WPARAM wParam,
????????????????????????????????????????? LPARAM lParam,
????????????????????????????????????????? UINT fuFlags,
????????????????????????????????????????? UINT uTimeout,
????????????????????????????????????????? PDWORD_PTR pdwResult);
這個函數可以設置等待其他線程返回消息的時間最大值。
線程間發送消息的第二個函數:
?????? BOOL SendMessageCallback(
?????????????????????????????????? HWND hwnd,
??????????????????? UINT uMsg,
??????????????????? WPARAM wParam,
??????????????????? LPARAM lParam,
??????????????????? SENDASYNCPROC pfnResultCallBack,
??????????????????? ULONG_PTR dwData);
當發送消息的線程調用該函數時發送消息到接收線程的發送消息隊列,并立即返回使發送線程可以繼續執行。當接收線程完成對消息的處理時,一個消息被登記到發送線程的應答消息隊列中,然后系統通過調用一個函數將這個應答通知給發送線程。
線程發送消息的第三個函數是:
?????? BOOL SendNotifyMessage(
???????????????????? HWND hwnd,
???????????????????? UINT uMsg,
???????????????????? WPARAM wParam,
???????????????????? LPARAM lParam);
該函數將一個消息置于接收線程的發送消息隊列中,并立即返回到發送線程,與PostMessage一樣,但有兩點不同:SendNotifyMessage是向另外的線程建立的窗口發送消息,發送的消息比起接收線程消息隊列中存放的登記消息有更高的優先級;當向發送線程自己創建的窗口發送消息時,SendNotifyMessage和SendMessage函數一樣,在消息被處理完后才能返回。
用于線程發送消息的第四個函數:
?????? BOOL ReplyMessage(LRESULT lResult);
當發送線程調用ReplyMessage,是告訴系統:消息結果應該包裝起來并登記到發送線程的應答消息隊列中,喚醒發送線程獲得結果并繼續執行。
喚醒一個線程
當一個線程調用GetMessage或WaitMessage,但沒有對這個線程或這個線程所建立窗口的消息時,系統可以掛起這個線程,這樣就不再分配給它CPU時間。當有一個消息登記或發送到這個線程,系統要設置一個喚醒標志,指出現在要給這個線程分配CPU時間,以便處理消息。正常情況下,如果用戶不按鍵或移動鼠標,就沒有消息發送給任何窗口。
當一個線程正在運行時,可以通過調用GetQueueStatus函數來查詢隊列的狀態:
?????? DWORD GetQueueStatus(UINT fuFlags);
參數fuFlags是一個標志或一組由OR連接起來的標志,可用來測試特定的喚醒位。
當一個線程調用GetMessage或PeekMessage時,系統必須檢查線程的隊列狀態標志的情況,并確定應該處理哪個消息:
1)如果QS_SENDMESSAGE標志被設置,系統向相應的窗口發送消息;
2)如果消息在線程的登記消息隊列中,函數GetMessage或PeekMessage填充傳遞給它們的MSG結構;
3)如果QS_QUIT標志被設置,GetMessage或PeekMessage返回一個WM_QUIT消息,并復位QS_QUIT標志;
4)如果消息在線程的虛擬輸入隊列,函數GetMessage或PeekMessage返回硬件輸入消息;
5)如果QS_PAINT標志被設置,GetMessage或PeekMessage為相應的窗口返回一個WM_PAINT消息;
6)如果QS_TIMER標志被設置,GetMessage或PeekMessage返回一個WM_TIMER消息;
GetMessage或PeekMessage函數導致一個線程睡眠,也可以利用內核對象或隊列狀態標志喚醒線程,函數:
?????? DWORD MsgWaitForMultipleObjects(
??????????????????????????? DWORD nCount,
??????????????????????????? PHANDLE phObjects,
??????????????????????????? BOOL fWaitAll,
??????????????????????????? DWORD dwMilliseconds,
??????????????????????????? DWORD dwWakeMask);
?????? DWORD MsgWaitForMultipleObjectsEx(
??????????????????????????? DWORD nCount,
??????????????????????????? PHANDLE phObjects,
??????????????????????????? DWORD dwMilliseconds,
??????????????????????????? DWORD dwWakeMask);
通過消息發送數據
特殊窗口消息WM_COPYDATA可以通過消息在進程間發送和接收數據。
例子:
?????? COPYDATASTRUCT cds;
?????? SendMessage(hwndReceiver,WM_COPYDATA,(WPARAM)hwndSender,(LPARAM)&cds);
其中COPYDATASTRUCT是一個結構,定義在WinUser.h文件中:
?????? typedef struct tagCOPYDATASTRUCT{
???????????????????? ULONG_PTR dwData;
???????????????????? DWORD cbData;
???????????????????? PVOID lpData;
???????????????????? }COPYDATASTRUCT;
當SendMessage看到要發送一個WM_COPYDATA消息時,它建立一個內存映像文件,大小是cbData字節,并從發送進程的地址空間向這個內存映像文件復制數據,然后向目的窗口發送消息。
?
總結
以上是生活随笔為你收集整理的窗口消息——Windows核心编程学习手札之二十六的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 未处理异常和C++异常——Windows
- 下一篇: IT项目十大灾难(转载)