1. 程式人生 > >淺談Windows SDK視窗程式的訊息機制

淺談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下的訊息機制,系統傳送訊息到程式,程式接收到訊息後的處理統稱為視窗過程。
要實現視窗過程當然需要先建立一個視窗程式了。視窗程式的建立很簡單,主要分為以下幾個步驟:

  1. 註冊視窗類
  2. 建立視窗及顯示視窗
  3. 建立訊息迴圈
  4. 編寫視窗過程函式
    接下來詳細說下細節。

註冊視窗類

註冊視窗要做的事情主要是設定視窗的屬性和特徵,通過呼叫RegisterClass(&wndclass)完成註冊,wndclass是一個叫作WNDCLASS的結構體,它可以設定視窗的很多屬性,在註冊之前有若干個欄位是必須要賦值的,它的結構體如下:

style:指定視窗的樣式

  • CS_HREDRAW:當視窗的水平寬度發生變化時視窗進行重繪
  • CS_VREDRAW:代表什麼不用說了吧
  • CS_NOCLOSE:沒有關閉按鈕
  • CS_DBLCLKS:可以傳送雙擊訊息
    去掉某個樣式:style = style &~CS_XXXX

lpfnWndProc:視窗過程函式,Windows系統中每個視窗都可以有一個視窗過程函式,它的建立過程如下:

  1. 將視窗過程函式賦值給lpfnWndProc
  2. 註冊視窗類,呼叫RegisterClass函式進行註冊
  3. 主迴圈中呼叫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);

建立視窗及顯示視窗

建立視窗沒有什麼特別需要講解的地方,下面將要貼出的程式碼說明了一切,不過還是有需要注意的地方。

  1. 當呼叫CreateWindows函式時,傳遞引數hWndParent時,如果指定為WS_CHILD,說明建立的是子視窗,子視窗會被父視窗所影響,具體影響如下:
    image.png
  2. 呼叫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函式的返回值。

就先介紹到這裡。