在創建窗口、顯示窗口、更新窗口後,我們需要編寫一個消息循環,不斷地從消息隊列中取出消息,並進行響應。要從消息隊列中取出消息,我們需要調用GetMessage()函數,該函數的原型聲明如下:在創建窗口、顯示窗口、更新窗口後,我們需要編寫一個消息循環,不斷地從消息隊列中取出消息,並進行響應。要從消息隊列中取出消息,我們需要調用GetMessage()函數,該函數的原型聲明如下:
BOOL GetMessage(
LPMSG lpMsg, // address of structure with message
HWND hWnd, // handle of window
UINT wMsgFilterMin, // first message
UINT wMsgFilterMax // last message
);
參數lpMsg指向一個消息(MSG)結構體,GetMessage從線程的消息隊列中取出的消息信息將保存在該結構體對象中。
參數hWnd指定接收屬於哪一個窗口的消息。通常我們將其設置爲NULL,用於接收屬於調用線程的所有窗口的窗口消息。
參數wMsgFilterMin指定要獲取的消息的最小值,通常設置爲0。
參數wMsgFilterMax指定要獲取的消息的最大值。如果wMsgFilterMin和wMsgFilter Max都設置爲0,則接收所有消息。
GetMessage函數接收到除WM_QUIT外的消息均返回非零值。對於WM_QUIT消息,該函數返回零。如果出現了錯誤,該函數返回-1,例如,當參數hWnd是無效的窗口句柄或lpMsg是無效的指針時。
通常消息循環代碼如下:
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
GetMessage函數只有在接收到WM_QUIT消息時,才返回0。此時while語句判斷的條件爲假,循環退出,程序纔有可能結束運行。在沒有接收到WM_QUIT消息時,Windows應用程序就通過這個while循環來保證程序始終處於運行狀態。
TranslateMessage函數用於將虛擬鍵消息轉換爲字符消息。字符消息被投遞到調用線程的消息隊列中,當下一次調用GetMessage函數時被取出。當我們敲擊鍵盤上的某個字符鍵時,系統將產生WM_KEYDOWN和WM_KEYUP消息。這兩個消息的附加參數(wParam和lParam)包含的是虛擬鍵代碼和掃描碼等信息,而我們在程序中往往需要得到某個字符的ASCII碼,TranslateMessage這個函數就可以將WM_KEYDOWN和WM_ KEYUP消息的組合轉換爲一條WM_CHAR消息(該消息的wParam附加參數包含了字符的ASCII碼),並將轉換後的新消息投遞到調用線程的消息隊列中。注意,TranslateMessage函數並不會修改原有的消息,它只是產生新的消息並投遞到消息隊列中。
DispatchMessage函數分派一個消息到窗口過程,由窗口過程函數對消息進行處理。DispachMessage實際上是將消息回傳給操作系統,由操作系統調用窗口過程函數對消息進行處理(響應)。
(1)操作系統接收到應用程序的窗口消息,將消息投遞到該應用程序的消息隊列中。
(2)應用程序在消息循環中調用GetMessage函數從消息隊列中取出一條一條的消息。取出消息後,應用程序可以對消息進行一些預處理,例如,放棄對某些消息的響應,或者調用TranslateMessage產生新的消息。
(3)應用程序調用DispatchMessage,將消息回傳給操作系統。消息是由MSG結構體對象來表示的,其中就包含了接收消息的窗口的句柄。因此,DispatchMessage函數總能進行正確的傳遞。
(4)系統利用WNDCLASS結構體的lpfnWndProc成員保存的窗口過程函數的指針調用窗口過程,對消息進行處理(即“系統給應用程序發送了消息”)。
以上就是Windows應用程序的消息處理過程。
提示:
(1)從消息隊列中獲取消息還可以調用PeekMessage函數,該函數的原型聲明如下所示:
BOOL PeekMessage(
LPMSG lpMsg, // message information
HWND hWnd, // handle to window
UINT wMsgFilterMin, // first message
UINT wMsgFilterMax, // last message
UINT wRemoveMsg // removal options
);
前4個參數和GetMessage函數的4個參數的作用相同。最後1個參數指定消息獲取的方式,如果設爲PM_NOREMOVE,那麼消息將不會從消息隊列中被移除;如果設爲PM_REMOVE,那麼消息將從消息隊列中被移除(與GetMessage函數的行爲一致)。關於PeekMessage函數的更多信息,請參見MSDN。
(2)發送消息可以使用SendMessage和PostMessage函數。SendMessage將消息直接發送給窗口,並調用該窗口的窗口過程進行處理。在窗口過程對消息處理完畢後,該函數才返回(SendMessage發送的消息爲不進隊消息)。PostMessage函數將消息放入與創建窗口的線程相關聯的消息隊列後立即返回。除了這兩個函數外,還有一個PostThreadMessage函數,用於向線程發送消息,對於線程消息,MSG結構體中的hwnd成員爲NULL。
消息循環錯誤分析
while(GetMessage(&msg,hwnd,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
…
注意代碼中以粗體顯示的部分。這段代碼基於這樣一個想法:第1章的程序只有一個窗口,而我們前面說了GetMessage函數的hWnd參數是用於指定接收屬於哪一個窗口的消息,於是不少人就在消息循環中爲GetMessage函數的hWnd參數指定了CreateWindow函數返回的窗口句柄。
讀者可以用上述代碼中的消息循環部分替換1.5節代碼中的消息循環部分,然後運行程序,關閉程序。你會發現你的機器變慢了,同時按下鍵盤上的Ctrl + Alt + Delete鍵,啓動Windows的任務管理器,切換到“進程”選項卡,單擊“CPU”項進行排序,你會發現如圖1.7所示的情況。
從圖1.7中可以看到,WinMain.exe的CPU佔用率接近100,難怪機器“變慢了”。那麼這是什麼原因呢?實際上這個問題的答案在MSDN中就可以找到,並且就在GetMessage函數的說明文檔中。不少初學者在遇到問題時,首先是頭腦一片空白,接着就去找人求助,這種思想用在程序開發的學習中,沒有什麼好處。筆者經常遇到學員問問題,結果有不少問題的答案在MSDN關於某個函數的解釋中就可看到(由於顯示器的限制,有的答案需要滾動窗口才能看到 J)。所以在這裏,筆者也建議讀者遇到問題一定要記得查看MSDN,學會使用MSDN並從中汲取知識,將使你受用無窮。
圖1.7 WinMain.exe的CPU佔用率接近100
回到正題,在1.4.3節介紹GetMessage函數時,曾說過如果hWnd參數是無效的窗口句柄或lpMsg參數是無效的指針時,GetMessage函數將返回-1。當我們關閉窗口時,調用了DestroyWindow來銷燬窗口,由於窗口被銷燬了,窗口的句柄當然也就是無效的句柄了,那麼GetMessage將返回-1。在C/C++語言中,非0即爲真,由於窗口被銷燬,句柄變爲無效,GetMessage總是返回-1,循環條件總是爲真,於是形成了一個死循環,機器當然就“變慢了”。J
在MSDN關於GetMessage函數的說明文檔中給出了下面的代碼:
BOOL bRet;
while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{
if (bRet == -1)
{
// handle the error and possibly exit
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
針對我們這個問題,可以修改上述代碼如下:
…
HWND hwnd;
hwnd=CreateWindow(…);
…
MSG msg;
BOOL bRet;
while( (bRet = GetMessage( &msg, hwnd, 0, 0 )) != 0)
{
if (bRet == -1)
{
// handle the error and possibly exit
return -1;
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
…