我們?cè)趯W(xué)習(xí)標(biāo)準(zhǔn)C++的時(shí)候,都知道每個(gè)應(yīng)用程序運(yùn)行時(shí)都會(huì)先進(jìn)入入口點(diǎn)函數(shù)main,而當(dāng)從main函數(shù)跳出時(shí)程序就結(jié)束了。在Windows編程里面,也是一樣的,只是我們的入口點(diǎn)函數(shù)不叫main,叫WinMain,這個(gè)函數(shù)不同于main,我們不能亂來,它的定義必須與聲明保持一致。
我建議各位安裝VS的時(shí)候,都順便更新幫助文檔到本地硬盤,這樣我們可以方便查找。有一點(diǎn)要注意,目前DestTop Develop的文檔基本上是英文的,做好心理準(zhǔn)備。
WinMain函數(shù)怎么寫呢,不用記的,到MSDN文檔一搜,直接復(fù)制就行了。
int CALLBACK WinMain(
_In_ HINSTANCE hInstance,
_In_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine,
_In_ int nCmdShow
);
這個(gè)函數(shù)帶了一個(gè)CALLBACK,說明它是一個(gè)回調(diào)函數(shù),那么這個(gè)CALLBACK是啥呢。我們先不管,我們先動(dòng)寫一個(gè)Windows,讓大家有一個(gè)更直觀的認(rèn)識(shí)。
1、啟動(dòng)你的開發(fā)工具,版本任意。
2、從菜單欄中依次【文件】【新建】【項(xiàng)目】,在新建項(xiàng)目窗口中,選擇Win32-Win32應(yīng)用程序。

2、點(diǎn)擊確定后,會(huì)彈出一個(gè)向?qū)?,單擊【下一步】。?xiàng)目類型選擇Windows應(yīng)用程序,附加選項(xiàng)選擇空項(xiàng)目,我們要自己編寫實(shí)現(xiàn)代碼。

3、單擊完成,項(xiàng)目創(chuàng)建成功。打開【解決方案資源管理器】,在“源文件”文件夾上右擊,從菜單中找到【添加】【新建項(xiàng)】,注意,是源文件,不要搞到頭文件去了。
在新建項(xiàng)窗口中選C++代碼文件,.cpp后綴的,不要選錯(cuò)了,選成頭文件,不然無法編譯,因?yàn)轭^文件是不參與編譯的。文件名隨便。
包含Windows.h頭文件,這個(gè)是最基本的。
#include <Windows.h>
然后是入口點(diǎn),這個(gè)我們直接把MSDN的聲明Ctrl + C,然后Ctrl + V上去就行了。
int CALLBACK WinMain(
_In_ HINSTANCE hInstance,
_In_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine,
_In_ int nCmdShow
)
{
return 0;
}
WinMain返回整型,返回0就行了,其實(shí)是進(jìn)程的退出碼,一定要0,不要寫其他,因?yàn)?表示正常退出,其他值表示非正常退出。
剛才我們提到這個(gè)函數(shù)帶了CALLBACK,那么,它是什么?很簡(jiǎn)單,你回到IDE,在CALLBACK上右擊,選【轉(zhuǎn)到定義】,看看吧。
我們看到它其實(shí)是一個(gè)宏,原型如下:
#define CALLBACK __stdcall
這時(shí)候我們發(fā)現(xiàn)了,它其實(shí)就是__stdcall,那么這個(gè)__stdcall是什么呢?它是和__cdecl關(guān)鍵字對(duì)應(yīng)的,這些資料,你網(wǎng)上搜一下就有了,如果你覺得不好理解,你不妨這樣認(rèn)為,__stdcall是專門用來調(diào)用Win API 的,反正MSDN上也是這樣說的,它其實(shí)是遵循Pascal的語法調(diào)用標(biāo)準(zhǔn),相對(duì)應(yīng)地,__cdecl是C語言的調(diào)用風(fēng)格,這個(gè)也是編譯器選項(xiàng)。 打開項(xiàng)目屬性,找到節(jié)點(diǎn)C/C++\高級(jí),然后查看一下調(diào)用約定,我們看到默認(rèn)是選擇C風(fēng)格調(diào)用的,所以,WIN API 函數(shù)才用上關(guān)鍵字__stdcall,如果你實(shí)在不懂,也沒關(guān)系,這個(gè)東西一般不影響我們寫代碼,但屬性窗口中的編譯器選項(xiàng)不要亂改,改掉了可能會(huì)導(dǎo)致一些問題。

那么CALLBACK有什么特別呢?一句話:函數(shù)不是我們調(diào)用的,但函數(shù)只定義了模型沒有具體處理,而代碼處理權(quán)在被調(diào)用者手里。怎么說呢,我們完全把它理解為.NET中的委托,我想這樣就好理解了,委托只聲明了方法的參數(shù)和返回值,并沒有具體處理代碼。
WinMain是由系統(tǒng)調(diào)用的,而WinMain中的代碼如何寫,那操作系統(tǒng)就不管了。就好像我告訴你明天有聚會(huì),一起去爬山,反正我是通知你了,至于去不去那是你決定了。
接下來看看入口點(diǎn)函數(shù)的參數(shù)。
注意,我們平時(shí)看到很多如HANDLE,HINSTANCE,HBRUSH,WPARAM。LPARAM,HICON,HWND等一大串?dāng)?shù)據(jù)類型,也許我們會(huì)說,怎么Windows開發(fā)有那么多數(shù)據(jù)類型。其實(shí)你錯(cuò)了,人總是被眼睛所看到的東西欺騙,Win API 中根本沒有什么新的數(shù)據(jù)類型,全都是標(biāo)準(zhǔn)C++中的類型,說白了,這些東西全是數(shù)字來的。如果你不信,自己可以研究一下。
它定義這些名字,只是方便使用罷了,比如下面這樣:
第一個(gè)變量指的是窗口的句柄,第二個(gè)指的是一個(gè)圖標(biāo)的句柄,第三個(gè)是當(dāng)前應(yīng)用程序的實(shí)例句柄,你看看,如果我們所有的句柄都是int,我們就無法判斷那些類型是專門用來表示光標(biāo)資源,不知道哪些類型是專用來表示位圖的句柄了,但是,如果我們這樣:
#defin HBRUSH int64
這樣就很直觀,我一看這名就知道是Brush Handlers,哦,我就明白它是專門用來管理內(nèi)存中的畫刷資源的,看,這就很明了,所以,通常這些新定義的類型或者宏,都是取有意義的名字。比如消息,它也是一個(gè)數(shù)字,如果我說115代表叫你去滾,但光是一個(gè)115誰知道你什么意思,但是,如果我們?yōu)樗x一個(gè)宏:
#define WM_GET_OUT 115
這樣,只要我SendMessage(hwnd, WM_GET_OUT, NULL, NULL),你就會(huì)收到一條消息,滾到一邊去。
WinMain的第一個(gè)參數(shù)是當(dāng)前應(yīng)用程序的實(shí)例句柄,第二個(gè)參數(shù)是前一個(gè)實(shí)例,比如我把kill.exe運(yùn)行了兩個(gè)實(shí)例,進(jìn)程列表中會(huì)有兩個(gè)kill.exe,這時(shí)候第一次運(yùn)行的實(shí)例號(hào)假設(shè)為0001,就傳遞第一個(gè)參數(shù)hInstance,第二次運(yùn)行的假設(shè)實(shí)例號(hào)為0002,就傳給了hPrevInstance參數(shù)。
lpCmdLine參數(shù)從名字上就猜到了,就是命令行參數(shù),那LPSTR是啥呢,它其實(shí)就是一個(gè)字符串,你可以跟入定義就知道了,它其實(shí)就是char*,指向char的指針,記得我上一篇文章中說的指針有創(chuàng)建數(shù)組的功能嗎?對(duì),其實(shí)這里傳入的命令行參數(shù)應(yīng)該是char[ ],這就是我在第一篇文章中要說指針的原因。
這里告訴大家一個(gè)技巧,我們?cè)趺粗滥男﹨?shù)是指針類型呢,因?yàn)椴皇撬袇?shù)都有 * 標(biāo)識(shí)。技巧還是在命名上,以后,只要我們看到P開頭的,或者LP開頭的,都是指針類型。
比如LPWSTR,LPCTSTR,LPRECT等等。
最后一個(gè)參數(shù)nCmdShow是主窗口的顯示方式。它定義了以下宏。
這個(gè)參數(shù)是操作系統(tǒng)傳入的,我們無法修改它。那么,應(yīng)用程序在運(yùn)行時(shí),是如何決定這個(gè)參數(shù)的呢?看看這個(gè),不用我介紹了吧,你一定很熟悉。

我們寫了WinMain,但我們還要在WinMain前面預(yù)先定義一個(gè)WindowProc函數(shù)。C++與C#,Java這些語言不同,你只需記住,C++編譯器的解析是從左到右,從上到下的,如果某函數(shù)要放到代碼后面來實(shí)現(xiàn),但在此之前要使用,那么你必須先聲明一下,不然編譯時(shí)會(huì)找不到。這里因?yàn)槲覀兺ǔ?huì)把WindowProc實(shí)現(xiàn)放在WinMain之后,但是在WinMain中設(shè)計(jì)窗口類時(shí)要用到它的指針,這時(shí)候,我們必須在WinMain之前聲明WindowProc。
同樣地,WindowProc的定義我們不用記,到MSDN直接抄就行了。
- #include <Windows.h>
-
- LRESULT CALLBACK WindowProc(
- _In_ HWND hwnd,
- _In_ UINT uMsg,
- _In_ WPARAM wParam,
- _In_ LPARAM lParam
- );
-
- int CALLBACK WinMain(
- _In_ HINSTANCE hInstance,
- _In_ HINSTANCE hPrevInstance,
- _In_ LPSTR lpCmdLine,
- _In_ int nCmdShow
- )
- {
- return 0;
- }
-
- LRESULT CALLBACK WindowProc(
- _In_ HWND hwnd,
- _In_ UINT uMsg,
- _In_ WPARAM wParam,
- _In_ LPARAM lParam
- )
- {
- return DefWindowProc(hwnd, uMsg, wParam, lParam);
- }
#include <Windows.h>
// 必須要進(jìn)行前導(dǎo)聲明
LRESULT CALLBACK WindowProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);
int CALLBACK WinMain(
_In_ HINSTANCE hInstance,
_In_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine,
_In_ int nCmdShow
)
{
return 0;
}
// 在WinMain后實(shí)現(xiàn)
LRESULT CALLBACK WindowProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
{
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
前導(dǎo)聲明與后面實(shí)現(xiàn)的函數(shù)的簽名必須一致,編譯才會(huì)認(rèn)為它們是同一個(gè)函數(shù)。在WindowProc中返回DefWindowProc是把我們不感興趣或者沒有處理的消息交回給操作系統(tǒng)來處理。也許你會(huì)問,函數(shù)的名字一定要叫WindowProc嗎?當(dāng)然不是了,你可以改為其他名字,如MyProc,但前提是返回值和參數(shù)的類型以及個(gè)數(shù)必須一致。
LRESULT CALLBACK MyProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
這個(gè)函數(shù)帶了CALLBACK,說明不是我們調(diào)用的,也是由操作系統(tǒng)調(diào)用的,我們?cè)谶@個(gè)函數(shù)里面對(duì)需要處理的消息進(jìn)行響應(yīng)。至于,為什么可以改函數(shù)的名字而系統(tǒng)為什么能找到這個(gè)函數(shù)呢,后面你就知道了。
設(shè)計(jì)窗口類,其實(shí)就是設(shè)計(jì)我們程序的主窗口,如有沒有標(biāo)題欄,背景什么顏色,有沒有邊框,可不可以調(diào)整大小等。要設(shè)計(jì)窗口類,我們用到一個(gè)結(jié)構(gòu)——
通常情況下,我們用WNDCLASS就可以了,當(dāng)然還有一個(gè)WNDCLASSEX的擴(kuò)展結(jié)構(gòu),在API里面,凡是看到EX結(jié)尾的都是擴(kuò)展的意思,比如CreateWindowEx就是CreateWindow的擴(kuò)展函數(shù)。
第一個(gè)成員是窗口的類樣式,注意,不要和窗口樣式(WS_xxxxx)混淆了,這里指的是這個(gè)窗口類的特征,不是窗口的外觀特征,這兩個(gè)style是不一樣的。
它的值可以參考MSDN,通常我們只需要兩個(gè)就可以了——CS_HREDRAW | CS_VREDRAW,從名字就看出來了,就是同時(shí)具備水平重畫和垂直重畫。因?yàn)楫?dāng)我們的窗口顯示的時(shí)候,被其他窗口擋住后重新顯示,或者大小調(diào)整后,窗口都要發(fā)生繪制,就像我們?cè)诩埳贤盔f一樣,每次窗口的變化都會(huì)“粉刷”一遍,并發(fā)送WM_PAINT消息。
lpfnWndProc參數(shù)就是用來設(shè)置你用哪個(gè)WindowProc來處理消息,前面我說過,我們只要不更改回調(diào)函數(shù)的返回值和參數(shù)的類型和順序,就可以隨意設(shè)置函數(shù)的名字,那為什么系統(tǒng)可以找到我們用的回調(diào)函數(shù)呢,對(duì)的,就是通過lpfnWndProc傳進(jìn)去的,它是一個(gè)函數(shù)指針,也就是它里面保存的是我們定義的WindowProc的入口地址,使用很簡(jiǎn)單,我們只需要把函數(shù)的名字傳給它就可以了。
cbClsExtra和cbWndExtra通常不需要,設(shè)為0就OK。hInstance是當(dāng)前應(yīng)用程序的實(shí)例句柄,從WinMain的hInstance參數(shù)中可以得到。hIcon和hCursor就不用我說了,看名字就知道了。
hbrBackground是窗口的背景色,你也可以不設(shè)置,但在處理WM_PAINT消息時(shí)必須繪制窗口背景。也可以直接用系統(tǒng)定義的顏色,MSDN為我們列出這些值,大家不用記,直接到MSDN拿來用就行了,這些都比較好理解,看名字就知道了。
lpszMenuName指的是菜單的ID,沒有菜單就NULL,lpszClassName就是我們要向系統(tǒng)注冊(cè)的類名,字符,不能與系統(tǒng)已存在的類名沖突,如“BUTTON”類。
所以,在WinMain中設(shè)計(jì)窗口類。
// 類名
WCHAR* cls_Name = L"My Class";
// 設(shè)計(jì)窗口類
WNDCLASS wc;
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
wc.lpfnWndProc = WindowProc;
wc.lpszClassName = cls_Name;
wc.hInstance = hInstance;
窗口類設(shè)計(jì)完成后,不要忘了向系統(tǒng)注冊(cè),這樣系統(tǒng)才能知道有這個(gè)窗口類的存在。向操作系統(tǒng)注冊(cè)窗口類,使用RegisterClass函數(shù),它的參數(shù)就是一個(gè)指向WNDCLASS結(jié)構(gòu)體的指針,所以我們傳遞的時(shí)候,要加上&符號(hào)。
// 注冊(cè)窗口類
RegisterClass(&wc);
窗口類注冊(cè)完成后,就應(yīng)該創(chuàng)建窗口,然后顯示窗口,調(diào)用CreateWindow創(chuàng)建窗口,如果成功,會(huì)返回一個(gè)窗口的句柄,我們對(duì)這個(gè)窗口的操作都要用到這個(gè)句柄。什么是句柄呢?其實(shí)它就是一串?dāng)?shù)字,只是一個(gè)標(biāo)識(shí)而已,內(nèi)存中會(huì)存在各種資源,如圖標(biāo)、文本等,為了可以有效標(biāo)識(shí)這些資源,每一個(gè)資源都有其唯一的標(biāo)識(shí)符,這樣,通過查找標(biāo)識(shí)符,就可以知道某個(gè)資源存在于內(nèi)存中哪一塊地址中,就好比你出身的時(shí)候,長(zhǎng)輩都要為你取個(gè)名字,你說名字用來干嗎?名字就是用來標(biāo)識(shí)你的,不然,你見到A叫小明,遇到B又叫小明,那誰知道哪個(gè)才是小明?。烤秃孟衲闵洗髮W(xué)去報(bào)到號(hào),會(huì)為你分配一個(gè)可以在本校學(xué)生中唯一標(biāo)識(shí)你的學(xué)號(hào),所有學(xué)生的學(xué)號(hào)都是不同的,這樣,只要通過索引學(xué)號(hào),就可以找到你的資料。
CreateWindow函數(shù)返回一個(gè)HWND類型,它就是窗口類的句柄。
-
- HWND hwnd = CreateWindow(
- cls_Name,
- L"我的應(yīng)用程序",
- WS_OVERLAPPEDWINDOW,
- 38,
- 20,
- 480,
- 250,
- NULL,
- NULL,
- hInstance,
- NULL);
- if(hwnd == NULL)
- return 0;
// 創(chuàng)建窗口
HWND hwnd = CreateWindow(
cls_Name, //類名,要和剛才注冊(cè)的一致
L"我的應(yīng)用程序", //窗口標(biāo)題文字
WS_OVERLAPPEDWINDOW, //窗口外觀樣式
38, //窗口相對(duì)于父級(jí)的X坐標(biāo)
20, //窗口相對(duì)于父級(jí)的Y坐標(biāo)
480, //窗口的寬度
250, //窗口的高度
NULL, //沒有父窗口,為NULL
NULL, //沒有菜單,為NULL
hInstance, //當(dāng)前應(yīng)用程序的實(shí)例句柄
NULL); //沒有附加數(shù)據(jù),為NULL
if(hwnd == NULL) //檢查窗口是否創(chuàng)建成功
return 0;
窗外觀的樣式都是WS_打頭的,是Window Style的縮寫,這個(gè)我就不說了,MSDN上全有了。
窗口創(chuàng)建后,就要顯示它,就像我們的產(chǎn)品做了,要向客戶展示。顯示窗口調(diào)用ShowWindow函數(shù)。
// 顯示窗口
ShowWindow(hwnd, SW_SHOW);
既然要顯示窗口了,那么ShowWindow的第一個(gè)參數(shù)就是剛才創(chuàng)建的窗口的句柄,第二個(gè)參數(shù)控制窗口如何顯示,你可以從SW_XXXX中選一個(gè),也可以用WinMain傳進(jìn)來的參數(shù),還記得WinMain的最后一個(gè)參數(shù)嗎?
為什么更新窗口這一步可有可無呢?因?yàn)橹灰绦蛟谶\(yùn)行著,只要不是最小化,只要窗口是可見的,那么,我們的應(yīng)用程序會(huì)不斷接收到WM_PAINT通知。這里先不說,后面你會(huì)明白的。好了,更新窗口,當(dāng)然是調(diào)用UpdateWindow函數(shù)。
// 更新窗口
UpdateWindow(hwnd);
Windows操作系統(tǒng)是基于消息控制機(jī)制的,用戶與系統(tǒng)之間的交互,程序與系統(tǒng)之間的交互,都是通過發(fā)送和接收消息來完成的。就好像軍隊(duì)一樣,命令一旦傳達(dá),就要執(zhí)行,當(dāng)然,我們的應(yīng)用程序和軍隊(duì)不一樣,我們收到指令不一要執(zhí)行,我們是可以選擇性地執(zhí)行。
我們知道,代碼是不斷往前執(zhí)行的,像我們剛才寫的WinMain函數(shù)一樣,如果你現(xiàn)在運(yùn)行程序,你會(huì)發(fā)現(xiàn)什么都沒有,是不是程序不能運(yùn)行呢,不是,其實(shí)程序是運(yùn)行了,只是它馬上結(jié)束了,只要程序執(zhí)行跳出了WinMain的右大括號(hào),程序就會(huì)結(jié)束了。那么,要如何讓程序不結(jié)束了,可能大家注意到我們?cè)贑程序中可以用一個(gè)getchar()函數(shù)來等到用戶輸入,這樣程序就人停在那里,直到用戶輸入內(nèi)容。但我們的窗口應(yīng)用不能這樣做,因?yàn)橛脩粲锌赡苓M(jìn)行其他操作,如最小化窗口,移動(dòng)窗口,改變窗口大小,或者點(diǎn)擊窗口上的按鈕等。因此,我們不能簡(jiǎn)地弄一個(gè)getchar在那里,這樣就無法響應(yīng)用戶的其他操作了。
可以讓程序留在某處不結(jié)束的另一個(gè)方法就是使用循環(huán),而且是死循環(huán),這樣程序才會(huì)永久地停在某個(gè)地方,但這個(gè)死循環(huán)必須具有跳出的條件,不然你的程序會(huì)永久執(zhí)行,直達(dá)停電或者把電腦砸了。
這樣消息循環(huán)就出現(xiàn)了,只要有與用戶交互,系統(tǒng)人不斷地向應(yīng)用程序發(fā)送消息通知,因?yàn)檫@些消息是不定時(shí)不斷發(fā)送的,必須有一個(gè)綬沖區(qū)來存放,就好像你去銀行辦理手續(xù)要排隊(duì)一樣,我們從最前端取出一條一條消息處理,后面新發(fā)送的消息會(huì)一直在排隊(duì),直到把所有消息處理完,這就是消息隊(duì)列。
要取出一條消息,調(diào)用GetMessage函數(shù)。函數(shù)會(huì)傳入一個(gè)MSG結(jié)構(gòu)體的指針,當(dāng)收到消息,會(huì)填充MSG結(jié)構(gòu)體中的成員變量,這樣我們就知道我們的應(yīng)用程序收到什么消息了,直到GetMessage函數(shù)取不到消息,條件不成立,循環(huán)跳出,這時(shí)應(yīng)用程序就退出。MSG的定義如下:
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG, *PMSG, *LPMSG;
hwnd不用說了,就是窗口句柄,哪個(gè)窗口的句柄?還記得WindowProc回調(diào)函數(shù)嗎?你把這個(gè)函數(shù)交給了誰來處理,hwnd就是誰的句柄,比如我們上面的代碼,我們是把WindowProc賦給了新注冊(cè)的窗口類,并創(chuàng)建了主窗口,返回一個(gè)表示主窗口的句柄,所以,這里MSG中的hwnd指的就是我們的主窗口。
message就是我們接收到的消息,看到,它是一個(gè)數(shù)字,無符號(hào)整型,所以我們操作的所有消息都是數(shù)字來的。wParam和lParam是消息的附加參數(shù),其實(shí)也是數(shù)值來的。通常,lParam指示消息的處理結(jié)果,不同消息的結(jié)果(返回值)不同,具體可參閱MSDN。
有了一個(gè)整型的值來表示消息,我們?yōu)槭裁催€需要附加參數(shù)呢?你不妨想一下,如果接收一條WM_LBUTTONDOWN消息,即鼠標(biāo)左鍵按下時(shí)發(fā)送的通知消息,那么,我們不僅知道左鍵按下這件事,我們更感趣的是,鼠標(biāo)在屏幕上的哪個(gè)坐標(biāo)處按下左鍵,按了幾下,這時(shí)候,你公憑一條WM_LBUTTONDOWN消息是無法傳遞這么多消息的。可能我們需要把按下左鍵時(shí)的坐標(biāo)放入wParam參數(shù)中;最典型的就是WM_COMMAND消息,因?yàn)橹灰闶褂貌藛?,點(diǎn)擊按鈕都會(huì)發(fā)送這樣一條消息,那么我怎么知道用戶點(diǎn)了哪個(gè)按鈕呢?如果窗口中只有一個(gè)按鈕,那好辦,用戶肯定單擊了它,但是,如果窗口上有10個(gè)按鈕呢?而每一個(gè)按鈕被單擊都會(huì)發(fā)送WM_COMMAND消息,你能知道用戶點(diǎn)擊了哪個(gè)按鈕嗎?所以,我們要把用戶點(diǎn)擊了的那個(gè)按鈕的句柄存到lParam參數(shù)中,這樣一來,我們就可以判斷出用戶到底點(diǎn)擊了哪個(gè)按鈕了。
GetMessage函數(shù)聲明如下:
BOOL WINAPI GetMessage(
_Out_ LPMSG lpMsg,
_In_opt_ HWND hWnd,
_In_ UINT wMsgFilterMin,
_In_ UINT wMsgFilterMax
);
這個(gè)函數(shù)在定義時(shí)帶了一個(gè)WINAPI,現(xiàn)在,按照前面我說的方法,你應(yīng)該猜到,它就是一個(gè)宏,而真實(shí)的值是__stdcall,前文中說過了。
第一個(gè)參數(shù)是以LP開頭,還記得嗎,我說過的,你應(yīng)該想到它就是 MSG* ,一個(gè)指向MSG結(jié)構(gòu)的指針。第二個(gè)參數(shù)是句柄,通常我們用NULL,因?yàn)槲覀儠?huì)捕捉整個(gè)應(yīng)用程序的消息。后面兩個(gè)參數(shù)是用來過濾消息的,指定哪個(gè)范圍內(nèi)的消息我接收,在此范圍之外的消息我拒收,如果不過濾就全設(shè)為0.。返回值就不說了,自己看。
// 消息循環(huán)
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
TranslateMessage是用于轉(zhuǎn)換按鍵信息的,因?yàn)殒I盤按下和彈起會(huì)發(fā)送WM_KEYDOWN和WM_KEYUP消息,但如果我們只想知道用戶輸了哪些字符,這個(gè)函數(shù)可以把這些消息轉(zhuǎn)換為WM_CHAR消息,它表示的就是鍵盤按下的那個(gè)鍵的字符,如“A”,這樣我們處理起來就更方便了。
DispatchMessage函數(shù)是必須調(diào)用的,它的功能就相當(dāng)于一根傳送帶,每收到一條消息,DispatchMessage函數(shù)負(fù)責(zé)把消息傳到WindowProc讓我們的代碼來處理,如果不調(diào)用這個(gè)函數(shù),我們定義的WindowProc就永遠(yuǎn)接收不到消息,你就不能做消息響應(yīng)了,你的程序就只能從運(yùn)行就開始死掉了,沒有響應(yīng)。
其實(shí)現(xiàn)在我們的應(yīng)用程序是可以運(yùn)行了,因?yàn)樵赪indowProc中我們調(diào)用了DefWindowProc,函數(shù),消息我們不作任何處理,又把控制權(quán)路由回到操作系統(tǒng)來默認(rèn)處理,所以,整個(gè)過程中,我們現(xiàn)在的消息循環(huán)是成立的,只不過我們不做任何響應(yīng)罷了。
好的,現(xiàn)在我把完整的代碼貼一下,方便你把前面我們說的內(nèi)容串聯(lián)起來。
- #include <Windows.h>
-
- LRESULT CALLBACK WindowProc(
- _In_ HWND hwnd,
- _In_ UINT uMsg,
- _In_ WPARAM wParam,
- _In_ LPARAM lParam
- );
-
-
- int CALLBACK WinMain(
- _In_ HINSTANCE hInstance,
- _In_ HINSTANCE hPrevInstance,
- _In_ LPSTR lpCmdLine,
- _In_ int nCmdShow
- )
- {
-
- WCHAR* cls_Name = L"My Class";
-
- WNDCLASS wc;
- wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
- wc.lpfnWndProc = WindowProc;
- wc.lpszClassName = cls_Name;
- wc.hInstance = hInstance;
-
- RegisterClass(&wc);
-
-
- HWND hwnd = CreateWindow(
- cls_Name,
- L"我的應(yīng)用程序",
- WS_OVERLAPPEDWINDOW,
- 38,
- 20,
- 480,
- 250,
- NULL,
- NULL,
- hInstance,
- NULL);
- if(hwnd == NULL)
- return 0;
-
-
- ShowWindow(hwnd, SW_SHOW);
-
-
- UpdateWindow(hwnd);
-
-
- MSG msg;
- while(GetMessage(&msg, NULL, 0, 0))
- {
- TranslateMessage(&msg);
- DispatchMessage(&msg);
- }
- return 0;
- }
-
- LRESULT CALLBACK WindowProc(
- _In_ HWND hwnd,
- _In_ UINT uMsg,
- _In_ WPARAM wParam,
- _In_ LPARAM lParam
- )
- {
- return DefWindowProc(hwnd, uMsg, wParam, lParam);
- }
#include <Windows.h>
// 必須要進(jìn)行前導(dǎo)聲明
LRESULT CALLBACK WindowProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);
// 程序入口點(diǎn)
int CALLBACK WinMain(
_In_ HINSTANCE hInstance,
_In_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine,
_In_ int nCmdShow
)
{
// 類名
WCHAR* cls_Name = L"My Class";
// 設(shè)計(jì)窗口類
WNDCLASS wc;
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
wc.lpfnWndProc = WindowProc;
wc.lpszClassName = cls_Name;
wc.hInstance = hInstance;
// 注冊(cè)窗口類
RegisterClass(&wc);
// 創(chuàng)建窗口
HWND hwnd = CreateWindow(
cls_Name, //類名,要和剛才注冊(cè)的一致
L"我的應(yīng)用程序", //窗口標(biāo)題文字
WS_OVERLAPPEDWINDOW, //窗口外觀樣式
38, //窗口相對(duì)于父級(jí)的X坐標(biāo)
20, //窗口相對(duì)于父級(jí)的Y坐標(biāo)
480, //窗口的寬度
250, //窗口的高度
NULL, //沒有父窗口,為NULL
NULL, //沒有菜單,為NULL
hInstance, //當(dāng)前應(yīng)用程序的實(shí)例句柄
NULL); //沒有附加數(shù)據(jù),為NULL
if(hwnd == NULL) //檢查窗口是否創(chuàng)建成功
return 0;
// 顯示窗口
ShowWindow(hwnd, SW_SHOW);
// 更新窗口
UpdateWindow(hwnd);
// 消息循環(huán)
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
// 在WinMain后實(shí)現(xiàn)
LRESULT CALLBACK WindowProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
{
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
所有代碼看上去貌似很正常,也遵守了流程,設(shè)計(jì)窗口類,注冊(cè)窗口類,創(chuàng)建窗口,顯示窗口,更新窗口,消息循環(huán)。是吧,這段代碼看上去毫無破綻,運(yùn)行應(yīng)該沒問題吧。好,如果你如此自信,那就試試吧。
按下F5試試運(yùn)行。 哈哈,結(jié)果會(huì)讓很多人失望,很多初學(xué)者就是這樣,一切看起來好像正常,于是有人開始罵VC是垃圾,是編譯器有bug,也有人開始想放棄了,媽的,這么難,不學(xué)了。人啊,總是這樣,老指責(zé)別人的問題,從不在自己身上找問題,是真的VC的bug嗎?
我前面說了,這段代碼貌似很正常,呵呵,你看到問題在哪嗎?給你兩分鐘來找錯(cuò)。我提示一下,這個(gè)程序沒有運(yùn)行是因?yàn)橹鞔翱诟揪蜎]有創(chuàng)建,因?yàn)槲以诖a里面做了判斷,如果窗口順柄hwnd為NULL,就退出,現(xiàn)在程序一運(yùn)行就退出了,明顯是窗口創(chuàng)建失敗。
…………
好了,不用找了,很多人找不出來,尤其是許多初學(xué)者,不少人找了一遍又一遍,都說沒有錯(cuò)誤,至少代碼提示沒說有錯(cuò),編譯運(yùn)行也沒報(bào)錯(cuò),所以不少人自信地說,代碼沒錯(cuò)。
其實(shí)你是對(duì)的,代碼確實(shí)沒有錯(cuò),而問題就出在WNDCLASS結(jié)構(gòu)上,認(rèn)真看一下MSDN上有關(guān)RegisterClass函數(shù)說明中的一句話,這句話很多人沒注意到,但它很關(guān)鍵。
You must fill the structure with the appropriate class attributes before passing it to the function.
現(xiàn)在你明白了吧,還不清楚?沒關(guān)系,看看我把代碼這樣改一下你就知道了。
現(xiàn)在,你運(yùn)行一下,你一定能看到窗口。
但現(xiàn)在你對(duì)窗口無法進(jìn)行操作,因?yàn)楹罄m(xù)的代碼還沒完成。
為什么現(xiàn)在又可以了呢?MSDN那句話的意思就是說我們?cè)谧?cè)窗口類之前必須填充WNDCLASS結(jié)構(gòu)體,何為填充,就是要為結(jié)構(gòu)的所有成員賦值,就算不需要你也要為它賦一個(gè)NULL或0,因?yàn)榻Y(jié)構(gòu)在創(chuàng)建時(shí)沒有對(duì)成員進(jìn)行初始化,這就導(dǎo)致變量無法正確的分配內(nèi)存,最后注冊(cè)失敗。
那么,如果一個(gè)結(jié)構(gòu)體成員很多,而我只需要用到其中三個(gè),其他的也要初始化,是不是很麻煩,是的,除了為每個(gè)成員賦值,還有一種較簡(jiǎn)單的方法,就是在聲明變量時(shí)給它賦一對(duì)大括號(hào),里面放置結(jié)構(gòu)體的應(yīng)該分配內(nèi)存的大小,如:
// 設(shè)計(jì)窗口類
WNDCLASS wc = { sizeof(WNDCLASS) };
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
wc.lpfnWndProc = WindowProc;
wc.lpszClassName = cls_Name;
wc.hInstance = hInstance;
這樣一來,我們也發(fā)現(xiàn),窗口也可以成功創(chuàng)建。
我們還可以更簡(jiǎn)單,直接把sizeof也去掉,在聲明變量時(shí),直接賦一對(duì)空的大括號(hào)就行了,就如這樣。
WNDCLASS wc = { };
這樣寫更簡(jiǎn)單,窗口類同樣可以正常注冊(cè)。大括號(hào)代表的是代碼塊,這樣,結(jié)構(gòu)體有了一個(gè)初值,因此它會(huì)按照結(jié)構(gòu)體的大小分配了相應(yīng)的內(nèi)存。
為什么會(huì)這樣呢?這里涉及到一個(gè)關(guān)于結(jié)構(gòu)體的一個(gè)很有趣的賦值方式。我們先放下我們這個(gè)例子,下面我寫一個(gè)簡(jiǎn)單的例子,你就明白了。
- #include <stdio.h>
- typedef struct rectStruct
- {
- int x;
- int y;
- int width;
- int height;
- } RECT, *PRECT;
-
- void main()
- {
- RECT rect = { 0, 0, 20, 30 };
- printf("矩形的坐標(biāo)是:%d, %d\n矩形的大?。?d , %d", rect.x, rect.y, rect.width, rect.height);
- getchar();
- }
#include <stdio.h>
typedef struct rectStruct
{
int x;
int y;
int width;
int height;
} RECT, *PRECT;
void main()
{
RECT rect = { 0, 0, 20, 30 };
printf("矩形的坐標(biāo)是:%d, %d\n矩形的大?。?d , %d", rect.x, rect.y, rect.width, rect.height);
getchar();
}
在本例中,我們定義了一個(gè)表示矩形的結(jié)構(gòu)體 RECT ,它有四個(gè)成員,分別橫坐標(biāo),縱坐標(biāo),寬度,高度,但是,我們?cè)诼暶骱唾x值中,我們只用了一對(duì)大括號(hào),把每個(gè)成員的值,按照定義的順序依次寫到大括號(hào)中,即{ 0, 0, 20, 30 },x的值為0,y的值為0,width為20,height的值為30。
也就是說,我們可以通過這種簡(jiǎn)單的方法向結(jié)構(gòu)變量賦值,注意值的順序要和成員變量定義的順序相同。
現(xiàn)在,回到我們的Windows程序來,我們明白了這種賦值方式,對(duì)于 WNDCLASS wc = { } 就不難理解了,這樣雖然大括號(hào)里面是空的,其實(shí)它已經(jīng)把變量初始化了,都賦了默認(rèn)值,這樣一來,就可以正確分配內(nèi)存了。
通常情況下,當(dāng)我們的主窗口關(guān)閉后,應(yīng)用程序應(yīng)該退出(木馬程序除外),但是,我們剛才運(yùn)行后發(fā)現(xiàn),為什么我的窗口關(guān)了,但程序不退出呢?前面我說了,要退出程序,就要先跳出消息循環(huán),和關(guān)閉哪個(gè)窗口無關(guān)。因此,我們要解決兩個(gè)問題:
1、如果跳出消息循環(huán);
2、什么時(shí)候退出程序。
其實(shí)兩個(gè)問題是可以合并到一起解決。
首先要知道,當(dāng)窗口被關(guān)閉,為窗口所分配的內(nèi)存會(huì)被銷毀,同時(shí),我們會(huì)收到一條WM_DESTROY消息,因而,我們只要在收到這條消息時(shí)調(diào)用PostQuitMessage函數(shù),這個(gè)函數(shù)提交一條WM_QUIT消息,而在消息循環(huán)中,GetMessage函數(shù)是不接收WM_QUIT消息的,這樣一來,GetMessage返回FALSE,就可以跳出消息循環(huán)了,這樣應(yīng)用程序就可以退出了。
所以,我們要做的就是捕捉WM_DESTROY消息,然后PostQuitMessage.
// 在WinMain后實(shí)現(xiàn)
LRESULT CALLBACK WindowProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
{
switch(uMsg)
{
case WM_DESTROY:
{
PostQuitMessage(0);
return 0;
}
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
我們會(huì)收到很多消息,所以用switch判斷一下是不是WM_DESTROY消息,如果是,退出應(yīng)用程序。
好了,這樣,我們一個(gè)完整的Windows應(yīng)用程序就做好了。

下面是完整的代碼清單。
|