Windows 消息 (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 之前调用它。 如果好奇,MSDN 文档的链接将提供更多信息。

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

例如,假设用户按下鼠标左键。 这会导致事件链:

  1. 操作系统将 WM_LBUTTONDOWN 消息置于消息队列中。
  2. 程序调用 GetMessage 函数。
  3. GetMessage 从队列中提取 WM_LBUTTONDOWN 消息并填充 MSG 结构。
  4. 程序调用 TranslateMessageDispatchMessage 函数。
  5. DispatchMessage 中,操作系统调用窗口过程。
  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。 我们将在 关闭窗口主题中返回此问题,但首先必须编写窗口过程。

已发布消息与已发送消息

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

这种区别的术语可能令人困惑:

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

目前,区别并不十分重要。 窗口过程处理所有消息。 但是,某些消息会绕过队列,直接转到窗口过程。 但是,如果应用程序在窗口之间通信,则可能会有所不同。 有关此问题的更全面讨论,请参阅关于 消息和消息队列的主题。

下一步

编写窗口过程