窗口消息(Win32 和 C++ 入门)

GUI 应用程序必须响应用户和操作系统的事件。

  • 用户的事件包括用户可以与程序交互的所有方式:鼠标单击、击键、触摸屏手势等。
  • 来自操作系统的事件包括可能影响程序行为方式的程序“外部”的任何内容。 例如,用户可能会插入新的硬件设备,或者 Windows 可能会进入低功率状态(睡眠或休眠)。

这些事件可以在程序运行过程中的任何时间以几乎任何顺序发生。 如何构建无法提前预测其执行流的程序?

为了解决此问题,Windows 使用消息传递模型。 操作系统通过向应用程序窗口传递消息来与其通信。 消息只是指定特定事件的数值代码。 例如,如果用户按下鼠标左键,窗口将收到一条消息,消息代码如下。

#define WM_LBUTTONDOWN    0x0201

某些消息具有与其关联的数据。 例如,WM_LBUTTONDOWN 消息包括鼠标光标的 x 坐标和 y 坐标。

若要向窗口传递消息,操作系统将调用为该窗口注册的窗口过程。 (现在您知道了窗口过程是什么。)

消息循环

应用程序在运行时将收到数千条消息。 (考虑到每次击键和单击鼠标按钮都会生成一条消息。)此外,应用程序可以有多个窗口,每个窗口都有其自己的窗口过程。 程序如何接收所有这些消息并将其传递到正确的窗口过程? 应用程序需要一个循环来检索消息并将其调度到正确的窗口。

对于创建窗口的每个线程,操作系统都会为窗口消息创建队列。 此队列保存在该线程上创建的所有窗口的消息。 队列本身已隐藏在程序中。 不能直接操作队列。 但是,可以通过调用 GetMessage 函数从队列拉取消息。

MSG msg;
GetMessage(&msg, NULL, 0, 0);

此函数从队列的头中删除第一条消息。 如果队列为空,该函数将阻塞,直到另一条消息进入队列。 事实上,GetMessage 的阻塞不会使程序无响应。 如果没有消息,则程序无需执行任何操作。 如果必须执行后台处理,可以创建额外的线程,它们在 GetMessage 等待另一条消息时继续运行。 (请参阅避免窗口过程中的瓶颈。)

GetMessage 的第一个参数是 MSG 结构的地址。 如果函数成功,它将使用有关消息的信息填充 MSG 结构。 这包括目标窗口和消息代码。 通过其他三个参数,可以过滤从队列中获取的消息。 在几乎所有情况下,您都会将这些参数设置为零。

尽管 MSG 结构包含有关消息的信息,但您几乎永远不会直接检查此结构。 而是将它直接传递给另外两个函数。

TranslateMessage(&msg); 
DispatchMessage(&msg);

TranslateMessage 函数与键盘输入相关。 它将击键(按下按键,松开按键)转换为字符。 您不必了解此函数的工作原理;只需记得在 DispatchMessage 之前调用它即可。

DispatchMessage 函数告诉操作系统调用消息目标窗口的窗口过程。 换句话说,操作系统会在其窗口表中查找窗口句柄,找到与窗口关联的函数指针,并调用该函数。

例如,假设用户按下鼠标左键。 这会引发一连串事件:

  1. 操作系统在消息队列上放置 WM_LBUTTONDOWN 消息。
  2. 您的程序调用 GetMessage 函数。
  3. GetMessage 从队列中提取 WM_LBUTTONDOWN 消息,并填写 MSG 结构。
  4. 程序调用 TranslateMessageDispatchMessage 函数。
  5. DispatchMessag 中,操作系统调用您的窗口过程。
  6. 窗口过程可以响应消息或忽略它。

当窗口过程返回时,它将返回到 DispatchMessage。 这会返回到下一条消息的消息循环。 只要程序正在运行,消息就会继续到达队列。 因此,必须有一个循环,不断从队列中拉取消息并将其发送出去。 您可以将循环视为执行以下操作:

// WARNING: Don't actually write your loop this way.

while (1)      
{
    GetMessage(&msg, NULL, 0,  0);
    TranslateMessage(&msg); 
    DispatchMessage(&msg);
}

当然,正如字面意思,此循环永远不会结束。 这就是 GetMessage 函数的返回值传入的位置。 通常,GetMessage 会返回非零值。 如果要退出应用程序并中断消息循环,请调用 PostQuitMessage 函数。

        PostQuitMessage(0);

PostQuitMessage 函数在消息队列上放置 WM_QUIT 消息。 WM_QUIT 是一条特殊消息:它会导致 GetMessage 返回零,从而向消息循环的末尾发出信号。 下面是修订的消息循环。

// Correct.

MSG msg = { };
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

只要 GetMessage 返回非零值,则 while 循环中的表达式的计算结果为 true。 调用 PostQuitMessage 后,表达式变为 false,程序会中断循环。 (此行为的一个有趣结果是,窗口过程永远不会收到 WM_QUIT 消息。因此,您不必在窗口过程中为此消息提供 case 语句。)

下一个明显的问题是何时调用 PostQuitMessage。 我们将在关闭窗口主题中返回此问题,但首先必须编写窗口过程。

已发布消息与已发送消息

上一部分讨论了进入队列的消息。 有时,操作系统将直接调用窗口过程,绕过队列。

此区别的术语可能会令人困惑:

  • 发布消息意味着消息进入消息队列,并通过消息循环(GetMessageDispatchMessage)进行分派。
  • 发送消息意味着消息跳过队列,操作系统直接调用窗口过程。

就目前而言,差异并不十分重要。 窗口过程处理所有消息。 但是,某些消息绕过队列并直接转到窗口过程。 但是,如果应用程序在窗口之间通信,情况就会有所不同。 您可以在主题关于消息和消息队列中找到对此问题的更深入讨论。

下一页

编写窗口过程