在 GUI(图形用户界面)应用程序中,事件驱动编程是核心概念。应用程序需要响应用户和操作系统的事件,以提供交互性和动态行为。
事件驱动编程
在 GUI 应用程序中,程序的行为是由事件驱动的。事件可以分为两类:
用户事件:
-
用户与应用程序交互时触发的事件。例如:鼠标点击、键盘输入、触摸屏手势等。
系统事件:
-
操作系统或其他外部因素触发的事件。例如:硬件插入、电源状态变化、窗口大小调整等。
这些事件可以在程序运行过程中的任何时间以几乎任何顺序发生。
如何构建无法提前预测其执行流的程序?
为了解决此问题,Windows 使用消息传递模型。 操作系统通过向应用程序窗口传递消息来与其通信。 消息只是指定特定事件的数值代码。 例如,如果用户按下鼠标左键,窗口将收到一条消息,消息代码如下。
#define WM_LBUTTONDOWN 0x0201
某些消息具有与其关联的数据。 例如,WM_LBUTTONDOWN 消息包括鼠标光标的 x 坐标和 y 坐标。若要向窗口传递消息,操作系统将调用为该窗口注册的窗口过程。若要向窗口传递消息,操作系统将调用为该窗口注册的窗口过程。
消息传递模型的工作原理
在 Windows 中,消息传递模型的工作流程如下:
事件发生:用户或系统触发事件(如鼠标点击、键盘输入、窗口大小调整等)。
生成消息:操作系统将事件转换为消息(如 WM_LBUTTONDOWN
、WM_KEYDOWN
等)。
发送消息:操作系统将消息放入应用程序的消息队列中。
处理消息:应用程序从消息队列中获取消息,并将其分发给相应的窗口过程函数。
响应消息:窗口过程函数根据消息类型执行相应的操作。
消息循环
应用程序在运行时将收到数千条消息。 (考虑到每次击键和单击鼠标按钮都会生成一条消息。)此外,应用程序可以有多个窗口,每个窗口都有其自己的窗口过程。 程序如何接收所有这些消息并将其传递到正确的窗口过程? 应用程序需要一个循环来检索消息并将其调度到正确的窗口。
对于创建窗口的每个线程,操作系统都会为窗口消息创建队列。 此队列保存在该线程上创建的所有窗口的消息。 队列本身已隐藏在程序中。 不能直接操作队列。 但是,可以通过调用 GetMessage 函数从队列拉取消息。
MSG msg;
GetMessage(&msg, NULL, 0, 0);
GetMessage
是消息循环的核心函数,用于从消息队列中获取消息。它的原型如下:
BOOL GetMessage(
LPMSG lpMsg, // 指向 MSG 结构的指针
HWND hWnd, // 窗口句柄(通常为 NULL,表示获取所有窗口的消息)
UINT wMsgFilterMin, // 过滤消息的最小值
UINT wMsgFilterMax // 过滤消息的最大值
);
此函数从队列的头中删除第一条消息。
GetMessage
的阻塞行为
如果消息队列为空,GetMessage
会阻塞,直到有新消息进入队列,这种阻塞行为不会导致程序无响应,因为程序在没有消息时不需要执行任何操作。如果程序需要执行后台处理(如文件下载、计算等),可以创建额外的线程,主线程继续运行消息循环,而其他线程在后台执行任务。
GetMessage 的第一个参数是 MSG 结构的地址。 如果函数成功,它将使用有关消息的信息填充 MSG 结构。 这包括目标窗口和消息代码。 通过其他三个参数,可以过滤从队列中获取的消息。 在几乎所有情况下,可以将这些参数设置为零。MSG
结构包含了消息的详细信息,但通常我们不会直接检查或操作这个结构。相反,我们会将 MSG
结构传递给以下两个函数来处理消息:
TranslateMessage(&msg);
DispatchMessage(&msg);
TranslateMessage
:该函数与键盘输入相关, 它将击键(按下按键,松开按键)转换为字符。 我们不必了解此函数的工作原理;只需记得在 DispatchMessage 之前调用它即可。
DispatchMessage
:该函数告诉操作系统调用消息目标窗口的窗口过程。 换句话说,操作系统会在其窗口表中查找窗口句柄,找到与窗口关联的函数指针,并调用该函数。
例如,假设用户按下鼠标左键。 这会引发一连串事件:
-
操作系统在消息队列上放置 WM_LBUTTONDOWN 消息。
-
您的程序调用 GetMessage 函数。
-
GetMessage 从队列中提取 WM_LBUTTONDOWN 消息,并填写 MSG 结构。
-
程序调用 TranslateMessage 和 DispatchMessage 函数。
-
在 DispatchMessag 中,操作系统调用您的窗口过程。
-
窗口过程可以响应消息或忽略它。
当窗口过程返回时,它将返回到 DispatchMessage。 这会返回到下一条消息的消息循环。 只要程序正在运行,消息就会继续到达队列。 因此,必须有一个循环,不断从队列中拉取消息并将其发送出去。 可以将循环视为执行以下操作:
while (GetMessage(&msg, NULL, 0, 0);)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
当然,正如字面意思,此循环永远不会结束。 这就是 GetMessage 函数的返回值传入的位置。 通常,GetMessage 会返回非零值。 如果要退出应用程序并中断消息循环,请调用 PostQuitMessage 函数。
PostQuitMessage(0);
PostQuitMessage 函数在消息队列上放置 WM_QUIT 消息。 WM_QUIT 是一条特殊消息:它会导致 GetMessage 返回零,从而向消息循环的末尾发出信号。 下面是修订的消息循环。
MSG msg = { };
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
只要 GetMessage 返回非零值,则 while 循环中的表达式的计算结果为 true。 调用 PostQuitMessage 后,表达式变为 false,程序会中断循环。 下一个明显的问题是何时调用 PostQuitMessage。 我们将在《Win32:关闭窗口》中描述。
最后代码
#include <windows.h>
#include <stdio.h>
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) ;
int WINAPI wWinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
PWSTR pCmdLine,
int nCmdShow)
{
//注册窗口类
const wchar_t CLASS_NAME[] = L"WolvenChan's Window Class";
WNDCLASS wc = { 0 };
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
RegisterClass(&wc);
//创建窗口
HWND hwnd = CreateWindowEx(
0,
CLASS_NAME,
TEXT("Hello Wolven"),
WS_OVERLAPPEDWINDOW,
//坐标、窗口长、高
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL
);
if (hwnd == NULL) {
MessageBox(NULL, L"Failed to create window!", L"Error", MB_ICONERROR);
return 0;
}
ShowWindow(hwnd, nCmdShow);
//消息循环
MSG msg = { 0 };
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg); // 翻译消息
DispatchMessage(&msg); // 分发消息
}
return (int)msg.wParam;
return 0;
}