让我们与星光探索者一起,探索Windows吧!

创建自己的窗口

前两期的探索内容并没有创建属于自己的窗口,那么我们应该如何创建属于自己的窗口呢?

一提到窗口,你很可能会想到下面的画面

没错,这就是一个窗口!实际上,这个应用程序的所有按钮,文本编辑框,菜单都是窗口。有的时候,我们在使用办公软件(如WPS,微软办公套件等),或浏览器时,我们通过点击一下鼠标右键,就弹出了一个菜单,俗称右键菜单。这个右键菜单也可以是窗口,当右键菜单没显示的时候,我们按了一下右键,他就显示了。这样看来,窗口既可以是显示的,也可以是不显示的

因而,我们可以认为窗口是:

  • 占据屏幕的某些部分。
  • 在给定时刻可能看不到或可能不可见。
  • 了解如何绘制自身。
  • 响应用户或操作系统的事件。

说了这么多,让我们一起探索如何创造自己的窗口吧!

一个完整的窗口程序需要这些步骤:注册窗口类、创建窗口、处理消息。话不多说,看代码:

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include<windows.h>

// 如果报错unresolved symbol无法解析的外部符号错误,就手动链接下面的库
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdi32.lib")


// 声明窗口处理函数,用于处理窗口消息
LRESULT CALLBACK WinProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);


INT APIENTRY WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow
)
{
static const TCHAR TITLE[] = TEXT("window");
static const TCHAR CLASSNAME[] = TEXT("MyStruct");

// WNDCLASS 是窗口类结构体
// 窗口类结构体至少要给 lpfnWndProc,hInstance, lpszClassName
// 这些属性赋值,否则创建窗口不会成功
WNDCLASS wc{0};
wc.hbrBackground = (HBRUSH)GetStockObject(DKGRAY_BRUSH); // 窗口的背景颜色
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); // 标题栏图标样式
wc.hCursor = LoadCursor(NULL, IDC_ARROW); // 鼠标样式
wc.hInstance = hInstance; // 实例句柄
wc.lpfnWndProc = WinProc; // 窗口过程函数
wc.lpszClassName = CLASSNAME; // 窗口类名称
wc.lpszMenuName = NULL; // 窗口菜单名称
wc.style = CS_HREDRAW | CS_VREDRAW; // 窗口风格

// 第一步:注册窗口类
RegisterClass(&wc);

// 第二步:创建窗口
// 调这个函数,返回窗口句柄,如果创建窗口失败,返回NULL
HWND hwnd = CreateWindowEx(
0, // 窗口的其他风格
CLASSNAME, // 窗口类名
TITLE, // 窗口标题栏文字
WS_OVERLAPPEDWINDOW, // 窗口风格

// CW_USEDEFAULT 表示使用默认值
// 窗口初始宽度和高度
CW_USEDEFAULT, CW_USEDEFAULT,
// 窗口初始x坐标和y坐标
CW_USEDEFAULT, CW_USEDEFAULT,

NULL, // 父窗口
NULL, // 菜单
hInstance, // 当前实例句柄
NULL // 附加的内存
);
// 判断窗口是否创建成功
if (hwnd == NULL)
{
// 创建窗口失败
return 0;
}

// 展示窗口(第一个参数为要展示的窗口句柄,第二个参数为显示的方式)
ShowWindow(hwnd, nCmdShow);
// 更新窗口
UpdateWindow(hwnd);

// 进行消息循环
MSG msg{0};
// GetMessage函数用于抓取窗口的消息,这样才能操作窗口
// 如果抓不到消息,GetMessage返回0,也就是说应用程序该结束了
while(GetMessage(&msg, NULL, 0, 0))
{
// 翻译消息(非必要步骤),如果要处理键盘事件,需要翻译消息
TranslateMessage(&msg);
// 分发消息(必要步骤,否则窗口无法得到消息)
DispatchMessage(&msg);
}

return 0;
}


// 窗口处理函数
// @param hwnd 窗口的句柄
// @param uMsg 消息类型
// @param wParam, lParam 窗口消息的两个附加信息,不同的消息,值是不同的
LRESULT CALLBACK WinProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
// 处理窗口摧毁消息
case WM_DESTROY:
PostQuitMessage(0); // 发送退出消息
return 0;
}
// 默认的窗口消息处理函数
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

编译此段代码,可以看到下面的结果:

成功创建出窗口了!

过程解析

首先是WNDCLASS结构体,示例代码已经大部分标的很清楚了。官网上介绍的比星光探索者探索到的还要详细,点击查看官网介绍WNDCLASS窗口类结构体

在示例代码中,调用了LoadIcon,LoadCursor来加载图标资源,原型如下:

1
2
3
4
5
6
7
8
9
10
HICON LoadIcon(
HINSTANCE hInstance, // 加载资源的实例句柄
LPCTSTR lpIconName // 图标名称字符串
);


HCURSOR LoadCursor(
HINSTANCE hInstance, // 加载资源的实例句柄
LPCTSTR lpCursorName // 加载光标名称
)

Windows的数据类型,如果以H开头,大多都是…的句柄的意思。

接下来是RegisterWindow函数CreateWindow函数,这些都是宏定义,都是根据程序使用的字符集来选择调用哪个版本的。例如CreateWindow如果是多字节字符集,将调用CreateWindowA版本,如果是Unicode字符集,将调用CreateWindowW版本,很多皆是如此。

ShowWindow 函数UpdateWindow 函数参数都很简单。

(重点内容) 窗口过程函数,是处理窗口消息的函数。Windows是基于消息系统的,当鼠标点击窗口或键盘输入时,将发送相应消息给窗口。因而窗口过程函数即为重要。窗口过程函数写法如下:

1
2
3
4
5
6
7
// WinProc是窗口处理函数的名称,可以随便改
LRESULT CALLBACK WinProc(
HWND hwnd, // 处理消息的窗口句柄
UINT uMsg, // 窗口消息
WPARAM wParam, // 窗口消息附带的参数
LPARAM lParam // 窗口消息附带的参数
);

此函数的返回值为窗口消息的处理结果,int类型可以转换成HRESULT类型,大多数情况下返回0表示正常处理。

如果不处理消息,可交给默认的过程函数DefWinProc处理。但是如果不正确处理相关消息,可能会导致窗口关不了,窗口没了进程还在的情况。

Windows消息机制

Windows是以消息为基础的操作系统。消息(MSG类型)组成如下:

  • 窗口句柄

  • 消息ID

  • 消息的两个附带信息

  • 消息产生时间

  • 消息产生时的鼠标位置

当系统通知窗口工作时,采用消息的方式派发给窗口。(本段落以下内容引用自微软官网)如果顶级窗口停止响应消息超过几秒钟,系统将认为窗口不会响应。 在这种情况下,系统会隐藏窗口,并将其替换为具有相同 Z 顺序、位置、大小和视觉属性的幽灵窗口。 这样,用户就可以移动它、调整大小,甚至关闭应用程序。 但是,这些操作是唯一可用的操作,因为应用程序实际上没有响应。 在调试器模式下,系统不会生成虚影窗口。

消息队列,顾名思义就是储存消息的队列,队列具有先进先出(First in first out, FIFO) 的特性。操作系统本身就维护有消息队列,每个应用程序也会有他自己的消息队列。

但是,并不是所有的消息都会进消息队列,有的消息是直接让窗口处理不进消息队列的。因此,根据消息进不进消息队列,我们可将消息分为队列消息和非队列消息

  • 队列消息:消息发送时,首先进入队列,然后通过消息循环,从队列当中获取。常见的队列消息有WM_PAINT,键盘消息,鼠标消息,定时器消息等

  • 非队列消息:消息发送时,直接找到消息接收窗口的窗口过程函数,直接调用此函数完成消息。常见的非队列消息有WM_CREATE,WM_SIZE等

上面说的都是常见的队列消息和非队列消息,只要你想让他进队列,他就是队列消息,否则是非队列消息。

抓取消息的API

GetMessage函数。 上面的消息循环就是用的这个抓取队列中的消息。如果这个函数的最后两个参数都为0,什么消息都抓取。一般不返回非0值。当遇到WM_QUIT消息(使应用程序退出的消息)时,GetMessage将返回FALSE。GetMessage函数是阻塞函数,如果没有消息会一直等待。

GetMessage函数在工作时,会按顺序执行下列步骤:

  • 在程序(线程)消息队列中查找消息,如果队列有消息,检查是否满足抓取消息的范围,满足就抓取

  • 如果程序(线程)消息队列没有消息,向系统队列获取属于本程序的消息,如果有就转发到程序的消息队列。

  • 如果还是没有消息,检查当前进程的所有窗口有没有需要重绘的区域,如果发现有需要绘制的区域,产生WM_PAINT消息,然后处理

  • 如果没有重绘区域,检查计时器有没有到时的计时器,如果有将产生WM_TIMER消息,然后处理

  • 如果没有计时器,就整理程序资源,内存等

  • 如果执行到了这一步,GetMessage就会一直等候,直到消息有为止

1
2
3
4
5
6
BOOL GetMessage(
LPMSG lpMsg, // 抓取到消息储存的消息结构体
HWND hWnd, // 抓取消息的窗口句柄
UINT wMsgFilterMin, // 抓取消息的最小值
UINT wMsgFilterMax // 抓取消息的最大值
);

PeekMessage函数用于以查看的方式查看队列有没有消息,如果没有消息返回0,否则返回非0值。这个函数的前四个参数与GetMessage函数意义完全相同,第五个参数的意义为如果有消息,如果值为PM_NOREMOVE,将不从消息队列中移除这个消息,如果值为PM_REMOVE,将从消息队列中移除这个消息。

1
2
3
4
5
6
7
BOOL PeekMessage(
LPMSG lpMsg,
HWND hWnd,
UINT wMsgFilterMin,
UINT wMsgFilterMax,
UINT wRemoveMsg
);

发送消息的API

SendMessage 函数 用于向指定窗口句柄的窗口发送消息,这种方式发送的消息是不进消息队列的。此函数会一直等待消息处理结果,其返回值即为目标窗口的处理结果

1
2
3
4
5
6
LRESULT SendMessage(
HWND hWnd, // 指定的窗口句柄
UINT Msg, // 发送的消息类型
WPARAM wParam, // 消息附带的参数
LPARAM lParam // 消息附带的参数
);

PostMessage函数用于向指定窗口句柄的窗口发送消息,这种方式发送的消息进消息队列。此函数发送完马上返回,不等待消息处理结束。

1
2
3
4
5
6
BOOL PostMessage(
HWND hWnd, // 指定的窗口句柄
UINT Msg, // 发送的消息类型
WPARAM wParam, // 消息附带的参数
LPARAM lParam // 消息附带的参数
);

本期到此结束,下期看星光探索者处理常用的消息及自定义消息