Windows消息机制

本文详细介绍了Windows消息机制,包括消息类型(系统定义和应用程序定义)、路由(列队和非列队)、循环,以及发布、发送、广播和查询消息的方法。还提及了消息死锁问题及避免方法,如使用特定函数和检查消息来源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Windows消息
系统以消息的形式将输入传递给窗口过程(在win32应用程序中一般为WndProc)。消息由系统和应用程序生成。系统对每个输入事件产生一条消息。例如,当用户敲击键盘、移动鼠标或单击滚动条等控件时,系统将产生消息以响应应用程序对系统所带来的变动。当应用程序更改系统字体资源或调整其中一个窗口的大小时,应用程序通过产生消息来指示windows 执行任务或与其它应用程序的窗口 通信。
系统向窗口过程发送一条消息,其中包含四个参数:窗口句柄(hwnd)、消息 (message)、消息参数(wParam和lParam)。该窗口句柄标识了发送消息所需的窗口,系统使用它来确定哪个窗口过程应该接收消息。
消息标识符是一个常量名,其标识了消息的用途。它使用消息标识符来确定如何处理该消息。例如,该消息标识符WM_PAINT告诉窗口过程,窗口的客户区已改变,必须重新绘制。
消息参数指定窗口过程处理消息时所使用的数据或数据的位置。消息参数的意义和值取决于消息(message)。消息参数可以是整数,已对齐的位段,指向包含其它数据结构的指针,等等。当消息不使用消息参数时,它们通常设置为NULL。窗口程序必须检查消息标识符,以确定如何解释消息参数。
消息类型
系统定义的消息
系统与应用程序通信时发送或发布系统定义的消息,它使用这些消息来控制应用程序的操作,并提供输入和其他信息为应用程序处理。应用程序还可以发送或发布系统定义的消息。应用程序通常使用这些消息来控制使用预注册窗口类创建的控制窗口的操作。
每个系统定义的消息都具有唯一的消息标识符和相应的符号常量(在软件开发工具包(SDK)头文件中定义),该消息表示消息(message)的目的。例如,WM_PAINT常数要求窗口绘制其内容。
符号常量指定系统定义消息所属的类别。常量的前缀标识可以解释和处理消息的窗口类型。以下是前缀及其相关的消息类别。消息前缀.docx
通常窗口消息涵盖广泛的信息和请求,包括的消息有鼠标和键盘输入,菜单和对话框输入,窗口创建和管理以及动态数据交换(DDE)。
应用程序定义的消息
应用程序可以创建消息供其自己的窗口使用,或在其它进程中与窗口通信。如果应用程序创建自己的消息,则接收它们的窗口过程必须解释这些消息并提供适当的处理。
消息标识符的值使用方式如下:
 在范围0x0000到0x03FF内为系统定义的消息,供系统使用,应用程序不能使用这些值;
 0x0400(WM_USER的值)到0x7FFF范围内的值可用于私有窗口类的消息标识符;
 如果你的应用程序为版本4.0,则可以使用0x8000(WM_APP)到0xBFFF范围内的消息标识符。
 当应用程序调用RegisterWindowMessage函数注册消息时,系统返回0xC000到0xFFFF范围内的消息标识符。
此函数返回的值保证在整个系统中是唯一的。如果其他应用程序出于不同目的使用相同的消息标识符,则使用此函数可防止可能出现的冲突。
消息路由
系统使用两种方法将消息路由(route)到窗口过程:将消息发送到名为消息队列的先进先出队列,临时存储消息的系统定义的内存对象,以及将消息直接发送到窗口过程。
发布到消息队列的消息称为列队消息(queued message)。这些主要是用户通过鼠标或键盘输入的结果,例如WM_MOUSEMOVE、WM_LBUTTONDOWN,WM_KEYDOWN和WM_CHAR消息。其他列队消息包括计时器、绘制和退出消息:WM_TIMER,WM_PAINT和WM_QUIT。大多数其它消息直接发送到窗口过程的消息称为非列队消息(nonqueued messages)。
列队消息
系统一次可以显示任意数量的窗口。 为了将鼠标和键盘输入路由到适当的窗口,系统将使用消息队列。
系统为每个GUI线程维护单个系统消息队列和一个特定于线程的消息队列。为了避免为非GUI线程创建消息队列的开销,最初创建的所有线程都没有消息队列。只有当线程第一次调用某个特定的用户函数时,系统才会创建线程特定的消息队列;没有GUI函数调用会导致消息队列的创建。
每当用户移动鼠标、单击鼠标按钮或在键盘上输入时,鼠标或键盘的设备驱动程序会将输入转换为消息,并将其放入系统消息队列。系统从系统消息队列中一次移除一条消息,检查它们以确定目标窗口,然后将它们发布到创建目标窗口的线程的消息队列。线程的消息队列接收由线程创建的windows 的所有鼠标和键盘消息。线程从其队列中移除消息,并指示系统将它们发送到适当的窗口过程进行处理。
除了WM_PAINT消息、WM_TIMER消息和WM_QUIT消息外,系统总是在消息队列的末尾发布消息。这样可以确保窗口以正确的先进先出(FIFO)顺序接收其输入消息。但是,WM_TIMER消息、WM_TIMER消息和WM_QUIT消息保留在队列中,并且仅当队列不包含其他消息时才会转发到窗口过程。此外,同一窗口的多个WM_PAINT消息合并为一个WM_PAINT消息,将客户区的所有无效部分合并为一个区域。组合WM_PAINT消息可以减少窗口重新绘制工作区的次数。
系统通过填充SMG结构,然后将其复制到消息队列,将消息发布到线程的消息队列。SMG中的信息包括:消息所针对的窗口句柄、消息标识符、两个消息参数、消息发布时间和鼠标光标位置。线程可以使用PostMessage或PostThreadMessage函数将消息发布到自己的消息队列或其他线程的队列。
应用程序可以使用GetMessage函数从队列中移除消息。要在不将消息从队列中移除的情况下检查消息,应用程序可以使用PeekMessage函数。此函数用MSG填充有关消息的信息。
从队列中移除消息后,应用程序可以使用DispatchMessage函数指导系统将消息发送到窗口过程进行处理。DispatchMessage接受指向MSG的指针,该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)
{
// 处理错误并可能退出
}
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_TOPMOST,则DispatchMessage将消息发送到系统中所有顶层窗口 的窗口过程。如果窗口句柄为NULL,则DispatchMessage与消息无关。

应用程序的主线程在初始化应用程序并创建至少一个窗口后启动其消息循环。启动之后,消息循环继续从线程的消息队列中检索消息,并将它们发送到相应的窗口。当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时提供空窗口句柄,则消息将被发布到与当前线程关联的队列中。由于未指定窗口句柄,应用程序必须在消息循环中处理消息。这是创建应用于整个应用程序而不是特定窗口的消息的一种方法。
有时,你可能希望向系统中的所有顶层窗口发送消息。应用程序可以通过调用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
要避免应用程序中出现潜在的死锁,请考虑使用SendNotifyMessag或SendMessageTimeout函数。否则,窗口过程可以通过调用InSendMessage或InSendMessageEx函数来确定它收到的消息是由另一个线程发送的。在处理消息时调用前面列表中的任何函数之前,窗口过程应首先调用InsendMessage或InsendMessageEx。如果此函数返回true,则窗口过程必须在使线程产生控制权的任何函数之前调用ReplyMessage函数。
广播消息
每个消息都由一个消息标识符和两个参数wParam和lParam组成。消息标识符是指定消息用途的唯一值。参数提供特定消息的附加信息,但wParam参数通常是一个类型值,用于提供有关消息的更多信息。
消息的广播只是将消息发送给系统中的多个接收器。要从应用程序广播消息,请使用BroadcastSystemMessage函数,指定消息的接收器。你必须指定一种或多种类型的接收器,而不是指定单个接收器。这些类型包括应用程序、可安装驱动程序、网络驱动程序和系统级设备驱动程序。系统向每个指定类型的所有成员发送广播消息。
系统通常根据系统级设备驱动程序或相关组件中发生的变化广播消息。驱动程序或相关组件将消息广播到应用程序和其它组件,以通知它们已改变。例如,负责磁盘驱动器的组件在软盘驱动器的设备驱动程序检测到介质更改时(如用户在驱动器中插入磁盘时)广播消息。
系统按以下顺序向接收器广播消息:系统级设备驱动程序、网络驱动程序、可安装驱动程序和应用程序。这意味着,如果选择系统级设备驱动程序作为接收器,则总能得到第一个响应消息的机会。在指定的接收器类型中,不保证驱动程序在其它驱动程序之前接收指定的消息。这意味着针对特定驱动程序的消息必须具有全局唯一的消息标识符,这样其他驱动程序就不会无意中处理它。
你还可以通过在SendMessage,SendMessageCallback,SendMessageTimeout,或 SendNotifyMessage函数中指定HWND_BROADCAST,将消息广播到所有顶层窗口。
应用程序通过其顶层窗口的窗口过程接收消息。消息不会发送到子窗口。服务器可以通过窗口过程或其服务控制程序(service control handlers)接收消息。
注释
系统级设备驱动程序使用相关的系统级函数来广播系统消息。
查询消息
你可以创建自己的自定义消息,并使用它们来协调应用程序和系统中其它组件之间的活动。如果你已经创建了自己的可安装驱动程序或系统级设备驱动程序,此功能非常有用。自定义消息可以在驱动程序和使用驱动程序的应用程序之间传递信息。
若要轮询(poll)接收器以获得执行指定操作的权限,请使用查询消息。调用BroadcastSystemMessage时,可以通过在dwFlags参数中设置BSF_QUERY 值来生成自己的查询消息。查询消息的每个接收器必须返回true,函数才能将消息发送给下一个接收器。如果任何接收器返回BROADCAST_QUERY_DENY,则广播立即结束,函数返回零。

消息循环:
应用程序通过执行一段名为“消息循环”的代码段来从消息列队中获取消息。

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

该循环中 GetMessage() 的函数原型为:
BOOL WINAPI GetMessage(
Out LPMSG lpMsg,
In_opt HWND hWnd,
In UINT wMsgFilterMin,
In UINT wMsgFilterMax);

lpMsg指向MSG结构的指针,该结构从线程的消息队列里接收信息。
hWnd需要获取消息的窗口的句柄,该窗口必须属于当前线程。当其值取NULL时,
wMsgFilterMin指定被检索的最小消息值的整数。
wMsgFilterMax指定被检索的最大消息值的整数。
GetMessage函数:
该函数为任何属于调用该线程的窗口检索消息,线程消息通过PostThreadMessage函数寄送给调用线程。
返回值:
如果函数取得WM_QUIT之外的其他消息,返回非零值。如果函数取得WM_QUIT消息,返回值是零。如果出现了错误,返回值是-1。例如,当hWnd是无效的窗口句柄或lpMsg是无效的指针时。若想获得更多的错误信息,请调用GetLastError函数。

BOOL TranslateMessage(CONST MSG * lpMsg);
该函数将虚拟键消息转换为字符消息。字符消息被寄送到调用线程的消息队列里,当下一次线程调用函数GetMessage或PeekMessage时被读出。
返回值:
如果消息被转换(即,字符消息被送到线程的消息队列中),返回非零值。
如果消息是 WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, 或 WM_SYSKEYUP,返回非零值,不考虑转换。
如果消息没有转换(即,字符消息没被送到线程的消息队列中),返回值是0。

LONG DispatchMessage(CONST MSG * lpmsg);
该函数调度一个消息给窗口程序。通常调度从GetMessage取得的消息。消息被调度到的窗口程序即是MainProc()函数。
返回值:
返回值是窗口程序返回的值。尽管返回值的含义依赖于被调度的消息,但返回值通常被忽略。

其中msg是一个结构变量,类型为MSG,其定义如下:
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
DWORD lPrivate;
} MSG, *PMSG;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值