0%

学习win32编程(1)--创建窗口

期中考试结束了,又回想起了期中考试前花了八天时间搞出来的小脚本,决定完善一下,增加点稳定性。

为什么要学习win32编程

脚本当时最大的问题就是浏览器最小化之后,视频的自动切换下一集的功能出现了故障,导致可能花了4,5集的时间结果只播放了一集。花了一天时间研究了一下chrome浏览器的工作原理,发现了setInterval函数的延迟不由JavaScript引擎完成而是由浏览器的一个定时器线程完成的,浏览器最小化之后那个线程停止运行导致setInterval函数延迟出错。具体的工作原理参见我的另一篇文章浅析chrome的工作原理

于是决定修复它。想了两种思路,第一种是借助Chrome的扩展程序,扩展程序可以获取活动选项卡,而且浏览器最小化后不会停止运行。但是还是没法保证setInterval函数在浏览器最小化之后仍然能够正常运行。第二种思路就是借助WebAssembly在js代码中嵌入底层c++代码,借助Windows的API来实现对浏览器相关线程的控制,这样就可以避免定时器线程暂停的问题。当然这个也是一个初步的思路,可行性只有在具体实践之后才能知道。(可能还需要借助扩展程序完成)

既然要在底层通过c++来操控浏览器,那win32编程是绕不过了。但也只是初步的把一些需要用到的知识学一下,顺便了解一下windows系统的运作原理等等。顺便把C语言也复习复习吧。ok,那就开始。

最基本的操作–创建窗口

0x00 引入

我们就先来一个最简单的windows应用程序。

1
2
3
4
5
#include <windows.h>
int main(){
MessageBox(NULL,"Hello Windows!","Hello Windows!",MB_OK);
return 0;
}

这段代码运行后会生成一个命令行界面和对话框,图标为Windows默认图标,标题为”Hello Windows!”,内容为”Hello Windows!”,再加上一个”确定”按钮。

恭喜我们成功生成了一个命令行外的东西–对话框!我们走出了摆脱命令行的巨大一步。

当然这个程序非常简单,而且说到底这个程序依然是属于c++程序,不能算是Windows程序,因为他的入口点函数依然是main函数,main函数是控制台应用程序的入口点。所以程序运行的时候依然会出现一个shell的界面。

那么我们如何不让他生成这个命令行呢,我们需要换一种思路了。

0x01 创建入口点函数WinMain

Windows编程中,我们需要使用WinMain函数来创建程序入口点。于是我们把上述代码修改一下:

1
2
3
4
5
6
7
8
9
10
11
#include <windows.h>

int CALLBACK WinMain(
_In_ HINSTANCE hInstance,
_In_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine,
_In_ int nCmdShow
){
MessageBox(NULL,"Hello Windows!","Hello Windows!",MB_OK);
return 0;
}

运行,发现刚刚的命令行没有了,只剩下一个对话框了!!!

我们来解释一下这个函数吧。这个WinMain函数是Windows程序的应用程序入口点,程序将从WinMain处开始执行。有一点需要注意,c++中的main函数里面的参数是可以不写的,但这里不行。四个参数都要写。
WinMain函数前面带了一个CALLBACK,这个CALLBACK是什么意思呢,我们再IDE上按住CTRL键左键点击这个函数转到函数定义,发现CALLBACK是一个宏,原型如下:

1
#define CALLBACK   __stdcall

这时我们发现这个CALLBACK其实就是__stdcall,那么__stdcall是什么?这个其实是跟C语言的__cdecl对应的,都是一种调用约定,这个可以不用具体深究它,可以将这个__stdcall理解为专门来调用Win API函数的就行了。
CALLBACK有什么特别功能吗?字面意思”callback”是回调的意思。我们的WinMain函数由操作系统调用,但WinMain函数具体做什么,操作系统就不管了,它有可能回头调用操作系统的一些API都是没有问题的。

下面我们来分析WinMain的这四个参数。我们经常会看到诸如HANDLE,HINSTANCE,HBRUSH,WPARAM。LPARAM,HICON,HWND等一大串数据类型,感觉好多好复杂啊……其实并不是,他们说到底还是C++的那几种基本数据类型,只是微软为了方便使用而重新定义了他们罢了。例如HINSTANCE类型,CTRL加鼠标左键转到定义,发现HINSTANCE也是一个宏定义:

1
typedef void *HINSTANCE;

第一个参数hInstance是当前应用程序的实例句柄,第二个参数hPrevInstance是前一个应用程序的实例句柄(如果你把这个应用程序开了两个的话),比如我把a.exe运行了两个实例,进程列表中会有两个a.exe,这时候第一次运行的实例号假设为01,就传递第一个参数hInstance,第二次运行的假设实例号为02,就传给了hPrevInstance参数。
第三个参数lpCmdLine名字上就可以理解到是命令行参数,但是LPSTR是什么意思呢?它是一个字符串,char*类型(注意,Windows编程中,所有以lp开头的,均为指针类型,比如LPSTR可以理解为long point string)
第四个参数是nCmdShow,它指明了主窗口显示方式,它有以下几个值:

Value Meaning
SW_HIDE 0 Hides the window and activates another window.
SW_MAXIMIZE 3 Maximizes the specified window.
SW_MINIMIZE 6 Minimizes the specified window and activates the next top-level window in the Z order.
SW_RESTORE 9 Activates and displays the window. If the window is minimized or maximized, the system restores it to its original size and position. An application should specify this flag when restoring a minimized window.
SW_SHOW 5 Activates the window and displays it in its current size and position.
SW_SHOWMAXIMIZED 3 Activates the window and displays it as a maximized window.
SW_SHOWMINIMIZED 2 Activates the window and displays it as a minimized window.
SW_SHOWMINNOACTIVE 7 Displays the window as a minimized window. This value is similar to SW_SHOWMINIMIZED, except the window is not activated.
SW_SHOWNA 8 Displays the window in its current size and position. This value is similar to SW_SHOW, except the window is not activated.
SW_SHOWNOACTIVE 4 Displays a window in its most recent size and position. This value is similar to SW_SHOWNORMAL, except the window is not activated.
SW_SHOWNORMAL 1 Activates and displays a window. If the window is minimized or maximized, the system restores it to its original size and position. An application should specify this flag when displaying the window for the first time.

完成了WinMain,我们还需要一个WindowProc函数,这个函数是用来处理传给我们应用程序的消息的(即消息处理函数)。该函数定义如下:

1
2
3
4
5
6
7
8
9
10
//这个函数也可以不叫WindowProc,可以命名成任何你想要的名字,但是命名成WindowProc更利于理解,而且也符合一般的消息处理函数的命名法则。
LRESULT CALLBACK WindowProc(
_In_ HWND hWnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
{
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

WindowProc用来处理源源不断从操作系统发送来的消息,当然如果我们对这些消息不感兴趣或者不想处理的话,就交给DefWindowProc函数来处理,这是Windows系统默认的消息处理函数。
其中:
hWnd是当前窗口的句柄。
uMsg是系统发过来的消息。
wParam是消息参数。
lParam是消息参数。
这个函数也带有CALLBACK,同理,这也是操作系统调用的函数。

0x02 设计和注册窗口类

设计窗口类,其实就是设计我们程序的主窗口,如有没有标题栏,背景什么颜色,有没有边框,可不可以调整大小等。要设计窗口类,我们用到一个结构————

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct tagWNDCLASS {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
} WNDCLASS, *PWNDCLASS;

通常情况下,我们用tagWNDCLASS就可以了,当然还有一个tagWNDCLASSEX的扩展结构,在API里面,凡是看到EX结尾的都是扩展的意思,比如CreateWindowEx就是CreateWindow的扩展函数。

第一个成员是窗口的类样式,注意,不要和窗口样式(WS_xxxxx)混淆了,这里指的是这个窗口类的特征,不是窗口的外观特征,这两个style是不一样的。

它的值可以参考MSDN,通常我们只需要两个就可以了——CS_HREDRAW | CS_VREDRAW,从名字就看出来了,就是同时具备水平重画和垂直重画。因为当我们的窗口显示的时候,被其他窗口挡住后重新显示,或者大小调整后,窗口都要发生绘制,就像我们在纸上涂鸦一样,每次窗口的变化都会“粉刷”一遍,并发送WM_PAINT消息。

lpfnWndProc参数就是用来设置你用哪个WindowProc来处理消息,前面我说过,我们只要不更改回调函数的返回值和参数的类型和顺序,就可以随意设置函数的名字,那为什么系统可以找到我们用的回调函数呢,对的,就是通过lpfnWndProc传进去的,它是一个函数指针,也就是它里面保存的是我们定义的WindowProc的入口地址,使用很简单,我们只需要把函数的名字传给它就可以了。

cbClsExtra和cbWndExtra通常不需要,设为0就OK。hInstance是当前应用程序的实例句柄,从WinMain的hInstance参数中可以得到。hIcon和hCursor就不用我说了,看名字就知道了。

hbrBackground是窗口的背景色,你也可以不设置,但在处理WM_PAINT消息时必须绘制窗口背景。也可以直接用系统定义的颜色,MSDN为我们列出这些值,大家不用记,直接到MSDN拿来用就行了,这些都比较好理解,看名字就知道了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
COLOR_ACTIVEBORDER
COLOR_ACTIVECAPTION
COLOR_APPWORKSPACE
COLOR_BACKGROUND
COLOR_BTNFACE
COLOR_BTNSHADOW
COLOR_BTNTEXT
COLOR_CAPTIONTEXT
COLOR_GRAYTEXT
COLOR_HIGHLIGHT
COLOR_HIGHLIGHTTEXT
COLOR_INACTIVEBORDER
COLOR_INACTIVECAPTION
COLOR_MENU
COLOR_MENUTEXT
COLOR_SCROLLBAR
COLOR_WINDOW /* 这个就是窗口的默认背景色 */
COLOR_WINDOWFRAME
COLOR_WINDOWTEXT

lpszMenuName指的是菜单的ID,没有菜单就NULL,lpszClassName就是我们要向系统注册的类名,字符,但不能与系统已存在的类名冲突。

随后,我们在WinMain中设计窗口类。

1
2
3
4
5
6
7
8
   // 类名
WCHAR* cls_Name = "My Class";
// 设计窗口类
WNDCLASS wc;
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
wc.lpfnWndProc = WindowProc;
wc.lpszClassName = cls_Name;
wc.hInstance = hInstance;

窗口类设计完成后,不要忘了向系统注册,这样系统才能知道有这个窗口类的存在。向操作系统注册窗口类,使用RegisterClass函数,它的参数是一个指向WNDCLASS结构体的指针,所以我们传递的时候,要加上&符号。

1
2
// 注册窗口类
RegisterClass(&wc);

0x03 创建和显示窗口

窗口类注册完成后,就应该创建窗口,然后显示窗口,调用CreateWindow创建窗口,如果成功,会返回一个窗口的句柄,我们对这个窗口的操作都要用到这个句柄。什么是句柄呢?其实它就是一串数字,只是一个标识而已,内存中会存在各种资源,如图标、文本等,为了可以有效标识这些资源,每一个资源都有其唯一的标识符,这样,通过查找标识符,就可以知道某个资源存在于内存中哪一块地址中。
创建窗口的函数CreateWindow返回一个HWND类型,它就是窗口类的句柄。函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
HWND WINAPI CreateWindow(

_In_opt_ LPCTSTR lpClassName, // 窗口类名称

_In_opt_ LPCTSTR lpWindowName, // 窗口标题

_In_ DWORD dwStyle, // 窗口风格,或称窗口格式

_In_ int x, // 初始 x 坐标

_In_ int y, // 初始 y 坐标

_In_ int nWidth, // 初始 x 方向尺寸

_In_ int nHeight, // 初始 y 方向尺寸

_In_opt_ HWND hWndParent, // 父窗口句柄

_In_opt_ HMENU hMenu, // 窗口菜单句柄

_In_opt_ HINSTANCE hInstance, // 程序实例句柄

_In_opt_ LPVOID lpParam // 创建参数

);

(好复杂QAQ,嗯看着注释就能理解了。)
调用一下这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建窗口
HWND hWnd = CreateWindow(
cls_Name, //类名,要和刚才注册的一致
"应用程序", //窗口标题文字
WS_OVERLAPPEDWINDOW, //窗口外观样式,这个具体的样式MSDN上搜吧,我这里就不罗列了
38, //窗口相对于父级的X坐标
20, //窗口相对于父级的Y坐标
480, //窗口的宽度
250, //窗口的高度
NULL, //没有父窗口,为NULL
NULL, //没有菜单,为NULL
hInstance, //当前应用程序的实例句柄
NULL); //没有附加数据,为NULL
if(hWnd == NULL) //检查窗口是否创建成功
return 0;

窗口创建后,就要显示它,就像我们的产品做了,要向客户展示。显示窗口调用ShowWindow函数。

1
ShowWindow(hWnd, SW_SHOW);

既然要显示窗口了,那么ShowWindow的第一个参数就是刚才创建的窗口的句柄,第二个参数控制窗口如何显示,你可以从SW_XXXX中选一个,也可以用WinMain传进来的参数,还记得WinMain的最后一个参数吗?没错,就是它了。

0x04 (可选)更新窗口

为什么更新窗口这一步可有可无呢?因为只要程序在运行着,只要不是最小化,只要窗口是可见的,那么,我们的应用程序会不断接收到WM_PAINT通知。(关于WM_PAINT后面再说)更新窗口调用的是UpdateWindow函数。

1
UpdateWindow(hWnd);

0x05 (important)消息循环

Windows操作系统是基于消息控制机制的,用户与系统之间的交互,程序与系统之间的交互,都是通过发送和接收消息来完成的。
我们知道,代码是不断往前执行的,像我们刚才写的WinMain函数一样,如果你现在运行程序,你会发现什么都没有,是不是程序不能运行呢,不是,其实程序是运行了,只是它马上结束了,只要程序执行跳出了WinMain的右大括号,程序就会结束了。那么,要如何让程序不结束了,可能大家注意到我们在C程序中可以用一个getchar()函数来等到用户输入,这样程序就人停在那里,直到用户输入内容。但我们的窗口应用不能这样做,因为用户有可能进行其他操作,如最小化窗口,移动窗口,改变窗口大小,或者点击窗口上的按钮等。因此,我们不能简地弄一个getchar在那里,这样就无法响应用户的其他操作了。
于是我们需要引入消息循环,只要有与用户交互,系统人不断地向应用程序发送消息通知,因为这些消息是不定时不断发送的,必须有一个绶冲区来存放,就好像你去银行办理手续要排队一样,我们从最前端取出一条一条消息处理,后面新发送的消息会一直在排队,直到把所有消息处理完,这就是消息队列。
要取出一条消息,调用GetMessage函数。函数会传入一个tagMSG结构体的指针,当收到消息,会填充tagMSG结构体中的成员变量,这样我们就知道我们的应用程序收到什么消息了,直到GetMessage函数取不到消息,条件不成立,循环跳出,这时应用程序就退出。
tagMSG结构定义如下:

1
2
3
4
5
6
7
8
typedef struct tagMSG {
HWND hWnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG, *PMSG, *LPMSG;

hWnd就不用说了,就是窗口句柄,哪个窗口的句柄?还记得WindowProc回调函数吗?你把这个函数交给了谁来处理,hWnd就是谁的句柄,比如我们上面的代码,我们是把WindowProc赋给了新注册的窗口类,并创建了主窗口,返回一个表示主窗口的句柄,所以,这里MSG中的hWnd指的就是我们的主窗口。
message就是我们接收到的消息,它是个无符号整数,所以我们操作的所有消息都是数字来的。wParam和lParam是消息的附加参数,也是数值来的。通常,lParam指示消息的处理结果,不同消息的结果(返回值)不同,具体参阅MSDN。

有了一个整数值来表示消息,我们为什么还需要附加参数呢?你不妨想一下,如果接收一条WM_LBUTTONDOWN消息,即鼠标左键按下时发送的通知消息,那么,我们不仅需要知道左键按下,我们更感兴趣的是,鼠标在屏幕上的哪个坐标处按下左键,按了几下,这时候,你只靠一条WM_LBUTTONDOWN是无法传递这么多消息的。可能我们需要把按下左键时的坐标放入wParam参数中;最典型的就是WM_COMMAND消息,因为只要你使用菜单,点击按钮都会发送这样一条消息,那么我怎么知道用户点了哪个按钮呢?如果窗口中只有一个按钮,那好办,但是,如果窗口上有10个按钮呢?而每一个按钮被单击都会发送WM_COMMAND消息,你能知道用户点击了哪个按钮吗?所以,我们要把用户点击了的那个按钮的句柄存到lParam参数中,这样一来,我们就可以判断出用户到底点击了哪个按钮了。

GetMessage函数声明如下:

1
2
3
4
5
6
BOOL WINAPI GetMessage(
_Out_ LPMSG lpMsg,
_In_opt_ HWND hWnd,
_In_ UINT wMsgFilterMin,
_In_ UINT wMsgFilterMax
);

这个函数在定义时带了一个WINAPI,应该猜到,它也是一个宏,而真实的值是__stdcall,前文中说过了。

第一个参数是以LP开头,还记得吗,我说过的,你应该想到它就是 MSG* ,一个指向MSG结构的指针。第二个参数是句柄,通常我们用NULL,因为我们会捕捉整个应用程序的消息。后面两个参数是用来过滤消息的,指定哪个范围内的消息接收,在此范围之外的消息拒收,如果不过滤就全设为0。

1
2
3
4
5
6
7
// 消息循环
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

TranslateMessage是用于转换按键信息的,因为键盘按下和弹起会发送WM_KEYDOWN和WM_KEYUP消息,但如果我们只想知道用户输了哪些字符,这个函数可以把这些消息转换为WM_CHAR消息,它表示的就是键盘按下的那个键的字符,如“A”,这样我们处理起来就更方便了。

DispatchMessage函数是必须调用的,它的功能就相当于一根传送带,每收到一条消息,DispatchMessage函数负责把消息传到WindowProc让我们的代码来处理,如果不调用这个函数,我们定义的WindowProc就永远接收不到消息,此时应用程序无响应。(死机啦……)

0x06 消息响应

其实现在我们的应用程序是可以运行了,因为在WindowProc中我们调用了DefWindowProc,函数,消息我们不作任何处理,又把控制权路由回到操作系统来默认处理,所以,整个过程的消息循环是成立的,只不过我们做默认响应罢了。

0x07 完整的创建窗口代码(不是Visual Studio自动生成的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#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"我的应用程序", //窗口标题文字
WS_OVERLAPPEDWINDOW, //窗口外观样式
38, //窗口相对于父级的X坐标
20, //窗口相对于父级的Y坐标
480, //窗口的宽度
250, //窗口的高度
NULL, //没有父窗口,为NULL
NULL, //没有菜单,为NULL
hInstance, //当前应用程序的实例句柄
NULL); //没有附加数据,为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);
}

0xFF 什么?你说窗口创建失败?

是啊,你的代码为什么窗口一创建就退出了?仔细想想……初学C语言的时候,老师告诉我们(或者自学的时候书上有提到)定义一个变量如果不初始化就直接运算操作的话,会出现未知错误。现在回到我们的代码里。我们发现WNDCLASS结构体成员没有初始化,好了,发现问题。

1
2
3
4
5
6
7
8
9
10
11
WNDCLASS wc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hCursor = LoadCursor(hInstance, IDC_ARROW);;
wc.hIcon = LoadIcon(hInstance, IDI_APPLICATION);;
wc.lpszMenuName = NULL;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
wc.lpfnWndProc = WindowProc;
wc.lpszClassName = cls_Name;
wc.hInstance = hInstance;

但是这样初始化它未免太麻烦,不如我们换个方式吧。

1
2
3
4
5
WNDCLASS wc = { };
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
wc.lpfnWndProc = WindowProc;
wc.lpszClassName = cls_Name;
wc.hInstance = hInstance;

至于为什么可以这样,复习一下C语言基础吧。我们换一个简单的例子来看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

#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("矩形的坐标是:%d, %d\n矩形的大小:%d , %d", rect.x, rect.y, rect.width, rect.height);
getchar();
}

我们定义了一个表示矩形的结构体 RECT ,它有四个成员,分别横坐标,纵坐标,宽度,高度,但是,我们在声明和赋值中,我们只用了一对大括号,把每个成员的值,按照定义的顺序依次写到大括号中,即{ 0, 0, 20, 30 },x的值为0,y的值为0,width为20,height的值为30。

也就是说,我们可以通过这种方法向结构变量赋值。
再回到那句代码WNDCLASS wc = { };上,是不是好理解一些了?我们通过这种巧妙的办法为结构体的所有变量赋了初值。

0xFFFF 什么?程序不能退出?你在逗我?

通常情况下,当我们的主窗口关闭后,应用程序应该退出(木马程序除外),但是,我们刚才运行后发现,为什么我的窗口关了,但程序不退出呢?我们知道,要退出程序,就要跳出消息循环,而与关闭哪个窗口是无关的。因此,我们要解决两个问题:
1.如果跳出消息循环;
2.什么时候退出程序。
其实两个问题可以合并到一起解决。
当窗口被关闭后,为窗口所分配的内存会被销毁,同时,我们会收到一条WM_DESTROY消息,因而,我们只要在收到这条消息时调用PostQuitMessage函数,这个函数提交一条WM_QUIT消息,而在消息循环中,WM_QUIT消息使GetMessage函数返回0,这样一来,GetMessage返回FALSE,就可以跳出消息循环了,这样应用程序就可以退出了。

所以,我们要做的就是捕捉WM_DESTROY消息,然后PostQuitMessage.
我们修改WindowProc函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

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消息,如果是,退出应用程序。

好了,这样,我们一个完整的Windows应用程序就做好了。

下面是完整的代码(这回没问题了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#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"我的应用程序", //窗口标题文字
WS_OVERLAPPEDWINDOW, //窗口外观样式
38, //窗口相对于父级的X坐标
20, //窗口相对于父级的Y坐标
480, //窗口的宽度
250, //窗口的高度
NULL, //没有父窗口,为NULL
NULL, //没有菜单,为NULL
hInstance, //当前应用程序的实例句柄
NULL); //没有附加数据,为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
)
{
switch(uMsg)
{
case WM_DESTROY:
{
PostQuitMessage(0);
return 0;
}
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}