標籤:編程思想 訊息機制 c++
《編程思想之訊息機制》一文中我們講了訊息的相關概念和訊息機制的類比,本文將進一步聊聊C++中的訊息機制。
從簡單例子探析核心原理
在講之前,我們先看一個簡單例子:建立一個視窗和兩個按鈕,用來控制視窗的背景顏色。其效果如下:
圖 2 :
Win32Test.h
#pragma once#include <windows.h>#include <atltypes.h>#include <tchar.h>//資源ID#define ID_BUTTON_DRAW 1000#define ID_BUTTON_SWEEP 1001// 註冊視窗類別ATOM AppRegisterClass(HINSTANCE hInstance);// 初始化視窗BOOL InitInstance(HINSTANCE, int);// 訊息處理函數(又叫視窗過程)LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);// (白色背景)按鈕事件void OnButtonWhite();// (灰色背景)按鈕事件void OnButtonGray();// 繪製事件void OnDraw(HDC hdc);
Win32Test.cpp
#include "stdafx.h"#include "Win32Test.h"//字元數組長度#define MAX_LOADSTRING 100//全域變數HINSTANCE hInst; // 當前執行個體TCHAR g_szTitle[MAX_LOADSTRING] = TEXT("Message process"); // 視窗標題TCHAR g_szWindowClass[MAX_LOADSTRING] = TEXT("AppTest"); // 視窗類別的名稱HWND g_hWnd; // 視窗控制代碼bool g_bWhite = false; // 是否為白色背景//WinMain入口函數int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow){ UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); // 註冊視窗類別 if(!AppRegisterClass(hInstance)) { return (FALSE); } // 初始化應用程式視窗 if (!InitInstance (hInstance, nCmdShow)) { return FALSE; } // 訊息迴圈 MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (int) msg.wParam;}// 註冊視窗類別ATOM AppRegisterClass(HINSTANCE hInstance){ WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION); wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); wcex.lpszMenuName = NULL; wcex.lpszClassName = g_szWindowClass; wcex.hIconSm = NULL; return RegisterClassEx(&wcex);}// 儲存執行個體化控制代碼並建立主視窗BOOL InitInstance(HINSTANCE hInstance, int nCmdShow){ hInst = hInstance; // 儲存handle到全域變數 g_hWnd = CreateWindow(g_szWindowClass, g_szTitle, WS_OVERLAPPEDWINDOW, 0, 0, 400, 300, NULL, NULL, hInstance, NULL); // 建立按鈕 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); 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); if (!g_hWnd) { return FALSE; } ShowWindow(g_hWnd, nCmdShow); UpdateWindow(g_hWnd); return TRUE;}// (視窗)訊息處理LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam){ int wmId, wmEvent; PAINTSTRUCT ps; HDC hdc; switch (message) { case WM_COMMAND: wmId = LOWORD(wParam); //wmEvent = HIWORD(wParam); switch (wmId) { case ID_BUTTON_DRAW: OnButtonWhite(); break; case ID_BUTTON_SWEEP: OnButtonGray(); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } break; case WM_PAINT: hdc = BeginPaint(hWnd, &ps); OnDraw(hdc); EndPaint(hWnd, &ps); break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0;}//事件處理//按下hBtWhite時的事件void OnButtonWhite(){ g_bWhite = true; InvalidateRect(g_hWnd, NULL, FALSE); //重新整理視窗}//按下hBtGray時的事件void OnButtonGray(){ g_bWhite = false; InvalidateRect(g_hWnd, NULL, FALSE); //重新整理視窗}//繪製事件(每次重新整理時重新繪製映像)void OnDraw(HDC hdc){ POINT oldPoint; SetViewportOrgEx(hdc, 0, 0, &oldPoint); RECT rcView; GetWindowRect(g_hWnd, &rcView); // 獲得控制代碼的畫布大小 HBRUSH hbrWhite = (HBRUSH)GetStockObject(WHITE_BRUSH); HBRUSH hbrGray = (HBRUSH)GetStockObject(GRAY_BRUSH); if (g_bWhite) { FillRect(hdc, &rcView, hbrWhite); } else { FillRect(hdc, &rcView, hbrGray); } SetViewportOrgEx(hdc, oldPoint.x, oldPoint.y, NULL);}
在上面這個例子中,訊息的流經過程如下:
圖 3 :訊息的流經過程
這與《編程思想之訊息機制》中圖1(訊息機制原理)是相吻合的,這就是Windows訊息機制的核心部分,也是Windows API開發的核心部分。
Windows系統和Windows下的程式都是以訊息為基礎,以事件為驅動。
RegisterClassEx的作用是註冊一個視窗,在調用CreateWindow建立一個視窗前必須向windows系統註冊獲惟一的標識。
while (GetMessage(&msg, NULL, 0, 0)){ TranslateMessage(&msg); DispatchMessage(&msg);}
這個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等。訊息的定義如下:
typedef struct tagMsg{ HWND hwnd; //接受該訊息的視窗控制代碼 UINT message; //訊息常量標識符,也就是我們通常所說的訊息編號 WPARAM wParam; //32位訊息的特定附加資訊,確切含義依賴於訊息值 LPARAM lParam; //32位訊息的特定附加資訊,確切含義依賴於訊息值 DWORD time; //訊息建立時的時間 POINT pt; //訊息建立時的滑鼠/游標在螢幕座標系中的位置}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發送的訊息是非隊列訊息, 被直接送到視窗過程處理,等訊息被處理後才返回。
圖 4 :訊息佇列
為證明這一過程,我們可以改動一下上面的這個例子。
1.在Win32Test.h中添加ID_BUTTON_TEST的定義
#define ID_BUTTON_TEST 1002
2.在OnButtonWhite中分別用SendMessage和PostMessage發送訊息
//按下hBtWhite時的事件
void OnButtonWhite(){ g_bWhite = true; InvalidateRect(g_hWnd, NULL, FALSE); //重新整理視窗 SendMessage(g_hWnd, WM_COMMAND, ID_BUTTON_TEST, 0); //PostMessage(g_hWnd, WM_COMMAND, ID_BUTTON_TEST, 0);}
3.在訊息迴圈中增加ID_BUTTON_TEST的判斷
while (GetMessage(&msg, NULL, 0, 0)) { if (LOWORD(msg.wParam) == ID_BUTTON_TEST) { OutputDebugString(L"This is a ID_BUTTON_TEST message."); // [BreakPoint1] } TranslateMessage(&msg); DispatchMessage(&msg); }
4.在視窗處理函數WndProc增加ID_BUTTON_TEST的判斷
case ID_BUTTON_TEST: { OutputDebugString(L"This is a ID_BUTTON_TEST message."); // [BreakPoint2] } break;case ID_BUTTON_DRAW: OnButtonWhite(); break;case ID_BUTTON_SWEEP: OnButtonGray(); break;
用斷點調試的方式我們發現,用SendMessage發送的ID_BUTTON_TEST訊息只會進入BreakPoint2,而PostMessage發送的ID_BUTTON_TEST會進入到BreakPoint1和BreakPoint2。
如果您有什麼疑惑和想法,請在評論處給予反饋,您的反饋就是最好的測評師!由於本人技術和能力有限,如果本博文有錯誤或不足之處,敬請諒解並給出您寶貴的建議!
========================編程思想系列文章回顧========================
編程思想之訊息機制
編程思想之日誌記錄
編程思想之異常處理
編程思想之Regex
編程思想之迭代器
編程思想之遞迴
編程思想之回調
深入Windows核心——C++中的訊息機制