淺談Windows SDK視窗程式的訊息機制
Windows系統的訊息機制
一個庫函式(比如fopen),最終會呼叫作業系統的API來實現其功能,在Windows中,不僅庫函式最終會呼叫系統函式,系統函式反過來也會呼叫使用者函式,這種機制就是通過訊息來實現的。
我們假設程式發生了一項滑鼠點選“關閉”按鈕的操作,系統會發現這次操作,並將這次操作包裝成訊息結構體傳送到程式的訊息列表中,每一個程式都有一個訊息列表(後面會講),訊息列表中的訊息一般會一個一個的執行,當程式執行到“關閉”的訊息時,會呼叫程式的“關閉”回撥函式——這就是上面說的Windows系統也會呼叫使用者函式的底層邏輯。
Windows系統就是基於事件驅動的,訊息就是其中比較重要的一種事件。
訊息的結構體如下:
typedef struct tagMSG
{
HWND hwnd;//訊息產生的視窗
UINT message;//訊息的型別
WPARAM wParam;//訊息的具體資訊
WPARAM lParam;//訊息的具體資訊
DWORD time;//訊息發生的時間
POINT pt;//滑鼠的當前位置
}
訊息佇列
每一個Windows系統中的程式執行以後,系統都會為它分配一個訊息佇列,訊息佇列中儲存有系統發給它的訊息,比如說滑鼠左鍵按下時,就會產生WM_LBUTTONDOWN的事件,系統檢測到這個事件後,會把這個事件包裝成一個訊息傳送給目標程式的訊息列表中,訊息列表中的訊息是迴圈不斷的進行處理的。
系統傳送訊息分為兩種,一種是進隊訊息,還有一種是不進隊訊息,不進隊訊息就是系統直接將訊息傳送給程式,不需要進入佇列等待,後面還會詳細說。
不進隊訊息
傳送訊息可以用SendMessage函式,這個函式直接將訊息傳送給視窗,當視窗過程回撥函式執行完畢後才會返回,這種方式傳送的訊息就是不進隊訊息。
進隊訊息
PostMessage將訊息放進目標程式的訊息佇列後會立即返回。還有一個PostThreadMessage函式,用於向執行緒傳送訊息,對於執行緒訊息,hwnd成員總是NULL。
Windows視窗程式的實現
上面介紹了Windows下的訊息機制,系統傳送訊息到程式,程式接收到訊息後的處理統稱為視窗過程。
要實現視窗過程當然需要先建立一個視窗程式了。視窗程式的建立很簡單,主要分為以下幾個步驟:
- 註冊視窗類
- 建立視窗及顯示視窗
- 建立訊息迴圈
- 編寫視窗過程函式
接下來詳細說下細節。
註冊視窗類
註冊視窗要做的事情主要是設定視窗的屬性和特徵,通過呼叫RegisterClass(&wndclass)完成註冊,wndclass是一個叫作WNDCLASS的結構體,它可以設定視窗的很多屬性,在註冊之前有若干個欄位是必須要賦值的,它的結構體如下:
style:指定視窗的樣式
- CS_HREDRAW:當視窗的水平寬度發生變化時視窗進行重繪
- CS_VREDRAW:代表什麼不用說了吧
- CS_NOCLOSE:沒有關閉按鈕
- CS_DBLCLKS:可以傳送雙擊訊息
去掉某個樣式:style = style &~CS_XXXX
lpfnWndProc:視窗過程函式,Windows系統中每個視窗都可以有一個視窗過程函式,它的建立過程如下:
- 將視窗過程函式賦值給lpfnWndProc
- 註冊視窗類,呼叫RegisterClass函式進行註冊
- 主迴圈中呼叫DispatchMessage進行訊息派發
視窗過程函式的宣告如下:
typedef LRESULT (CALLBACK *WNDPROC)(HWND,UINT,WPARAM,WPARAM);
LRESULT實際上是long型別,CALLBACK實際上是__stdcall,在VC++開發環境中,預設的編譯選項是__cdecl,這種呼叫約定適用引數可變的函式,因為它是在函式外進行棧楨平衡。如果需要使用__stdcall需要指定,Windows API都遵循__stdcall。在Windows NT4.0以後程式中要使用回撥函式必須遵循__stdcall。
LoadIcon:這個欄位用來指定圖示,它的第二個引數用來指定一個資源名,Windows SDK的資源命名規範一般是ID+型別,比如說按鈕就是IDB_XXX。
hbrBackground:當視窗發生重繪時,系統使用這個欄位指定的背景色作為重繪的顏色
貼上一段參考程式碼:
WNDCLASSEXW wcex;
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.cbSize = sizeof(WNDCLASSEXW);
wcex.lpfnWndProc = (WNDPROC)WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInst;
wcex.hCursor = LoadCursor(nullptr, MAKEINTRESOURCE(IDI_ZZPEANALYZER));
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ZZPEANALYZER));
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
wcex.lpszMenuName = MAKEINTRESOURCE(IDC_ZZPEANALYZER);
RegisterClassExW(&wcex);
建立視窗及顯示視窗
建立視窗沒有什麼特別需要講解的地方,下面將要貼出的程式碼說明了一切,不過還是有需要注意的地方。
- 當呼叫CreateWindows函式時,傳遞引數hWndParent時,如果指定為WS_CHILD,說明建立的是子視窗,子視窗會被父視窗所影響,具體影響如下:
- 呼叫UpdateWindow函式會發送一個WM_PAINT訊息來重新整理視窗
HWND hWnd = CreateWindowEx(WS_EX_ACCEPTFILES,szWindowClass,szTitle,WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,700,500,NULL,NULL,hInstance,NULL);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nShowCmd);
UpdateWindow(hWnd);
建立訊息迴圈
每個程式都有一個訊息佇列,可以通過GetMessage函式獲取佇列中的訊息。
GetMessage原型如下:
BOOL GetMessage(LPMSG lpMsg,//訊息結構體
HWND hWnd,//接收指定視窗的訊息
UINT wMsgFilterMin,//獲取的訊息最小值
UINT wMsgFilterMax);//獲取的訊息最大值
GetMessage只有在接收到WM_QUIT時才會返回0,如果出現錯誤會返回-1,主迴圈一般如下:
MSG msg;
while (GetMessage(&msg,nullptr,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
GetMessage獲取到訊息佇列中的訊息以後,會執行TranslateMessage(),這個函式是用來將虛擬按鍵轉換成字元的,比如說使用者按下某個按鍵會產生WM_KEYDOWN和WM_KEYUP兩個虛擬鍵程式碼,TranslateMessage()會將這兩個虛擬鍵碼轉為WM_CHAR,並將這個message放到訊息佇列中,當下次呼叫GetMessage時這個訊息會被呼叫,TranslateMessage()並不會修改訊息,只是會新增一個訊息。
DispatchMessage()用來將訊息回傳給系統,系統會呼叫視窗過程的回撥函式。
PeekMessage
和GetMessage()類似的函式還有PeekMessage,這個函式跟GetMessage()唯一的不同是有一個wRemoveMsg欄位,當它是PM_REMOVE的時候在獲取到訊息後會將訊息從訊息佇列中刪除,跟GetMessage一致,當它是PM_NOREMOVE時不會將訊息從訊息佇列中移除。
編寫視窗過程函式
還記得在註冊視窗時繫結的回撥函式嗎?它就是視窗過程函式的入口。
wcex.lpfnWndProc = (WNDPROC)WndProc;
這個回撥函式的宣告如下:
引數在上面的註冊視窗部分講解過,就不贅述了。這個回撥函式內部的實現主要是通過switch的方式來分類不同的訊息處理函式,下面的圖可供參考:
WM_CHAR:當用戶按下鍵盤上的一個字元鍵,這個分支會被呼叫
WM_PAINT:當視窗的一部分或全部變為無效時就會呼叫這個分支,具體有以下幾種情況觸發:
- 視窗剛建立時
- 呼叫UpdateWindow時
- 視窗大小變化時(前提需要註冊視窗時設定了CS_HREDRAW和CS_VREDRAW標誌)
- 視窗被遮蓋再顯示時
注意,只有在WM_PAINT分支內部才可以使用BeginPaint(對應EndPaint),在外部只能通過GetDC函式來獲取DC(對應ReleaseDC)。
DC全稱Device Context,它包含了顯示器、圖形裝置驅動器的一些資訊,要在視窗上顯示文字或者顯示圖形都需要用到DC,如果沒有DC,我們就需要了解圖形裝置和它的驅動程式,通過呼叫驅動程式的介面來完成圖形的顯示,而圖形裝置有很多種,每一種都是不一樣的,如果真要去了解這些驅動程式再作畫,那工作量也太大了,因此微軟提供了DC,由它去跟圖形裝置驅動程式打交道,我們只要使用它就可以直接畫圖了
WM_CLOSE:當用戶單擊視窗上的關閉按鈕時,系統會發送一條WM_CLOSE訊息。
DestroyWindow函式會向視窗過程傳送WM_DESTROY訊息,DestroyWindow函式執行完視窗就已經被銷燬了,但是程式沒還沒有退出,因此需要在WM_DESTROY分支裡面進行最後的處理。
如果程式沒有響應WM_CLOSE訊息,系統就會呼叫DefWindowProc函式,這個函式會呼叫DestroyWindow函式來響應這條WM_CLOSE訊息。
WM_DESTROY:在這個分支裡呼叫了PostQuitMessage,它會向訊息佇列中傳送一條WM_QUIT訊息,之前我們說過GetMessage在收到WM_QUIT會返回0,當它返回0時,主迴圈就停止了。傳遞給PostQuitMessage的引數會作為WM_QUIT訊息的wParam引數,這個值通常作為WinMain函式的返回值。
就先介紹到這裡。