关于消息和消息队列

与基于 MS-DOS 的应用程序不同,基于Windows的应用程序是事件驱动的。 它们不会 (进行显式函数调用,例如 C 运行时库调用) 以获取输入。 相反,他们等待系统向其传递输入。

系统将应用程序的所有输入传递到应用程序中的各个窗口。 每个窗口都有一个函数,称为窗口过程,每当系统具有窗口的输入时,系统都会调用该函数。 窗口过程处理输入并将控件返回到系统。 有关窗口过程的详细信息,请参阅 窗口过程

如果顶级窗口停止响应消息超过几秒钟,系统将认为窗口不会响应。 在这种情况下,系统会隐藏窗口,并将其替换为具有相同 Z 顺序、位置、大小和视觉属性的幽灵窗口。 这样,用户就可以移动它、调整大小,甚至关闭应用程序。 但是,这些操作是唯一可用的操作,因为应用程序实际上没有响应。 在调试器模式下,系统不会生成虚影窗口。

本部分讨论以下主题:

Windows消息

系统以 消息的形式将输入传递给窗口过程。 消息由系统和应用程序生成。 系统在每个输入事件(例如,用户键入时、移动鼠标或单击滚动条等控件)生成消息。 系统还会生成消息,以响应应用程序带来的系统更改,例如应用程序更改系统字体资源的池或调整其窗口的大小。 应用程序可以生成消息,以指示其自己的窗口执行任务或与其他应用程序中的窗口通信。

系统使用一组四个参数将消息发送到窗口过程:一个窗口句柄、一个消息标识符和两个名为 消息参数的值。 窗口句柄标识消息的预期窗口。 系统使用它来确定应接收消息的窗口过程。

消息标识符是标识消息用途的命名常量。 当窗口过程收到消息时,它使用消息标识符来确定如何处理消息。 例如,消息标识符 WM_PAINT 告知窗口过程窗口的工作区已更改,必须重新绘制。

消息参数指定处理消息时窗口过程使用的数据或数据的位置。 消息参数的含义和值取决于消息。 消息参数可以包含整数、打包位标志、指向包含其他数据的结构的指针等。 当消息不使用消息参数时,它们通常设置为 NULL。 窗口过程必须检查消息标识符以确定如何解释消息参数。

消息类型

本部分介绍两种类型的消息:

System-Defined消息

当系统与应用程序通信时,系统会发送或发布 系统定义的消息 。 它使用这些消息来控制应用程序的操作,并为要处理的应用程序提供输入和其他信息。 应用程序还可以发送或帖子系统定义的消息。 应用程序通常使用这些消息来控制通过使用预注册的窗口类创建的控件窗口的操作。

每个系统定义消息都具有唯一的消息标识符和在软件开发工具包 (SDK 中定义的相应符号 (常量) 头文件) 声明消息目的。 例如,窗口绘制其内容的 WM_PAINT 常量请求。

符号常量指定系统定义消息所属的类别。 常量前缀标识可以解释和处理消息的窗口类型。 下面是前缀及其相关消息类别。

前缀 消息类别 文档
ABMABN 应用程序桌面工具栏 Shell 消息和通知
ACMACN 动画控件 动画控件消息动画控件通知
BCMBCNBMBN Button 控件 按钮控件消息按钮控件通知
CBCBN ComboBox 控件 ComboBox 控件消息组合框控件通知
CBEMCBEN ComboBoxEx 控件 ComboBoxEx 消息ComboBoxEx 通知
CCM 常规控制 控制消息
CDM “常见”对话框 常见对话框消息
DFM 默认上下文菜单 Shell 消息和通知
DL 拖动列表框 拖动列表框通知
DM 默认推送按钮控件 对话框消息
DTMDTN 日期和时间选取器控件 日期和时间选取器消息以及日期和时间选取器通知
EMEN 编辑控件 编辑控件消息编辑控件通知富编辑消息富编辑通知
HDMHDN 标头控件 标头控件消息标头控件通知
HKM 热键控制 热键控制消息
IPMIPN IP 地址控件 IP 地址消息IP 地址通知
LBLBN 列表框控件 列表框消息列表框通知
LM SysLink 控件 SysLink 控制消息
LVMLVN 列表视图控件 列表视图消息列表视图通知
MCMMCN 月历控件 月历消息月历通知
PBM 进度栏 进度栏消息
PGMPGN 寻呼控件 寻呼控件消息寻呼控件通知
PSMPSN 属性表 属性表消息属性表通知
RBRBN Rebar 控件 Rebar 控件消息Rebar 控件通知
SBSBN 状态栏窗口 状态栏消息状态栏通知
SBM 滚动条控件 滚动条消息
SMC Shell 菜单 Shell 消息和通知
STMSTN 静态控件 静态控件消息静态控件通知
TBTBN 工具栏 工具栏控件消息工具栏控件通知
TBMTRBN Trackbar 控件 跟踪栏控件消息跟踪栏控件通知
中医TCN Tab 控件 选项卡控件消息选项卡控件通知
TDMTDN 任务对话框 任务对话框消息任务对话通知
TTMTTN 工具提示控件 工具提示控件消息工具提示控件通知
TVMTVN 树视图控件 树视图消息树视图通知
UDMUDN 向上控件 向上关闭消息向上通知
WM 常规
剪贴板消息
剪贴板通知
常见对话框通知
游标通知
数据复制消息
桌面窗口管理器消息
设备管理消息
对话框通知
动态数据Exchange消息
动态数据Exchange通知
挂钩通知
键盘加速器消息
键盘加速器通知
键盘输入消息
键盘输入通知
菜单通知
鼠标输入通知
多个文档接口消息
原始输入通知
滚动条通知
计时器通知
窗口消息
窗口通知

常规窗口消息涵盖各种信息和请求,包括鼠标和键盘输入的消息、菜单和对话框输入、窗口创建和管理以及动态数据Exchange (DDE) 。

Application-Defined消息

应用程序可以创建消息,供其自己的窗口使用,或与其他进程中的窗口通信。 如果应用程序创建自己的消息,则接收它们的窗口过程必须解释消息并提供适当的处理。

消息标识符值如下所示:

  • 系统通过0x03FF (系统定义消息WM_USER - 1) 的值,在范围0x0000中保留消息标识符值。 应用程序不能将这些值用于专用消息。
  • 范围中的值0x0400 (通过0x7FFF WM_USER) 的值可用于专用窗口类的消息标识符。
  • 如果应用程序标记为版本 4.0,则可以通过专用消息的0xBFFF,在0x8000 (WM_APP) 范围内使用消息标识符值。
  • 当应用程序调用 RegisterWindowMessage 函数以注册消息时,系统会在范围0xC000中返回消息标识符0xFFFF。 保证此函数返回的消息标识符在整个系统中是唯一的。 如果其他应用程序出于不同目的使用相同的消息标识符,则使用此函数可防止发生冲突。

消息路由

系统使用两种方法将消息路由到窗口过程:将消息发布到一个名为 消息队列的先出队列、一个临时存储消息的系统定义的内存对象,以及将消息直接发送到窗口过程。

发布到消息队列的消息称为 排队消息。 这些主要是通过鼠标或键盘输入的用户输入的结果,例如 WM_MOUSEMOVEWM_LBUTTONDOWNWM_KEYDOWNWM_CHAR 消息。 其他排队消息包括计时器、绘制和退出消息: WM_TIMERWM_PAINTWM_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 使用指向之前调用 GetMessagePeekMessage 函数填充的 MSG 的指针。 DispatchMessage 将窗口句柄、消息标识符和两个消息参数传递给窗口过程,但它不会传递消息发布时间或鼠标光标位置。 应用程序可以通过在处理消息时调用 GetMessageTimeGetMessagePos 函数来检索此信息。

当线程在其消息队列中没有消息时,线程可以使用 WaitMessage 函数向其他线程生成控制。 该函数会暂停线程,并且不会返回,直到新消息放置在线程的消息队列中。

可以调用 SetMessageExtraInfo 函数,将值与当前线程的消息队列相关联。 然后调用 GetMessageExtraInfo 函数以获取与 GetMessage 或PeekMessage 函数检索的最后一条消息关联的值。

未排队的消息

未排队的消息会立即发送到目标窗口过程,绕过系统消息队列和线程消息队列。 系统通常会发送非排队消息,以通知影响它的事件的窗口。 例如,当用户激活新的应用程序窗口时,系统会发送一系列消息,包括 WM_ACTIVATEWM_SETFOCUSWM_SETCURSOR。 这些消息通知窗口已激活,键盘输入将定向到窗口,并且鼠标光标已在窗口边框内移动。 当应用程序调用某些系统函数时,非排队消息也可能会导致。 例如,系统在应用程序使用 SetWindowPos 函数移动窗口后发送WM_WINDOWPOSCHANGED消息。

发送非排队消息的某些函数包括 BroadcastSystemMessageBroadcastSystemMessageExSendMessage、SendMessageTimeoutSendNotifyMessage

消息处理

应用程序必须删除并处理发布到其线程的消息队列的消息。 单线程应用程序通常在其 WinMain 函数中使用消息循环来删除消息并将其发送到相应的窗口过程进行处理。 具有多个线程的应用程序可以在创建窗口的每个线程中包含一个消息循环。 以下各节介绍了消息循环的工作原理和说明窗口过程的角色:

消息Loop

一个简单的消息循环由以下三个函数中的一个函数调用组成: GetMessageTranslateMessageDispatchMessage。 请注意,如果出现错误, 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 函数结束自己的循环,这通常是在应用程序主窗口的窗口过程中响应 WM_DESTROY 消息。

如果将窗口句柄指定为 GetMessage 的第二个参数,则仅从队列中检索指定窗口的消息。 GetMessage 还可以筛选队列中的消息,只检索属于指定范围的消息。 有关筛选消息的详细信息,请参阅 消息筛选

如果线程要从键盘接收字符输入,则线程的消息循环必须包含 TranslateMessage 。 每次用户按下键时,系统会在 (WM_KEYDOWN 生成虚拟键消息, 并WM_KEYUP) 。 虚拟键消息包含一个虚拟键代码,用于标识按下的键,但不标识其字符值。 若要检索此值,消息循环必须包含 TranslateMessage,它将虚拟密钥消息转换为字符消息 (WM_CHAR) 并将其重新放入应用程序消息队列中。 然后,可以在消息循环的后续迭代和调度到窗口过程中删除字符消息。

DispatchMessage 函数向与 MSG 结构中指定的窗口句柄关联的窗口过程发送消息。 如果窗口句柄 HWND_TOPMOSTDispatchMessage 会将消息发送到系统中所有顶级窗口的窗口过程。 如果窗口句柄为 NULLDispatchMessage 不会对消息执行任何操作。

应用程序的主线程在初始化应用程序并创建至少一个窗口后启动其消息循环。 启动后,消息循环将继续从线程的消息队列中检索消息,并将其调度到相应的窗口。 当 GetMessage 函数从消息队列中删除 WM_QUIT 消息时,消息循环将结束。

即使应用程序包含多个窗口,消息队列也需要一个消息循环。 DispatchMessage 始终将消息调度到正确的窗口;这是因为队列中的每个消息都是一个 MSG 结构,其中包含消息所属窗口的句柄。

可以通过多种方式修改消息循环。 例如,你可以从队列中检索消息,而无需将它们调度到窗口。 这对于帖子未指定窗口的消息的应用程序非常有用。 还可以指示 GetMessage 搜索特定消息,并将其他消息留在队列中。 如果必须暂时绕过消息队列的通常 FIFO 顺序,则这非常有用。

使用快捷键的应用程序必须能够将键盘消息转换为命令消息。 为此,应用程序的消息循环必须包含对 TranslateAccelerator 函数的调用。 有关快捷键的详细信息,请参阅 键盘加速器

如果线程使用无模式对话框,则消息循环必须包含 IsDialogMessage 函数,以便对话框可以接收键盘输入。

窗口过程

窗口过程是一个函数,用于接收并处理发送到该窗口的所有消息。 每个窗口类都有一个窗口过程,使用该类创建的每个窗口都使用相同的窗口过程来响应消息。

系统通过将消息数据作为参数传递给过程,将消息发送到窗口过程。 然后,窗口过程为消息执行适当的操作;它检查消息标识符,处理消息时使用消息参数指定的信息。

窗口过程通常不会忽略消息。 如果未处理消息,则必须将消息发送回系统进行默认处理。 窗口过程通过调用 DefWindowProc 函数执行此操作,该函数执行默认操作并返回消息结果。 然后,窗口过程必须返回此值作为其自己的消息结果。 大多数窗口过程仅处理一些消息,并通过调用 DefWindowProc 将其他人传递给系统。

由于窗口过程由属于同一类的所有窗口共享,因此它可以处理多个不同窗口的消息。 若要标识受消息影响的特定窗口,窗口过程可以检查通过消息传递的窗口句柄。 有关窗口过程的详细信息,请参阅 窗口过程

消息筛选

应用程序可以通过使用 GetMessagePeekMessage 函数指定消息筛选器来选择要从消息队列 (检索的特定消息,同时忽略其他消息) 。 筛选器是由第一个和最后一个标识符) 、窗口句柄或两者指定的消息标识符范围 (。 GetMessagePeekMessage 使用消息筛选器选择要从队列检索的消息。 如果应用程序必须搜索消息队列中稍后到达队列的消息,则消息筛选非常有用。 如果应用程序在处理已发布的消息之前必须处理输入 (硬件) 消息,则它还很有用。

WM_KEYFIRSTWM_KEYLAST常量可用作筛选值来检索所有键盘消息;WM_MOUSEFIRSTWM_MOUSELAST常量可用于检索所有鼠标消息。

筛选消息的任何应用程序都必须确保可以发布满足消息筛选器的消息。 例如,如果应用程序筛选未接收键盘输入的窗口中 的WM_CHAR 消息, 则 GetMessage 函数不会返回。 这实际上会“挂起”应用程序。

发布和发送消息

任何应用程序都可以帖子并发送消息。 与系统一样,应用程序通过将消息复制到消息队列来发布消息,并通过将消息数据作为参数传递给窗口过程来发送消息。 若要帖子消息,应用程序使用 PostMessage 函数。 应用程序可以通过调用 SendMessageBroadcastSystemMessageSendMessageCallbackSendMessageTimeoutSendNotifyMessageSendDlgItemMessage 函数来发送消息。

发布消息

应用程序通常会发布消息以通知特定窗口执行任务。 PostMessage 为消息创建 MSG 结构,并将消息复制到消息队列。 应用程序的消息循环最终会检索该消息,并将其调度到相应的窗口过程。

应用程序无需指定窗口即可帖子消息。 如果应用程序在调用 PostMessage 时提供 NULL 窗口句柄,则会将消息发布到与当前线程关联的队列。 由于未指定窗口句柄,因此应用程序必须在消息循环中处理消息。 这是创建应用于整个应用程序而不是特定窗口的消息的一种方法。

有时,可能需要将消息帖子系统中的所有顶级窗口。 应用程序可以通过调用 PostMessage 并在 hwnd 参数中指定HWND_TOPMOST,将消息帖子到所有顶级窗口。

常见的编程错误是假定 PostMessage 函数始终发布消息。 当消息队列已满时,情况并非如此。 应用程序应检查 PostMessage 函数的返回值,以确定消息是否已发布,如果尚未发布,请重新发布消息。

发送消息

应用程序通常会发送一条消息来通知窗口过程以立即执行任务。 SendMessage 函数将消息发送到对应于给定窗口的窗口过程。 该函数会等待窗口过程完成处理,然后返回消息结果。 父窗口和子窗口通常通过相互发送消息进行通信。 例如,具有编辑控件作为其子窗口的父窗口可以通过向其发送消息来设置控件的文本。 控件可以通过将消息发送回父窗口来通知父窗口对用户执行的文本所做的更改。

SendMessageCallback 函数还会向对应于给定窗口的窗口过程发送消息。 但是,此函数会立即返回。 在窗口过程处理消息后,系统调用指定的回调函数。 有关回调函数的详细信息,请参阅 SendAsyncProc 函数。

有时,你可能希望向系统中的所有顶级窗口发送消息。 例如,如果应用程序更改系统时间,则必须通过发送 WM_TIMECHANGE 消息通知所有顶级窗口。 应用程序可以通过调用 SendMessage 并在 hwnd 参数中指定HWND_TOPMOST,将消息发送到所有顶级窗口。 还可以通过调用 BroadcastSystemMessage 函数并在 lpdwRecipients 参数中指定BSM_APPLICATIONS,向所有应用程序广播消息。

通过使用 InSendMessageInSendMessageEx 函数,窗口过程可以确定是否正在处理另一个线程发送的消息。 当消息处理取决于消息的来源时,此功能非常有用。

消息死锁

调用 SendMessage 函数向另一个线程发送消息的线程无法继续执行,直到接收消息的窗口过程返回。 如果接收线程在处理消息时生成控制,则发送线程无法继续执行,因为它正在等待 SendMessage 返回。 如果接收线程附加到与发送方相同的队列,则可能会导致应用程序死锁发生。 (请注意,日记挂钩将线程附加到同一个 queue.)

请注意,接收线程不需要显式生成控制;调用以下任一函数可能会导致线程隐式生成控制。

若要避免应用程序中潜在的死锁,请考虑使用 SendNotifyMessageSendMessageTimeout 函数。 否则,窗口过程可以通过调用 InSendMessageInSendMessageEx 函数来确定它收到的消息是由另一个线程发送的。 在处理消息时调用上述列表中的任何函数之前,窗口过程应首先调用 InSendMessageInSendMessageEx。 如果此函数返回 TRUE,则窗口过程必须在导致线程产生控制的任何函数之前调用 ReplyMessage 函数。

广播消息

每个消息都包含一个消息标识符和两个参数 ,wParamlParam。 消息标识符是指定消息用途的唯一值。 这些参数提供特定于消息的其他信息,但 wParam 参数通常是一个类型值,可提供有关消息的详细信息。

消息广播只是将邮件发送到系统中的多个收件人。 若要从应用程序广播消息,请使用 BroadcastSystemMessage 函数,指定消息的收件人。 必须指定一个或多个收件人类型,而不是指定单个收件人。 这些类型是应用程序、可安装驱动程序、网络驱动程序和系统级设备驱动程序。 系统将广播消息发送到每个指定类型的所有成员。

系统通常会广播消息,以响应系统级别设备驱动程序或相关组件中发生的更改。 驱动程序或相关组件将消息广播给应用程序和其他组件,以通知它们更改。 例如,当软盘驱动器的设备驱动程序检测到媒体更改(例如用户在驱动器中插入磁盘时)时,负责磁盘驱动器的组件就会广播消息。

系统按以下顺序将消息广播给收件人:系统级设备驱动程序、网络驱动程序、可安装驱动程序和应用程序。 这意味着系统级设备驱动程序(如果选择为收件人)始终获得第一个响应消息的机会。 在给定收件人类型中,不会保证在任何其他驱动程序之前接收给定邮件。 这意味着,用于特定驱动程序的消息必须具有全局唯一的消息标识符,以便其他驱动程序无意中处理它。

还可以通过在 SendMessageSendMessageCallbackSendMessageTimeoutSendNotifyMessage 函数中指定HWND_BROADCAST,将消息广播到所有顶级窗口。

应用程序通过其顶级窗口的窗口过程接收消息。 消息不会发送到子窗口。 服务可以通过窗口过程或其服务控制处理程序接收消息。

注意

系统级设备驱动程序使用相关的系统级函数来广播系统消息。

查询消息

可以创建自己的自定义消息,并使用它们协调应用程序与系统中其他组件之间的活动。 如果已创建自己的可安装驱动程序或系统级设备驱动程序,这尤其有用。 自定义消息可以携带驱动程序和使用驱动程序的应用程序的信息。

若要轮询收件人以执行给定操作的权限,请使用 查询消息。 调用 BroadcastSystemMessage 时,可以通过在 dwFlags 参数中设置BSF_QUERY值来生成自己的查询消息。 查询邮件的每个收件人都必须返回 TRUE ,函数才能将邮件发送到下一个收件人。 如果任何收件人返回 BROADCAST_QUERY_DENY,则广播将立即结束,函数返回零。