深入理解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消息循环的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 解决: Client does not
- 下一篇: 详谈Windows消息循环机制