理解Windows消息循環機制


理解消息循環和整個消息傳送機制對Windows編程十分重要。如果對消息處理的整個過程不了解,在windows編程中會遇到很多令人困惑的地方。

什么是消息(Message)

每個消息是一個整型數值,如果查看頭文件(查看頭文件了解API是一個非常好的習慣和普遍的做法)可以發現如下一些宏定義:

#define WM_INITDIALOG                   0x0110
#define WM_COMMAND                      0x0111

#define WM_LBUTTONDOWN                  0x0201
//...


在 Windows通信中,至少一些基本Windows通信,幾乎都要用到消息。如果你想讓窗口或控件(實質上,控件是特殊的窗口)執行何種動作,你應該傳送 一個消息給它;如果另一個窗口想讓你執行何種操作,它可以傳送一個消息給你。如果一個事件,如敲擊鍵盤、移動鼠標、點擊按鈕等,系統將消息傳送給窗口,如 果你是這些窗口之一,你將接收到消息執行相應的操作。

每個Windows消息共有兩個參數,wParam和lParam。最初的 wParam是16位(Win16時代)的,lParam是32位的。在Win32中,兩個參數都是32位的。並不是所有的消息都是用這兩個參數,每個消 息使用它們的方式也不盡相同。如WM_CLOSE消息會忽略上述兩個參數;再如WM_COMMAND消息使用上述兩個參數,wParam包含”兩個” 值,HIWORD(wParam)是通知信息(如果可用),LOWORD(wParam)是發送消息的控件或菜單的ID,lParam是發送消息的控件的 HWND(窗口句柄),如果這個值為NULL,表示這個消息不是由控件發送的。

HIWORD()和LOWORD()是Windows定義的宏,分別取出一個32位整型值的高字和低字。在Win32中,一個”字”是一個16位整型,DWORD(Double WORD)是32位整型。

可 以用PostMessage()或SendMessage()發送消息。PostMessage()把一個消息放入消息隊列(Message Queue)后立即返回,也就是當調用PostMessage(),函數執行完成返回時,很可能消息尚未處理。SendMessage()直接將消息發送 到窗口,直到這個消息處理完成才返回。如果要關閉一個窗口,可以給它發送一個WM_CLOSE消息,像PostMessage(hwnd, WM_CLOSE, 0, 0); 效果跟點擊窗口右上角的(關閉)按鈕是一樣的。注意這里的wParam和lParam的值都是0,因為前面提到過,WM_CLOSE消息會忽略上述兩個參數。

對話框(Dialogs)

如 果使用對話框,為跟控件通信,你需要向控件發送消息。你或者可以使用GetDlgItem()函數根據控件的ID取得控件的句柄,然后調用 SendMessage()函數發送消息;或者使用SendDlgItemMessage()組合了上面的步驟。傳入一個窗口句柄和子控件的ID能夠取得 子控件的句柄,用這個句柄發送消息。跟SendDlgItemMessage()類似的API如GetDlgItemText()能夠對所有的窗口進行操 作,而不僅僅是對話框。

什么是消息隊列(Message Queue)

假 設一個場景:系統正在處理WM_PAINT消息,就在這時用戶在鍵盤上敲擊了一些按鍵,這時會發生什么呢?系統應該中斷繪圖操作然后處理按鍵消息還是應該 丟棄按鍵的消息?很明顯這些都是不合理的,因此我們引入了消息隊列,當消息發送過來,將消息加入消息隊列,當一個消息被處理時,將其從消息隊列移除。這樣 確保消息不會丟失,當你正在處理一個消息時,其它到來的消息可以加入到消息隊列直到被處理。

什么是消息循環(Message Loop)

while(GetMessage(&Msg, NULL, 0, 0) > 0)
{
    TranslateMessage(&Msg);
    DispatchMessage(&Msg);
}


上面代碼的執行過程為:
1. 消息循環調用GetMessage()從消息隊列中查找消息進行處理,如果消息隊列為空,程序將停止執行並等待(程序阻塞)。
2. 事件發生時導致一個消息加入到消息隊列(例如系統注冊了一個鼠標點擊事件),GetMessage()將返回一個正值,這表明有消息需要被處理,並且消息已經填充到傳入的MSG參數中;當傳入WM_QUIT消息時返回0;如果返回值為負表明發生了錯誤。
3. 取出消息(在Msg變量中)並將其傳遞給TranslateMessage()函數,這個函數做一些額外的處理:將虛擬鍵值信息轉換為字符信息。這一步實際上是可選的,但有些地方需要用到這一步。
4. 上面的步驟執行完后,將消息傳遞給DispatchMessage()函數。DispatchMessage()函數將消息分發到消息的目標窗口,並且查找目標窗口過程函數,給窗口過程函數傳遞窗口句柄、消息、wParam、lParam等參數然后調用該函數。
5. 在窗口過程函數中,檢查消息和其他參數,你可以用它來實現你想要的操作。如果不想處理某些特殊的消息,你應該總是調用DefWindowProc()函數,系統將按按默認的方式處理這些消息(通常認為是不做任何操作)。
6. 一旦一個消息處理完成,窗口過程函數返回,DispatchMessage()函數返回,繼續循環處理下一個消息。

消 息循環對Windows編程來說是一個非常重要的概念。窗口過程函數並不是系統自動調用的,而是由開發人員自己通過調用 DispatchMessage()間接的調用的。如果你願意,可以調用GetWindowLong()函數通過窗口句柄查找到窗口過程函數直接調用達到 消息處理的目的。

while(GetMessage(&Msg, NULL, 0, 0) > 0)
{
    WNDPROC fWndProc = (WNDPROC)GetWindowLong(Msg.hwnd, GWL_WNDPROC);
    fWndProc(Msg.hwnd, Msg.message, Msg.wParam, Msg.lParam);
}

我嘗試着寫了上面的代碼,它確實能工作,但這里存在各種問題,像Unicode/ANSI編碼轉換、定時器回調等等都這樣的代碼都不適合,並且很可能導致很多打斷很多程序的正常運行。因此這樣的代碼在這里僅僅是試驗,真實項目中一定不能編寫這樣的代碼。

注 意這里我們用GetWindowLong()來獲得相關窗口的窗口過程函數。為什么我們不直接調用WndProc()函數呢?消息循環會處理程序中所有窗 口的消息,包括像按鈕、列表框等有他們自己的窗口過程函數的控件,因此我們要保證調用正確的窗口過程函數。盡管有時幾個窗口調用同一個窗口過程函數,但函 數的第一個參數 (窗口的句柄) 通常用於告知窗口過程函數是那個窗口發送的消息。

代碼可以看出,程序的大部分時間都在處理消息循環。窗 口會不斷的處理發過來的消息,但如果要退出程序該怎么做呢?因為我們用的是while()循環,如果GetMessage()返回的是FALSE(即0) 會退出循環,程序能夠執行到WinMain()結束處,即程序退出:這正是PostQuitMessage()函數完成的工作,該函數會將WM_QUIT 消息添加到消息隊列的隊尾,GetMessage()從消息隊列取出WM_QUIT消息,填充Msg結構,返回的不是正數,而是0。與此同時,結構Msg 的成員wParam的值會被置為你傳給PostQuitMessage()函數參數的值,你可以選擇忽略它或做為WinMain()函數的返回值即進程的 退出代碼(Exit Code)。

注意:如果發生錯誤,GetMessage()函數將返回-1。你應該記住這點,說不定你的程序會因此 出錯。盡管GetMessage()返回值位BOOL型,但它可以返回TRUE或FALSE之外的值,因為BOOL被定義成UINT(unsigned int)。下面的程序貌似能正常工作,但有些時候不能正常工作。

while(GetMessage(&Msg, NULL, 0, 0))

while(GetMessage(&Msg, NULL, 0, 0) != 0)

while(GetMessage(&Msg, NULL, 0, 0) == TRUE)


上面的代碼都是錯誤的!有些程序中你會看到會使用第一中方式,使用這種方式你必須保證GetMessage()總是執行成功,否則應該使用下面這段代碼:

while(GetMessage(&Msg, NULL, 0, 0) > 0)


希望你對Windows消息循環能有很好的理解,如果還沒有,慢慢來,在使用過程中會逐漸理解的。

英文原文:http://winprog.org/tutorial/message_loop.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM