窗口消息
GUI应用程序必须响应来自用户和操作系统的事件。
- 来自用户的事件包括用户与程序交互的所有方式:鼠标点击、按键、触摸屏手势等等。
- 来自操作系统的事件包括程序之外的任何可能影响程序行为的东西。例如,用户可能插入一个新的硬件设备,或者Windows可能进入低功耗状态(睡眠或休眠)。
这些事件可以在程序运行时的任何时间发生,几乎可以按任何顺序发生。如何构造一个不能预先预测执行流(flow)的程序?
为了解决这个问题,Windows使用了消息传递模型。操作系统通过传递消息与应用程序窗口进行通信。消息只是指定特定事件的数字代码。例如,如果用户按下鼠标左键,窗口将接收到具有以下消息代码的消息。
C++
#define WM_LBUTTONDOWN 0x0201
一些消息具有与它们相关联的数据。例如,WM_LBUTTONDOWN消息包含鼠标光标的x坐标和y坐标。
要将消息传递给窗口,操作系统将调用为该窗口注册的窗口过程。(现在你知道窗口程序的作用了。)
消息环(The message loop)
应用程序在运行时将收到数千条消息。(考虑到每次击键和点击鼠标按钮都会产生一条消息。)另外,应用程序可以有多个窗口,每个窗口都有自己的窗口过程。程序如何接收所有这些消息并将它们发送到正确的窗口过程?应用程序需要一个循环来检索消息并将其发送到正确的窗口。
对于创建窗口的每个线程,操作系统都为窗口消息创建一个队列(queue)。此队列保存了在该线程上创建的所有窗口的消息。队列本身在程序是隐藏不可见的。您不能直接操作队列。但是,您可以通过调用 GetMessage 函数从队列中提取消息。
C++
MSG msg; GetMessage(&msg, NULL, 0, 0);
此函数会从队列的头部移除第一个消息。如果队列为空,函数将阻塞,直到另一条消息入队列。事实上,GetMessage阻塞不会使您的程序没有响应。如果没有消息,程序就没有什么可做的。如果您必须执行后台处理,您可以创建额外的线程,在GetMessage等待另一个消息时继续运行。(参见 Avoiding Bottlenecks in Your Window Procedure)。
GetMessage的第一个参数是MSG结构体的地址。如果函数成功,它将在MSG结构体中填充消息信息。这包括目标窗口和消息代码。其他三个参数可以过滤(filter)从队列中获取的消息。通常情况下,这些参数设置为零。
虽然MSG结构体包含了关于消息的信息,但是您几乎永远不会直接检查这个结构体。相反,您会将把它直接传递给另外两个函数:
C++
TranslateMessage(&msg);
DispatchMessage(&msg);
TranslateMessage与键盘输入有关。它将按键(按键向下,按键向上)转换成字符。你并不需要知道这个函数是如何工作的;只需要记住在 DispatchMessage之前调用它。如果您有兴趣,MSDN文档的链接将为您提供更多的信息。
DispatchMessage 函数告诉操作系统调用作为消息目标的窗口过程。换句话说,操作系统在窗口表中查找窗口句柄,找到与窗口关联的函数指针,并调用该函数。
例如,假设用户按下鼠标左键。这导致一系列事件:
- 操作系统在消息队列(message queue)上放置一个WM_LBUTTONDOWN消息。
- 你编写的程序调用GetMessage函数。
- GetMessage从队列中提取WM_LBUTTONDOWN消息并填充MSG结构体。
- 你编写的程序调用TranslateMessage 函数和DispatchMessage 函数。
- DispatchMessage中,操作系统调用窗口过程(window procedure)。
- 你的窗口过程(window procedure)可以响应消息,也可以忽略它
当窗口过程(window procedure)调用结束后,它返回到DispatchMessage。接着将返回到下一个消息的消息循环。只要您的程序正在运行,不断会有消息进入队列。因此,您必须有一个循环,该循环不断地从队列中提取消息并分派它们。你可以把这个循环想成是这样的:
C++
// WARNING: Don't actually write your loop this way. while (1) { GetMessage(&msg, NULL, 0, 0); TranslateMessage(&msg); DispatchMessage(&msg); }
当然,正如所写的,这个循环永远不会结束。这就是GetMessage函数的返回值。通常,GetMessage返回一个非零值。当您想要退出应用程序并跳出消息循环时,请调用 PostQuitMessage函数。
C++
PostQuitMessage(0);
PostQuitMessage 函数将一个WM_QUIT消息放到消息队列中。WM_QUIT是一个特殊的消息:它导致GetMessage 返回零,发出消息循环结束的信号。下面是修改后的消息循环。
syntax
// Correct. MSG msg = { }; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); }
只要 GetMessage 返回一个非零值,while循环中的表达式将计算为true。调用PostQuitMessage后,表达式变为false,程序将跳出循环。(这种行为的一个有趣结果是,您的窗口过程永远不会收到WM_QUIT 消息。因此,您不必在窗口过程中为该消息写一个case语句。
下一个问题是什么时候调用PostQuitMessage。我们将在 Closing the Window主题中回到这个问题,在此之前,我们必须编写窗口过程(window procedure)。
发布消息与发送消息(Posted Messages versus Sent Messages)
前一节讨论了进入队列的消息。有时,操作系统会直接调用窗口过程,绕开队列。
两个术语可能会令人困惑:
- 发布消息(Posting a message)意味着消息进入消息队列,并通过消息循环(GetMessage 和 DispatchMessage)发送。
- 发送消息(Sending a message)意味着消息跳过队列,操作系统直接调用窗口过程。
就目前而言,差异并不十分重要。窗口过程(window procedure)处理所有消息。但是,一些消息绕过队列,直接进入窗口过程。但是,如果您的应用程序在windows之间进行通信,则会产生不同的结果。您可以在 About Messages and Message Queues的主题中找到对这个问题更深入的讨论。