Windows消息機制
1.1 基本概念
API: Application Programming Interface, 應用程序編程接口。編寫Windows應用程序本質上就是調用Windows提供的大量API函數,這些API函數大多數都在頭文件Windows.h中聲明。
SDK: Software Development Kit, 軟件開發包。指的是由廠商提供的軟件開發包,內含API函數庫,幫助文檔,示例程序,開發工具等等。
以上兩個概念都是很寬泛的,沒有特指某個特定的設備,語言或者是廠商。只要是在已經準備好的接口之上進行軟軟件開發,我們都可以把這個編程接口成爲API, 把實現該藉口的庫,使用文檔,開發工具統稱爲SDK。
窗口:Windows平臺下應用程序和用戶進行交互的圖形接口,他表現爲屏幕上的矩形區域。一個典型的窗口包含客戶區和非客戶區兩大塊,客戶區域通常用來顯示文字和圖形,是窗口的主題;非客戶區域則包含菜單欄,標題欄,系統菜單,最大最小化框等等。除了典型的窗口外,應用程序彈出的對話框和消息框也屬於窗口,Window桌面其實也是由系統創建的窗口。
句柄: 資源的標識稱爲句柄,常見的資源包括窗口,圖標,光標等等,他們在創建的時候都會被分配內存,而句柄就是這段內存的標識,可以理解成是指針的一種抽象。需要記住的幾個句柄類型:HWND(窗口句柄),HICON(圖標句柄),HBRUSH(畫刷句柄),HCURSOR(光標句柄)。
1.2 Windows API中常見宏定義
宏名 | 實際類型 | |
---|---|---|
CHAR: | char | |
WCHAR: | wchar_t | |
LPARAM: | long | |
WPARAM: | unsigned int | |
LRESULT | long | |
LPSTR: | char * | |
LPWSTR: | wchar_t * | |
LPCSTR: | const char * | |
LPCWSTR: | cong wchar_t * | |
WORD: | unsigned short | |
DWORD: | unsigned long | |
TCHAR: | 如果定義UNICODE則爲WCHAR,否則是CHAR | |
LPTSTR: | 如果定義UNICODE則爲LPWSTR,否則是LPSTR | |
LPCTSTR: | 如果定義UNICODE則爲LPCWSTR,否則是LPCSTR | |
__TEXT(): | 根據UNICODE決定是否轉換爲寬字符 | |
TEXT(): | __TEXT() |
一些跟字符有關的函數也通過宏的方法封裝了寬字符和ASCII兩種版本:
宏名 | 實際類型 |
---|---|
wsprintf | sprintfA或者sprintfW |
MessageBox | MessageBoxA或者MessageBoxW |
_tmain | main或者wmain |
_tWinMain | WinMain或者wWinMain |
其中**_tXXX系列宏在tchar.h**中定義,很多函數都由這種版本
1.3 消息和消息隊列
不同於控制檯應用程序,Windows桌面應用程序採用事件驅動機制,換句話說程序的執行不再是簡單的順序執行(配合循環和分支結構),事件驅動機制下程序可以響應事件(event)來完成特定功能,而事件驅動的底層原理就是Windows的消息機制。用戶的操作(鼠標點擊,鍵盤按下等等)都會被操作系統感知並封裝爲消息(message)再投放到應用程序的消息隊列中,而應用程序內部通過消息循環不斷從消息隊列中取出並**解析(translate)消息,之後應用程序會將消息再次分派(dispatch)**給操作系統並由操作系統調用窗口過程函數來對特定消息做出響應。
消息:Windows通過MSG結構體來封裝消息
typedef struct tagMSG{
HWND hwnd;//消息所屬窗口的句柄
UINT message;//消息常量(WM_XXX),實際上是一個整數標識
WPARAM wParam;//附加信息
LPARAM lParam;//附加信息
DWORD time;//消息產生的事件
POINT pt;//消息產生時光標位置
}MSG;
消息隊列:一個由操作系統給程序維護的數據結構,該程序所創建的窗口的全部隊列消息都會被操作系統塞進程序消息隊列中
**隊列消息和非隊列消息:**隊列消息是要進入消息隊列的,非隊列消息則直接通過窗口過程響應。
1.4 WinMain函數
windows console application的入口函數是main(),windows application的入口函數是WinMain(),二者類似都是被可執行文件中的啓動代碼調用。
int WINAPI WinMain(//WINAPI是 對__stdcall的宏定義,Win32 API均要求顯示聲明該調用約定,而標準C函數則爲__cdecl(vc默認)
HINSTANCE hInstance,//該程序當前實例的句柄
HINSTANCE hPrevInstance,//該程序上一個實例的句柄,對於Win32 app而言該參數總爲NULL,無意義
LPSTR lpCmdLine,//命令行參數,調用程序時傳入的參數會被系統存入這個參數中
int nCmdShow//程序顯示狀態(窗口最大化、最小化、隱藏之類的),和應用程序無關,由調用方指定
) ;
創建窗口:大概分爲四個步驟
- 創建窗口類
- 註冊窗口類
- 創建窗口
- 顯示更新窗口
創建窗口類:創建一個WNDCLASS結構體的實例
typedef struct _WNDCLASS{
uint style;//窗口類樣式,CS_XXX宏
WNDCLASS lpfnWndProc;//窗口過程,typedef LRESULT (CALLBACK* WNDCLASS)(HWND, UINT, WPARAM, LPARAM);,因此WNDCLASS用來定義函數指針類型,lpfnWndProc是函數指針變量
int cbClsExtra;//類附加信息,通常設置0
int cbWndExtra;//窗口附加信息,通常設置0
HANDLE hInstance;//實例句柄
HICON hIcon;//圖標句柄,使用默認圖標則爲NULL
HCURSOR hCursor;//光標句柄
HBRUSH hBrush;//背景畫刷句柄
LPCTSTR lpszMenuName;//菜單資源名
LPCTSTR lpszClassName;//窗口類名
}WNDCLASS;
HICON LoadIcon(HINSTANCE hInstance, LPCTSTR lpIconName);//加載圖標,加載系統標準圖標時第一個參數爲NULL
HCURSOR LoadCursor(HINSTANCE hInstance, LPCTSTR lpCursorName);//加載光標,加載系統標準光標時第一個參數爲NULL
HGDIOBJ GetStockObject(int fnObject);//獲取GDI資源的句柄(畫刷,畫筆,字體,調色板等),其返回值要強制轉換爲HBRUSH類型才能給背景畫刷句柄字段賦值
窗口過程是一個回調函數(CALLBACK實際上也是對__stdcall的宏定義),該函數不由應用程序調用,而是在響應特定消息的時候由操作系統調用,窗口類要保存窗口過程指針的目的也是爲了讓操作系統能夠找到回調函數。
LoadIcon函數用於加載圖標,第一個參數的意義已經介紹過了,重點是第二個參數,他是字符常量的指針,而VC中資源標識符是一個整數(IDX_XXX宏),需要用MAKEINTRESOURCE宏把ID轉換到CONST CHAR*類型。
LoadCursor函數同上類似,只不過加載的是指針資源。
GetStockObject函數會返回要求的畫筆,畫刷,字體,或者調色板的句柄,其參數形式很多,常用的如BLACK_BRUSH。
註冊窗口類:
ATOM RegisterClass(CONST WNDCLASS* lpWndClass);
該函數接受之前創建的窗口結構體爲參數,向操作系統註冊該結構體描述的窗口類,操作系統依據這個窗口類創建窗口,自然操作系統也就知道該類窗口對應的窗口過程是什麼,基於同一個窗口類創建的全部窗口公用一個窗口過程函數。
創建窗口:
HWND CreateWindow(
LPCTSTR lpClassName,//窗口類名
LPCTSTR lpWindowName,//窗口名
DWORD dwStyle,//窗口樣式,WS_XXX宏,最常用的是WS_OVERLAPPEDWINDOW,這個樣式由很多基本樣式組合(或運算)得來,有標題欄,最大最小化按鈕,可調邊框。
int x,//左上角,設置爲CW_USEDEFAULT則會使用默認左上角座標,並且忽略y
int y,//左上角
int nWidth,//寬度,設置爲CW_USEDEFAULT會使用默認寬高,並且忽略nHeight
int nHeight,//高度
HWND hWndParent,//父窗口句柄
HMENU hMenu,//菜單句柄,菜單不是窗口
HANDLE hInstance,//實例句柄
LPVOID lpParam//數據指針,指向WM_CREATE消息的lParam字段,一般設置NULL
);
顯示窗口:
BOOL ShowWindow(
HWND hwnd,
int ncmdShow//顯示樣式,SW_XXX宏
);
更新窗口:
BOOL UpdateWindow(HWND hwnd);//該函數給窗口發送WM_PAINT消息,該消息是非隊列消息
該函數用於更新窗口的客戶區(client area)。當窗口的客戶區非空時,該函數向指定窗口的窗口過程發送WM_PAINT消息,否則不發送任何消息。需要注意的是WM_PATIN消息會跳過消息隊列。
消息循環:
消息循環的作用前面已經介紹過了,這裏看看消息循環的固定實現方法:
//先看看消息循環中用到的幾個重要的API
/*
*該函數用於從調用線程的消息隊列中取回消息,該函數會等待消息隊列中有可供取回的消息再返回。
*倘若取回的消息不是WM_QUIT,該函數返回非0值,否則返回0
*當函數發生錯誤時返回值是-1,比如lpMsg指針非法或者hwnd句柄無效,調用GetLastError()查看函數內錯誤信息
*如果第二個參數是hwnd(當前窗口句柄),運行時一切正常,但退出時會出錯(消息循環永遠無法退出),因爲關閉窗口以後GetMessage會因爲句柄無效而永遠返回-1(返回0才quit)
*解決方法就是第二個參數寫0,此時GetMessage不僅會接受窗口消息還會接受線程消息,線程消息就是由PostThreadMessage()發送的消息
*WM_QUIT就是由PostQuitMessage()發送的線程消息,所以即便窗口退出了該消息還是可以接收到
*如果第二個參數非0那就必然出現在關閉窗口後返回-1的情況,此時應該退出主程序
*/
BOOL GetMessage(
LPMSG lpMsg,
HWND hWnd,//爲0時接受全部窗口的消息和線程消息
UINT wMsgFilterMin,
UINT wMsgFilterMax
);
//與之類似的另一個函數PeekMessage()同樣從消息隊列取消息,但是該函數不會等待消息被送入隊列
/*
*把虛擬鍵消息轉譯爲字符消息再重新投入線程消息隊列
*/
BOOL TranslateMessage(
const MSG *lpMsg
);
/*
*發送消息到窗口過程,通常都是通過GetMessage得到的消息
*/
LRESULT DispatchMessage(
const MSG *lpMsg
);
while((bRet = GetMessage(&msg, hwnd, 0, 0)) != 0){
if(bRet == -1){
//可能是WM_DESTROY導致窗口銷燬,程序應該退出
return -1;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
窗口過程函數:
窗口過程函數用於處理髮送給窗口的消息,該函數聲明如下:
LRESULT CALLBACK WindowProc( //自己創建的窗口過程原型與此相同即可,函數名稱隨意,另窗口類中的函數指針指向這個函數即可
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
窗口過程的一般形式:
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam){
switch(){
case WM_CHAR:
case WM_LBUTTONDOWN:
case WM_PAINT:
case WM_CLOSE:
case WM_DESTROY:
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);//如果沒有匹配的消息就調用默認窗口過程處理,這一項是必須的
}
}
1.5 第一個Windows應用程序
#include<Windows.h>
#include<stdio.h>
LRESULT CALLBACK WinMainProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
);
int WINAPI WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow
)
{
WNDCLASS wndcls;
wndcls.cbClsExtra = 0;
wndcls.cbWndExtra = 0;
wndcls.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wndcls.hCursor = LoadCursor(NULL,IDC_CROSS);
wndcls.hIcon = LoadIcon(NULL,IDC_APPSTARTING);
wndcls.lpfnWndProc = WinMainProc;
wndcls.hInstance = hInstance;
wndcls.lpszClassName = L"WinMain";//L前綴表示寬字符
wndcls.lpszMenuName = NULL;
wndcls.style = CS_HREDRAW | CS_VREDRAW;
RegisterClass(&wndcls);
HWND hwnd;
hwnd = CreateWindow(L"WinMain", L"Windows Application",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
ShowWindow(hwnd, SW_SHOWNORMAL);
UpdateWindow(hwnd);//發送WM_PAINT消息
MSG msg;
BOOL bRet;
while ((bRet=GetMessage(&msg,hwnd,0,0)) != 0) {
//如果窗口句柄不是NULL就要判斷返回值-1的情形
if (bRet == -1) {
return -1;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;//最後一次取出來的消息必然是WM_QUIT
}
LRESULT CALLBACK WinMainProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
switch (uMsg) {
case WM_CHAR:
wchar_t szChar[20];
wsprintf(szChar, L"char code is %d", wParam);
MessageBox(hwnd, szChar, L"char", 0);
break;
case WM_LBUTTONDOWN:
MessageBox(hwnd, L"mouse clicked", L"message", 0);
HDC hdc;
hdc = GetDC(hwnd);//獲取設備上下文
TextOut(hdc, 0, 50, L"Hello,World!",lstrlen(L"Hello,World!"));
ReleaseDC(hwnd, hdc);
break;
//客戶區全部或者部分無效後系統發送WM_PAINT,此外UpdateWindow也會發送該消息
//不在WM_PAINT中進行的繪圖在窗口重繪後就消失了
case WM_PAINT:
HDC hDC;
//該結構體用於存放繪製信息,只有WM_PAINT纔會攜帶繪製信息,所以BeginPaint只能響應WM_PAINT
PAINTSTRUCT ps;
hDC = BeginPaint(hwnd, &ps);//BeginPaint只能用於響應WM_PAINT消息
TextOut(hDC, 0, 0, L"http://cmiao.me", lstrlen(L"http://cmiao.me"));
EndPaint(hwnd, &ps);
break;
case WM_CLOSE:
if (IDYES == MessageBox(hwnd, L"是否真的結束?",L"message", MB_YESNO)) {
DestroyWindow(hwnd);//DestroyWindow()在關閉窗口的同時產生WM_DESTROY消息,該消息不進入隊列
}
break;
case WM_DESTROY:
PostQuitMessage(0);//該函數產生WM_QUIT(線程消息),如果GetMessage第二個參數爲某個窗口將無法接受該消息
break;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
break;
}
return 0;
}
注意,調用BeginPaint時如果客戶區的背景還沒有擦除,則BeginPaint會給窗口發送WM_ERASEBKGND,系統會使用窗口類的畫刷字段來擦除背景,該函數返回設備上下文
1.6 匈牙利命名法
遵循這種標識符命名約定有助於在開發中理解記憶變量類型和含義:
前綴 | 含義 |
---|---|
a | 數組 |
b | 布爾 |
c | 字符 |
p | 指針 |
fn | 函數 |
dw | 無符號長整型 |
h | 句柄 |
i | 整型 |
l | 長整型 |
lp | 長指針 |
s | 字符串 |
sz | 以零結尾的字符串 |
w | 無符號整型 |
x,y | 無符號整型(座標) |
cb | 字節數 |