关于消息和消息队列
与基于 MS-DOS 的应用程序不同,基于 Windows 的应用程序是事件驱动的。 它们不会 ((如 C 运行时库调用)进行显式函数调用,) 获取输入。 而是等待系统向其传递输入。
系统将应用程序的所有输入传递到应用程序的各个窗口。 每个窗口都有一个函数(称为窗口过程),每当有窗口的输入时,系统都会调用该函数。 窗口过程处理输入并将控件返回到系统。 有关窗口过程的详细信息,请参阅 窗口过程。
如果顶级窗口停止响应消息超过几秒钟,系统会认为该窗口没有响应。 在这种情况下,系统会隐藏窗口,并将其替换为具有相同 Z 顺序、位置、大小和视觉属性的虚影窗口。 这允许用户移动它、调整大小,甚至关闭应用程序。 但是,这些是唯一可用的操作,因为应用程序实际上没有响应。 在调试器模式下,系统不会生成虚影窗口。
本部分讨论以下主题:
Windows 消息
系统以 消息的形式将输入传递给窗口过程。 消息由系统和应用程序生成。 系统在每个输入事件处生成一条消息,例如,当用户键入、移动鼠标或单击控件(如滚动条)时。 系统还会生成消息,以响应应用程序带来的系统更改,例如,当应用程序更改系统字体资源池或调整其某个窗口的大小时。 应用程序可以生成消息,以指示其自己的窗口执行任务或与其他应用程序中的窗口通信。
系统向窗口过程发送消息,其中包含一组四个参数:一个窗口句柄、一个消息标识符和两个称为 消息参数的值。 窗口句柄标识消息的目标窗口。 系统使用它来确定哪个窗口过程应接收消息。
消息标识符是一个命名常量,用于标识消息的用途。 当窗口过程收到消息时,它使用消息标识符来确定如何处理消息。 例如,消息标识符 WM_PAINT 告知窗口过程窗口的工作区已更改,必须重新绘制。
消息参数指定窗口过程在处理消息时使用的数据或数据的位置。 消息参数的含义和值取决于消息。 消息参数可以包含整数、打包的位标志、指向包含其他数据的结构的指针,等等。 当消息不使用消息参数时,它们通常设置为 NULL。 窗口过程必须检查消息标识符来确定如何解释消息参数。
消息类型
本部分介绍两种类型的消息:
System-Defined消息
系统在与应用程序通信时发送或发布 系统定义的消息 。 它使用这些消息来控制应用程序的操作,并为要处理的应用程序提供输入和其他信息。 应用程序还可以发送或发布系统定义的消息。 应用程序通常使用这些消息来控制使用预注册窗口类创建的控件窗口的操作。
每个系统定义的消息都有一个唯一的消息标识符和相应的符号常量 (在软件开发工具包中定义, (SDK) 声明消息用途的头文件) 。 例如, WM_PAINT 常量请求窗口绘制其内容。
符号常量指定系统定义消息所属的类别。 常量前缀标识可以解释和处理消息的窗口类型。 下面是前缀及其相关消息类别。
前缀 | 邮件类别 | 文档 |
---|---|---|
ABM 和 ABN | 应用程序桌面工具栏 | Shell 消息和通知 |
ACM 和 ACN | 动画控件 | 动画控件消息 和 动画控件通知 |
BCM、 BCN、 BM 和 BN | Button 控件 | 按钮控件消息 和 按钮控件通知 |
CB 和 CBN | ComboBox 控件 | ComboBox 控件消息 和 ComboBox 控件通知 |
CBEM 和 CBEN | ComboBoxEx 控件 | ComboBoxEx 消息 和 ComboBoxEx 通知 |
Ccm | 常规控制 | 控制消息 |
CDM | “通用”对话框 | 通用对话框消息 |
Dfm | 默认上下文菜单 | Shell 消息和通知 |
Dl | 拖动列表框 | 拖动列表框通知 |
DM | 默认按钮控件 | 对话框消息 |
DTM 和 DTN | 日期和时间选取器控件 | 日期和时间选取器消息以及日期和时间选取器通知 |
EM 和 EN | 编辑控件 | 编辑控件消息、 编辑控件通知、 丰富编辑消息和 丰富编辑通知 |
HDM 和 HDN | 标头控件 | 标头控件消息 和 标头控件通知 |
HKM | 热键控制 | 热键控制消息 |
IPM 和 IPN | IP 地址控件 | IP 地址消息 和 IP 地址通知 |
LB 和 LBN | 列表框控件 | 列出框消息 和 列表框通知 |
LM | SysLink 控件 | SysLink 控制消息 |
LVM 和 LVN | 列表视图控件 | 列表视图消息 和 列表视图通知 |
MCM 和 MCN | 月历控件 | 月历消息 和 月历通知 |
PBM | 进度条 | 进度栏消息 |
PGM 和 PGN | Pager 控件 | 寻呼控件消息 和 寻呼控件通知 |
PSM 和 PSN | 属性表 | 属性表消息 和 属性表通知 |
RB 和 RBN | Rebar 控件 | Rebar 控件消息 和 Rebar 控件通知 |
SB 和 SBN | 状态栏窗口 | 状态栏消息 和 状态栏通知 |
SBM | 滚动条控件 | 滚动条消息 |
Smc | Shell 菜单 | Shell 消息和通知 |
STM 和 STN | 静态控件 | 静态控件消息 和 静态控件通知 |
TB 和 TBN | 工具栏 | 工具栏控件消息 和 工具栏控件通知 |
TBM 和 TRBN | 跟踪条控件 | 跟踪条控件消息 和 跟踪条控件通知 |
中医 和 TCN | Tab 控件 | 选项卡控件消息 和 选项卡控件通知 |
TDM 和 TDN | “任务”对话框 | 任务对话框消息 和 任务对话框通知 |
TTM 和 TTN | 工具提示控件 | 工具提示控件消息 和 工具提示控件通知 |
TVM 和 TVN | 树视图控件 | 树视图消息 和 树视图通知 |
UDM 和 UDN | 向上-向下控制 | 向上-向下消息 和 向上-向下通知 |
Wm | 常规 |
常规窗口消息涵盖各种信息和请求,包括鼠标和键盘输入、菜单和对话框输入、窗口创建和管理以及动态数据交换 (DDE) 的消息。
Application-Defined消息
应用程序可以创建消息供其自己的窗口使用,或与其他进程中的窗口通信。 如果应用程序创建自己的消息,接收消息的窗口过程必须解释消息并提供适当的处理。
消息标识符值的使用方式如下:
- 系统保留0x0000范围内的消息标识符值,0x03FF (系统定义消息的值 WM_USER – 1) 。 应用程序不能将这些值用于私人消息。
- 范围中的值0x0400 (WM_USER) 的值0x7FFF可用于专用窗口类的消息标识符。
- 如果应用程序标记为版本 4.0,则可以使用范围中的消息标识符值0x8000 (WM_APP 通过0xBFFF) 私人消息。
- 当应用程序调用 RegisterWindowMessage 函数来注册消息时,系统会返回0xC000 0xFFFF范围内的消息标识符。 此函数返回的消息标识符保证在整个系统中是唯一的。 使用此函数可防止其他应用程序出于不同目的使用相同的消息标识符时可能出现的冲突。
消息路由
系统使用两种方法将消息路由到窗口过程:将消息发布到称为 消息队列的先入先出队列,一个临时存储消息的系统定义的内存对象,以及将消息直接发送到窗口过程。
发布到消息队列的消息称为 排队消息。 这些主要是用户通过鼠标或键盘输入的结果,例如 WM_MOUSEMOVE、 WM_LBUTTONDOWN、 WM_KEYDOWN和 WM_CHAR 消息。 其他排队消息包括计时器、画图和退出消息: WM_TIMER、 WM_PAINT和 WM_QUIT。 其他大多数消息(直接发送到窗口过程)称为 未排队消息。
排队的消息
系统一次可以显示任意数量的窗口。 若要将鼠标和键盘输入路由到相应的窗口,系统会使用消息队列。
系统为每个 GUI 线程维护一个系统消息队列和一个特定于线程的消息队列。 为了避免为非 GUI 线程创建消息队列的开销,所有线程最初都是在没有消息队列的情况下创建的。 仅当线程首次调用特定用户函数之一时,系统才会创建线程特定的消息队列;没有 GUI 函数调用会导致创建消息队列。
每当用户移动鼠标、单击鼠标按钮或键盘上的类型时,鼠标或键盘的设备驱动程序会将输入转换为消息,并将其置于系统消息队列中。 系统一次从系统消息队列中删除一个消息,检查它们以确定目标窗口,然后将其发布到创建目标窗口的线程的消息队列。 线程的消息队列接收线程创建的窗口的所有鼠标和键盘消息。 线程从其队列中删除消息,并指示系统将其发送到相应的窗口过程进行处理。
除了 WM_PAINT 消息、 WM_TIMER 消息和 WM_QUIT 消息外,系统始终在消息队列的末尾发布消息。 这可确保窗口按正确的先入先出 (FIFO) 顺序接收其输入消息。 但是, WM_PAINT 消息、 WM_TIMER 消息和 WM_QUIT 消息将保留在队列中,并且仅在队列不包含其他消息时转发到窗口过程。 此外,同一窗口的多个 WM_PAINT 消息合并为单个 WM_PAINT 消息,将工作区的所有无效部分合并到单个区域中。 组合 WM_PAINT 消息可以减少窗口必须重新绘制其工作区内容的次数。
系统通过填充 MSG 结构,然后将其复制到消息队列,将消息发布到线程的消息队列。 MSG 中的信息包括:消息目标窗口的句柄、消息标识符、两个消息参数、消息的发布时间和鼠标光标位置。 线程可以使用 PostMessage 或 PostThreadMessage 函数将消息发布到其自己的消息队列或另一个线程的队列。
应用程序可以使用 GetMessage 函数从其队列中删除消息。 若要检查消息而不将其从队列中删除,应用程序可以使用 PeekMessage 函数。 此函数使用有关消息的信息填充 MSG 。
从队列中删除消息后,应用程序可以使用 DispatchMessage 函数指示系统将消息发送到窗口过程进行处理。 DispatchMessage 采用指向 MSG 的指针,该指针由之前对 GetMessage 或 PeekMessage 函数的调用填充。 DispatchMessage 将窗口句柄、消息标识符和两个消息参数传递给窗口过程,但它不会传递消息的发布时间或鼠标光标位置。 应用程序可以在处理消息时通过调用 GetMessageTime 和 GetMessagePos 函数来检索此信息。
当线程的消息队列中没有消息时,线程可以使用 WaitMessage 函数对其他线程进行控制。 函数挂起线程,直到新消息放入线程的消息队列中才会返回。
可以调用 SetMessageExtraInfo 函数,将值与当前线程的消息队列相关联。 然后调用 GetMessageExtraInfo 函数以获取与 GetMessage 或 PeekMessage 函数检索的最后一条消息关联的值。
未排队的消息
未排队的消息会立即发送到目标窗口过程,绕过系统消息队列和线程消息队列。 系统通常发送未排队的消息,以通知窗口影响它的事件。 例如,当用户激活新的应用程序窗口时,系统会向窗口发送一系列消息,包括 WM_ACTIVATE、 WM_SETFOCUS和 WM_SETCURSOR。 这些消息通知窗口已激活,键盘输入已定向到窗口,鼠标光标已在窗口边框内移动。 当应用程序调用某些系统函数时,也可能导致未排队的消息。 例如,在应用程序使用 SetWindowPos 函数移动窗口后,系统会发送WM_WINDOWPOSCHANGED消息。
发送非排队消息的一些函数包括 BroadcastSystemMessage、 BroadcastSystemMessageEx、 SendMessage、 SendMessageTimeout 和 SendNotifyMessage。
消息处理
应用程序必须删除并处理发布到其线程的消息队列的消息。 单线程应用程序通常在其 WinMain 函数中使用消息循环来删除消息并将其发送到相应的窗口过程进行处理。 具有多个线程的应用程序可以在创建窗口的每个线程中包含一个消息循环。 以下部分介绍了消息循环的工作原理,并说明了窗口过程的角色:
消息循环
一个简单的消息循环由对以下三个函数中的每一个函数的一个函数调用组成: GetMessage、 TranslateMessage 和 DispatchMessage。 请注意,如果出现错误, GetMessage 将返回 –1,因此需要进行特殊测试。
MSG msg;
BOOL bRet;
while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{
if (bRet == -1)
{
// handle the error and possibly exit
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
GetMessage 函数从队列中检索消息,并将其复制到 MSG 类型的结构。 它返回非零值,除非遇到 WM_QUIT 消息,在这种情况下,它返回 FALSE 并结束循环。 在单线程应用程序中,结束消息循环通常是关闭应用程序的第一步。 应用程序可以使用 PostQuitMessage 函数结束其自己的循环,通常用于响应应用程序main窗口的窗口过程中的WM_DESTROY消息。
如果将窗口句柄指定为 GetMessage 的第二个参数,则仅从队列中检索指定窗口的消息。 GetMessage 还可以筛选队列中的消息,仅检索位于指定范围内的那些消息。 有关筛选消息的详细信息,请参阅 消息筛选。
如果线程要从键盘接收字符输入,则线程的消息循环必须包含 TranslateMessage 。 系统 (WM_KEYDOWN生成虚拟密钥 消息,并在 用户每次按下某个键 时WM_KEYUP) 。 虚拟键消息包含一个虚拟键代码,用于标识按下的键,但不包含其字符值。 若要检索此值,消息循环必须包含 TranslateMessage,它将虚拟键消息转换为字符消息 (WM_CHAR) ,并将其重新放入应用程序消息队列。 然后,可以在消息循环的后续迭代时删除字符消息,并将其调度到窗口过程。
DispatchMessage 函数将消息发送到与 MSG 结构中指定的窗口句柄关联的窗口过程。 如果窗口句柄 HWND_TOPMOST, DispatchMessage 会将消息发送到系统中所有顶级窗口的窗口过程。 如果窗口句柄为 NULL, 则 DispatchMessage 对消息不执行任何操作。
应用程序的main线程在初始化应用程序并创建至少一个窗口后启动其消息循环。 启动后,消息循环将继续从线程的消息队列中检索消息,并将其调度到相应的窗口。 当 GetMessage 函数从消息队列中删除 WM_QUIT 消息时,消息循环将结束。
即使应用程序包含多个窗口,消息队列也只需要一个消息循环。 DispatchMessage 始终将消息调度到正确的窗口;这是因为队列中的每个消息都是一个 MSG 结构,其中包含消息所属窗口的句柄。
可以通过多种方式修改消息循环。 例如,可以从队列中检索消息,而无需将它们调度到窗口。 这对于发布未指定窗口的消息的应用程序很有用。 还可以指示 GetMessage 搜索特定消息,将其他消息保留在队列中。 如果必须暂时绕过消息队列的常规 FIFO 顺序,这非常有用。
使用快捷键的应用程序必须能够将键盘消息转换为命令消息。 为此,应用程序的消息循环必须包含对 TranslateAccelerator 函数的调用。 有关快捷键的详细信息,请参阅 键盘加速键。
如果线程使用无模式对话框,则消息循环必须包含 IsDialogMessage 函数,以便对话框可以接收键盘输入。
窗口过程
窗口过程是一个函数,用于接收和处理发送到窗口的所有消息。 每个窗口类都有一个窗口过程,使用该类创建的每个窗口都使用该窗口过程来响应消息。
系统通过将消息数据作为参数传递给过程,将消息发送到窗口过程。 然后,窗口过程对消息执行适当的操作;它检查消息标识符,并在处理消息时使用消息参数指定的信息。
窗口过程通常不会忽略消息。 如果它不处理消息,则必须将消息发送回系统进行默认处理。 窗口过程通过调用 DefWindowProc 函数执行此操作,该函数执行默认操作并返回消息结果。 然后,窗口过程必须将此值作为其自己的消息结果返回。 大多数窗口过程仅处理一些消息,并通过调用 DefWindowProc 将其他消息传递给系统。
由于窗口过程由属于同一类的所有窗口共享,因此它可以处理多个不同窗口的消息。 若要标识受消息影响的特定窗口,窗口过程可以检查随消息一起传递的窗口句柄。 有关窗口过程的详细信息,请参阅 窗口过程。
消息筛选
应用程序可以选择从消息队列 (检索特定消息,同时通过使用 GetMessage 或 PeekMessage 函数指定消息筛选器来忽略) 的其他消息。 筛选器是由第一个和最后一个标识符) 和/或窗口句柄指定的一系列消息标识符 (。 GetMessage 和 PeekMessage 使用消息筛选器选择要从队列中检索的消息。 如果应用程序必须在消息队列中搜索队列中稍后到达的消息,则消息筛选非常有用。 如果应用程序在处理已发布消息之前必须处理输入 (硬件) 消息,它也很有用。
WM_KEYFIRST和WM_KEYLAST常量可用作筛选值来检索所有键盘消息;WM_MOUSEFIRST和WM_MOUSELAST常量可用于检索所有鼠标消息。
筛选消息的任何应用程序必须确保可以发布满足消息筛选器的消息。 例如,如果应用程序在未接收键盘输入的窗口中筛选 WM_CHAR 消息, 则 GetMessage 函数不会返回。 这实际上“挂起”了应用程序。
发布和发送消息
任何应用程序都可以发布和发送消息。 与系统一样,应用程序通过将消息复制到消息队列来发布消息,并通过将消息数据作为参数传递给窗口过程来发送消息。 若要发布消息,应用程序使用 PostMessage 函数。 应用程序可以通过调用 SendMessage、 BroadcastSystemMessage、 SendMessageCallback、 SendMessageTimeout、 SendNotifyMessage 或 SendDlgItemMessage 函数来发送消息。
发布消息
应用程序通常会发布一条消息,通知特定窗口执行任务。 PostMessage 为消息创建 MSG 结构,并将消息复制到消息队列。 应用程序的消息循环最终检索消息并将其调度到相应的窗口过程。
应用程序可以在不指定窗口的情况下发布消息。 如果应用程序在调用 PostMessage 时提供 NULL 窗口句柄,则会将消息发布到与当前线程关联的队列。 由于未指定窗口句柄,因此应用程序必须在消息循环中处理消息。 这是创建应用于整个应用程序(而不是特定窗口)的消息的一种方法。
有时,你可能想要将消息发布到系统中的所有顶级窗口。 应用程序可以通过调用 PostMessage 并在 hwnd 参数中指定HWND_TOPMOST,将消息发布到所有顶级窗口。
常见的编程错误是假定 PostMessage 函数始终发布消息。 当消息队列已满时,这不是事实。 应用程序应检查 PostMessage 函数的返回值,以确定消息是否已发布,如果尚未发布,则重新发布消息。
发送消息
应用程序通常会发送一条消息,通知窗口过程立即执行任务。 SendMessage 函数将消息发送到对应于给定窗口的窗口过程。 函数将等待窗口过程完成处理,然后返回消息结果。 父窗口和子窗口通常通过相互发送消息进行通信。 例如,具有编辑控件作为其子窗口的父窗口可以通过向其发送消息来设置控件的文本。 控件可以通过将消息发送回父窗口,通知父窗口用户执行的文本更改。
SendMessageCallback 函数还会向对应于给定窗口的窗口过程发送消息。 但是,此函数会立即返回。 窗口过程处理消息后,系统会调用指定的回调函数。 有关回调函数的详细信息,请参阅 SendAsyncProc 函数。
有时,你可能想要向系统中的所有顶级窗口发送消息。 例如,如果应用程序更改了系统时间,它必须通过发送 WM_TIMECHANGE 消息来通知所有顶级窗口有关更改的信息。 应用程序可以通过调用 SendMessage 并在 hwnd 参数中指定HWND_TOPMOST,将消息发送到所有顶级窗口。 还可以通过调用 BroadcastSystemMessage 函数并在 lpdwRecipients 参数中指定BSM_APPLICATIONS,将消息广播到所有应用程序。
通过使用 InSendMessage 或 InSendMessageEx 函数,窗口过程可以确定它是否正在处理另一个线程发送的消息。 当消息处理依赖于消息来源时,此功能非常有用。
消息死锁
在接收消息的窗口过程返回之前,调用 SendMessage 函数将消息发送到另一个线程的线程无法继续执行。 如果接收线程在处理消息时生成控件,则发送线程无法继续执行,因为它正在等待 SendMessage 返回。 如果接收线程附加到发送方所在的同一队列,则可能导致应用程序死锁。 (请注意,日志挂钩会将线程附加到同一队列。)
请注意,接收线程不需要显式生成控制;调用以下任何函数都可能导致线程隐式产生控件。
- DialogBox
- DialogBoxIndirect
- DialogBoxIndirectParam
- DialogBoxParam
- GetMessage
- MessageBox
- PeekMessage
- SendMessage
为了避免应用程序中出现潜在的死锁,请考虑使用 SendNotifyMessage 或 SendMessageTimeout 函数。 否则,窗口过程可以通过调用 InSendMessage 或 InSendMessageEx 函数来确定它收到的消息是由另一个线程发送的。 在处理消息时调用上述列表中的任何函数之前,窗口过程应首先调用 InSendMessage 或 InSendMessageEx。 如果此函数返回 TRUE,则窗口过程必须在导致线程产生控制的任何函数之前调用 ReplyMessage 函数。
广播消息
每条消息由一个消息标识符和两个参数( wParam 和 lParam)组成。 消息标识符是指定消息用途的唯一值。 参数提供特定于消息的其他信息,但 wParam 参数通常是一个类型值,用于提供有关消息的详细信息。
消息广播只是向系统中的多个收件人发送消息。 若要广播来自应用程序的消息,请使用 BroadcastSystemMessage 函数,指定消息的收件人。 必须指定一个或多个类型的收件人,而不是指定单个收件人。 这些类型包括应用程序、可安装的驱动程序、网络驱动程序和系统级设备驱动程序。 系统将广播消息发送到每个指定类型的所有成员。
系统通常广播消息以响应系统级设备驱动程序或相关组件中发生的更改。 驱动程序或相关组件将消息广播到应用程序和其他组件,以通知它们更改。 例如,每当软盘驱动器的设备驱动程序检测到媒体更改时(例如当用户在驱动器中插入磁盘时),负责磁盘驱动器的组件会广播一条消息。
系统按以下顺序将消息广播给收件人:系统级设备驱动程序、网络驱动程序、可安装的驱动程序和应用程序。 这意味着,系统级设备驱动程序(如果选择为收件人)始终获得对邮件做出响应的第一个机会。 在给定的收件人类型中,任何驱动程序都不能保证在任何其他驱动程序之前接收给定消息。 这意味着,用于特定驱动程序的消息必须具有全局唯一的消息标识符,以便其他驱动程序不会无意中对其进行处理。
还可以通过在 SendMessage、SendMessageCallback、SendMessageTimeout 或 SendNotifyMessage 函数中指定HWND_BROADCAST,将消息广播到所有顶级窗口。
应用程序通过其顶级窗口的窗口过程接收消息。 消息不会发送到子窗口。 服务可以通过窗口过程或其服务控制处理程序接收消息。
注意
系统级设备驱动程序使用相关的系统级函数来广播系统消息。
查询消息
可以创建自己的自定义消息,并使用它们协调应用程序与系统中其他组件之间的活动。 如果已创建自己的可安装驱动程序或系统级设备驱动程序,这尤其有用。 自定义消息可以向/从驱动程序以及使用该驱动程序的应用程序传递信息。
若要轮询收件人执行给定操作的权限,请使用 查询消息。 调用 BroadcastSystemMessage 时,可以通过在 dwFlags 参数中设置BSF_QUERY值来生成自己的查询消息。 查询消息的每个收件人都必须返回 TRUE ,函数才能将消息发送到下一个收件人。 如果任何接收方 返回BROADCAST_QUERY_DENY,则广播将立即结束,函数将返回零。