編程思想之訊息機制,編程思想機制
編程思想之訊息機制訊息機制從一個劇情開始
路遙的《平凡的世界》因為翻拍成電視劇,又再次火起來了!我們就從這裡開始吧,其小說是以這樣一個情境開頭的:
在一個半山腰縣立高中的大院壩裡,在一個校園內的南牆根下,按班級排起了十幾個縱隊的年輕男女,各班的值日生正忙碌地給眾人分發飯菜…… 菜分為甲、乙、丙三等,甲菜以馬鈴薯、白菜、粉條為主,還有可人大肉片,乙菜沒有肉,丙菜只有清水煮白蘿蔔。主食也分為三等:白面饃,玉米面饃,高粱面饃,白、黃、黑分別代表了三種差別,學生們戲稱歐洲、亞洲、非洲。每個人的飯菜都是昨天登記好並付了飯票的,在這一長長的隊伍中自然以光景較好的富家子弟排在最前,光景一般的隨後,而那些家庭貧困少吃缺穿的學生只能在其他學生走後才姍姍來遲……
這一活生生的例子雖然看著有些悲涼(排隊打飯的情景相信曾經是學生的你一定經曆過,可能沒這般悲涼而已),卻像極了訊息機制的原理,也許發明訊息機制的靈感就是原來於這樣的生活吧!排隊的學生就是訊息佇列,值日生分發飯菜就訊息迴圈並完成訊息處理,學生吃飯就類似於事件處理。
什麼是訊息?
何為訊息?訊息就是帶有某種資訊的訊號,如你用滑鼠點擊一個視窗會產生滑鼠的訊息,鍵盤輸入字元會產生鍵盤的訊息,一個視窗大小的改變也會產生訊息。
訊息從何而來?根據馮·諾依曼的體繫結構電腦有運算器、儲存空間、控制器和輸入裝置和輸出裝置五大組件組成,訊息主要來自輸入裝置,如鍵盤、滑鼠、掃描器等,也可來自已視窗和作業系統。
訊息機制的三大要點:訊息佇列、訊息迴圈(分發)、訊息處理。其結構如下:
圖 1 :訊息機制原理
訊息佇列就是存放訊息的一種隊列,具有先進先出的特點。每產生一個訊息都會添加進訊息佇列中,在Window中訊息佇列是在作業系統中定義的。訊息佇列就如同一群排隊打飯的少男少女,這群人中光景較好的排在前面,光景較差的排在後面,可以理解成是一種優先順序隊列!要想更多的瞭解隊列的相關知識,可參見隊列。
訊息迴圈就是通過迴圈(如while)不斷地從訊息佇列中取得隊首的訊息,並將訊息分發出去。類似於上面的例子中分發飯菜值日生。
訊息處理就是在接收到訊息之後根據不同的訊息類型做出不同的處理。上面例子中值日生根據學生不同類型的飯票給他們不同等級的飯菜就是訊息處理,學生手中的飯票就是訊息所攜帶的資訊。
事件是根據接收到的訊息的具體資訊做出的特定的處理,放在代碼中是事件響應函數。上面的例子中學生拿到飯菜後吃飯就是具體的事件。
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
編程思想之迭代器
編程思想之遞迴
編程思想之回調