上節講了消息的相關概念,本文將進一步聊聊C++中的消息機制。
從簡單例子探析核心原理
在講之前,我們先看一個簡單例子:創建一個窗口和兩個按鈕,用來控制窗口的背景顏色。其效果

圖1.效果圖
Win32Test.h
1 #pragma once 2 3 #include <windows.h> 4 #include <atltypes.h> 5 #include <tchar.h> 6 7 //資源ID 8 #define ID_BUTTON_DRAW 1000 9 #define ID_BUTTON_SWEEP 1001 10 11 // 注冊窗口類 12 ATOM AppRegisterClass(HINSTANCE hInstance); 13 // 初始化窗口 14 BOOL InitInstance(HINSTANCE, int); 15 // 消息處理函數(又叫窗口過程) 16 LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); 17 // (白色背景)按鈕事件 18 void OnButtonWhite(); 19 // (灰色背景)按鈕事件 20 void OnButtonGray(); 21 // 繪制事件 22 void OnDraw(HDC hdc);
Win32Test.cpp
1 #include "stdafx.h" 2 #include "Win32Test.h" 3 4 5 //字符數組長度 6 #define MAX_LOADSTRING 100 7 8 //全局變量 9 HINSTANCE hInst; // 當前實例 10 TCHAR g_szTitle[MAX_LOADSTRING] = TEXT("Message process"); // 窗口標題 11 TCHAR g_szWindowClass[MAX_LOADSTRING] = TEXT("AppTest"); // 窗口類的名稱 12 HWND g_hWnd; // 窗口句柄 13 bool g_bWhite = false; // 是否為白色背景 14 15 //WinMain入口函數 16 int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) 17 { 18 UNREFERENCED_PARAMETER(hPrevInstance); 19 UNREFERENCED_PARAMETER(lpCmdLine); 20 // 注冊窗口類 21 if(!AppRegisterClass(hInstance)) 22 { 23 return (FALSE); 24 } 25 // 初始化應用程序窗口 26 if (!InitInstance (hInstance, nCmdShow)) 27 { 28 return FALSE; 29 } 30 31 // 消息循環 32 MSG msg; 33 while (GetMessage(&msg, NULL, 0, 0)) 34 { 35 TranslateMessage(&msg); 36 DispatchMessage(&msg); 37 } 38 return (int) msg.wParam; 39 } 40 41 42 43 // 注冊窗口類 44 ATOM AppRegisterClass(HINSTANCE hInstance) 45 { 46 WNDCLASSEX wcex; 47 wcex.cbSize = sizeof(WNDCLASSEX); 48 wcex.style = CS_HREDRAW | CS_VREDRAW; 49 wcex.lpfnWndProc = WndProc; 50 wcex.cbClsExtra = 0; 51 wcex.cbWndExtra = 0; 52 wcex.hInstance = hInstance; 53 wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION); 54 wcex.hCursor = LoadCursor(NULL, IDC_ARROW); 55 wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); 56 wcex.lpszMenuName = NULL; 57 wcex.lpszClassName = g_szWindowClass; 58 wcex.hIconSm = NULL; 59 60 return RegisterClassEx(&wcex); 61 } 62 63 64 65 // 保存實例化句柄並創建主窗口 66 BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) 67 { 68 hInst = hInstance; // 保存handle到全局變量 69 g_hWnd = CreateWindow(g_szWindowClass, g_szTitle, WS_OVERLAPPEDWINDOW, 0, 0, 400, 300, NULL, NULL, hInstance, NULL); 70 // 創建按鈕 71 HWND hBtWhite = CreateWindowEx(0, L"Button", L"白色", WS_CHILD | WS_VISIBLE | BS_TEXT, 100, 100, 50, 20, g_hWnd, (HMENU)ID_BUTTON_DRAW, hInst, NULL); 72 HWND hBtGray = CreateWindowEx(0, L"Button", L"灰色", WS_CHILD | WS_VISIBLE | BS_CENTER, 250, 100, 50, 20, g_hWnd, (HMENU)ID_BUTTON_SWEEP, hInst, NULL); 73 74 if (!g_hWnd) 75 { 76 return FALSE; 77 } 78 ShowWindow(g_hWnd, nCmdShow); 79 UpdateWindow(g_hWnd); 80 81 return TRUE; 82 } 83 84 85 86 // (窗口)消息處理 87 LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) 88 { 89 int wmId, wmEvent; 90 PAINTSTRUCT ps; 91 HDC hdc; 92 93 switch (message) 94 { 95 case WM_COMMAND: 96 wmId = LOWORD(wParam); 97 //wmEvent = HIWORD(wParam); 98 99 switch (wmId) 100 { 101 case ID_BUTTON_DRAW: 102 OnButtonWhite(); 103 break; 104 case ID_BUTTON_SWEEP: 105 OnButtonGray(); 106 break; 107 default: 108 return DefWindowProc(hWnd, message, wParam, lParam); 109 } 110 break; 111 case WM_PAINT: 112 hdc = BeginPaint(hWnd, &ps); 113 OnDraw(hdc); 114 EndPaint(hWnd, &ps); 115 break; 116 case WM_DESTROY: 117 PostQuitMessage(0); 118 break; 119 default: 120 return DefWindowProc(hWnd, message, wParam, lParam); 121 } 122 return 0; 123 } 124 125 126 127 //事件處理 128 129 //按下hBtWhite時的事件 130 void OnButtonWhite() 131 { 132 g_bWhite = true; 133 InvalidateRect(g_hWnd, NULL, FALSE); //刷新窗口 134 } 135 136 //按下hBtGray時的事件 137 void OnButtonGray() 138 { 139 g_bWhite = false; 140 InvalidateRect(g_hWnd, NULL, FALSE); //刷新窗口 141 } 142 143 //繪制事件(每次刷新時重新繪制圖像) 144 void OnDraw(HDC hdc) 145 { 146 POINT oldPoint; 147 SetViewportOrgEx(hdc, 0, 0, &oldPoint); 148 RECT rcView; 149 GetWindowRect(g_hWnd, &rcView); // 獲得句柄的畫布大小 150 HBRUSH hbrWhite = (HBRUSH)GetStockObject(WHITE_BRUSH); 151 HBRUSH hbrGray = (HBRUSH)GetStockObject(GRAY_BRUSH); 152 if (g_bWhite) 153 { 154 FillRect(hdc, &rcView, hbrWhite); 155 } else 156 { 157 FillRect(hdc, &rcView, hbrGray); 158 } 159 SetViewportOrgEx(hdc, oldPoint.x, oldPoint.y, NULL); 160 }
在上面這個例子中,消息的流經過程如下:

圖2.消息的流經過程
這與《編程思想之消息機制》中圖1(消息機制原理)是相吻合的,這就是Windows消息機制的核心部分,也是Windows API開發的核心部分。Windows系統和Windows下的程序都是以消息為基礎,以事件為驅動。
RegisterClassEx的作用是注冊一個窗口,在調用CreateWindow創建一個窗口前必須向windows系統注冊獲惟一的標識。
1 while (GetMessage(&msg, NULL, 0, 0)) 2 { 3 TranslateMessage(&msg); 4 DispatchMessage(&msg); 5 }
這個while循環就是消息循環,不斷地從消息隊列中獲取消息,並通過DispatchMessage(&msg)將消息分發出去。消息隊列是在Windows操作系統中定義的(我們無法看到對應定義的代碼),對於每一個正在執行的Windows應用程序,系統為其建立一個“消息隊列”,即應用程序隊列,用來存放該程序可能創建的各種窗口的消息。DispatchMessage會將消息傳給窗口函數(即消息處理函數)去處理,也就是WndProc函數。WndProc是一個回調函數,在注冊窗口時通過wcex.lpfnWndProc將其傳給了操作系統,所以DispatchMessage分發消息后,操作系統會調用窗口函數(WndProc)去處理消息。
每一個窗口都應該有一個函數負責消息處理,程序員必須負責設計這個所謂的窗口函數Wndproc。
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
中的四個參數就是消息的相關信息(消息來自的句柄、消息類型等),函數中通過switch/case根據不同的消息類型分別進行不同的處理。在收到相應類型的消息之后,可調用相應的函數去處理,如OnButtonWhite、OnButtonGray、OnDraw,這就是事件處理的雛形。 在default中調用了DefWindowProc,DefWindowProc是操作系統定義的默認消息處理函數,這是因為所有的消息都必須被處理,應用程序不處理的消息需要交給操作系統處理。
消息的定義和類型
Windows消息都以WM_為前綴,意思是“Windows Message” ,如WM_CREATE、WM_PAINT等。消息的定義如下:
1 typedef struct tagMsg 2 { 3 HWND hwnd; //接受該消息的窗口句柄 4 UINT message; //消息常量標識符,也就是我們通常所說的消息號 5 WPARAM wParam; //32位消息的特定附加信息,確切含義依賴於消息值 6 LPARAM lParam; //32位消息的特定附加信息,確切含義依賴於消息值 7 DWORD time; //消息創建時的時間 8 POINT pt; //消息創建時的鼠標/光標在屏幕坐標系中的位置 9 }MSG;
消息主要有三種類型:
1. 命令消息(WM_COMMAND):命令消息是程序員需要程序做某些操作的命令。凡UI對象產生的消息都是這種命令消息,可能來自菜單、加速鍵或工具欄按鈕等,都以WM_COMMAND呈現。
2. 標准窗口消息:除WM_COMMAND之處,任何以WM_開頭的消息都是這一類。標准窗口消息是系統中最為常見的消息,它是指由操作系統和控制其他窗口的窗口所使用的消息。例如CreateWindow、DestroyWindow和MoveWindow等都會激發窗口消息,以及鼠標移動、點擊,鍵盤輸入都是屬於這種消息。
3. Notification:這種消息由控件產生,為的是向其父窗口(通常是對話框窗口)通知某種情況。當一個窗口內的子控件發生了一些事情,而這些是需要通知父窗口的,此刻它就上場啦。通知消息只適用於標准的窗口控件如按鈕、列表框、組合框、編輯框,以及Windows公共控件如樹狀視圖、列表視圖等。
隊列消息 和非隊列消息
Windows中有一個系統消息隊列,對於每一個正在執行的Windows應用程序,系統為其建立一個“消息隊列”,即應用程序隊列,用來存放該程序可能創建的各種窗口的消息。
(1)隊列消息(Queued Messages)
消息會先保存在消息隊列中,通過消息循環從消息隊列中獲取消息並分發到各窗口函數去處理,如鼠標、鍵盤消息就屬於這類消息。
(2)非隊列消息(NonQueued Messages)
就是消息會直接發送到窗口函數處理,而不經過消息隊列。 如: WM_ACTIVATE, WM_SETFOCUS, WM_SETCURSOR, WM_WINDOWPOSCHANGED就屬於此類。
PostMessage與SendMessage的區別
PostMessage發送的消息是隊列消息,它會把消息Post到消息隊列中; SendMessage發送的消息是非隊列消息, 被直接送到窗口過程處理,等消息被處理后才返回。

圖3.消息隊列示意圖
為證明這一過程,我們可以改動一下上面的這個例子。
1.在Win32Test.h中添加ID_BUTTON_TEST的定義
1 #define ID_BUTTON_TEST 1002
2.在OnButtonWhite中分別用SendMessage和PostMessage發送消息
1 //按下hBtWhite時的事件 2 void OnButtonWhite() 3 { 4 g_bWhite = true; 5 InvalidateRect(g_hWnd, NULL, FALSE); //刷新窗口 6 SendMessage(g_hWnd, WM_COMMAND, ID_BUTTON_TEST, 0); 7 //PostMessage(g_hWnd, WM_COMMAND, ID_BUTTON_TEST, 0); 8 }
3.在消息循環中增加ID_BUTTON_TEST的判斷
1 while (GetMessage(&msg, NULL, 0, 0)) 2 { 3 if (LOWORD(msg.wParam) == ID_BUTTON_TEST) 4 { 5 OutputDebugString(L"This is a ID_BUTTON_TEST message."); // [BreakPoint1] 6 } 7 TranslateMessage(&msg); 8 DispatchMessage(&msg); 9 }
4.在窗口處理函數WndProc增加ID_BUTTON_TEST的判斷
1 case ID_BUTTON_TEST: 2 { 3 OutputDebugString(L"This is a ID_BUTTON_TEST message."); // [BreakPoint2] 4 } 5 break; 6 case ID_BUTTON_DRAW: 7 OnButtonWhite(); 8 break; 9 case ID_BUTTON_SWEEP: 10 OnButtonGray(); 11 break;
用斷點調試的方式我們發現,用SendMessage發送的ID_BUTTON_TEST消息只會進入BreakPoint2,而PostMessage發送的ID_BUTTON_TEST會進入到BreakPoint1和BreakPoint2。
轉自 luoweifu 《深入Windows內核——C++中的消息機制》
