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

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

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

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

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

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