参考:http://msdn.microsoft.com/en-us/library/windows/desktop/ms644927(v=vs.85).aspx
与基于DOS的应用程序不同,基于Windows的应用程序是事件驱动的。他们不显式指定功能调用(如C运行时库的调用)来获得输入,相反地,他们会等待系统来传递输入。系统为应用程序中的每个窗口传递所有的输入。每个窗口都有一个称为窗口函数的函数,每当有针对一个窗口的输入时系统就会调用它。窗口函数处理该输入然后返回控制权到系统。有关更多信息,请参见Window Procedures.
当一个顶级窗口停止对消息的响应超过几秒后,系统会认为该窗口无响应。此时,系统会隐藏该窗口并用一个有着相同Z序、位置、尺寸和可视化效果的克隆窗口来替代它以保证用户可以移动、更改尺寸,还可以关闭程序。因为程序实际上并没有响应,所以仅有这些可用的功能。处于调试模式时系统不会创建克隆窗口。
该节中会讨论以下主题s:
- 窗口消息
- 消息类型
- 系统定义消息
- 应用程序定义消息
- 消息路由
- 队列消息
- 非队列消息
- 消息处理
- 消息循环
- 窗口函数
- 消息过滤
- 寄送与发送消息
- 寄送消息
- 发送消息
- 消息死锁
- 消息广播
- 消息查询
一 窗口消息
系统通过消息的形式向窗口传递输入。消息由系统或应用程序产生,系统在每个输入事件触发时都会产生一个消息——比如当用户打字、移动鼠标或点击滚动条等控件。在响应由应用程序引起的系统更改时,系统也会产生消息,比如当一个程序更改了系统字体资源池或是更改了窗口的尺寸。应用程序可以产生消息来指示其窗口完成一些任务或是与另一个应用程序中的窗口取得联系。
系统使用四个参数来发送一个消息到窗口函数:一个窗口句柄、一个消息标识和两个消息参数的值。窗口句柄标识了消息要到达的窗口,系统用它来决定哪个窗口函数来接收该消息。消息标识是表示消息大意的命名常量,当一个窗口函数接收到消息时,会根据消息标识来判断如何处理该消息。例如,消息标识WM_PAINT就告诉窗口函数该窗口的客户区域已改变需要重绘。消息参数指定了供窗口函数处理消息时使用的数据或数据位置,消息参数的取值还意思由该消息决定,一个消息参数可以包含一个整数、填充标志位、一个指向包含额外数据的结构的指针等等。如果一个消息不使用消息参数,通常将他们设置为NULL。窗口函数必须检查消息标识来决定如何理解其消息参数。
二 消息类型
该节介绍两种类型的消息This section describes the two types of messages:
- 系统定义消息
- 应用程序定义消息
1 系统定义消息
系统在需要与一个应用程序通信时会发送或寄送一个系统定义消息。系统使用这些消息来控制应用程序的操作,提供应用程序要处理的输入和其他信息。应用程序也可以发送或寄送系统定义消息。通常应用程序使用这些消息来控制预注册窗口类创建的窗口的操作。
每个系统定义消息有一个唯一的消息标识和一个对应的符号常量(定义在软件开发包(SDK)的头文件中)以表示消息的意思。比如WM_PAINT请求一个窗口绘制其内容。符号常量指定一个系统定义消息隶属于的分组。常量的前缀标识可以解释和处理该消息的窗口的类型。下面是一些前缀及它们对应的消息分组。
前缀 |
分类 |
参考 |
ABM、ABN | Application desktop toolBar | Shell Messages and Notifications |
ACM、ACN | Animation control | Animation Control Messages、Animation Control Notifications |
BCM、BCN、BM、BN | Button control | Button Control Messages、Button Control Notifications |
CB、CBN | ComBox control | ComboBox Control Messages、ComboBox Control Notifications |
CBEM、CBEN | ComBoxEx control | ComboBoxEx Messages、ComboBoxEx Notifications |
CCM | General control | Control Messages |
CDM | Common dialog box | Common Dialog Box Messages |
DFM | Default context menu | Shell Messages and Notifications |
DL | Drag list box | Drag List Box Notifications |
DM | Default push button control | Dialog Box Messages |
DTM、DTN | Date and time picker control | Date and Time Picker Messages、Date and Time Picker Notifications |
EM、EN | Edit control | Edit Control Messages、Edit Control Notifications、Rich Edit Messages、Rich Edit Notifications |
HDM、HDN | Header control | Header Control Messages、Header Control Notifications |
HKM | Hot key control | Hot Key Control Messages |
IPM、IPN | IP address control | IP Address Messages、IP Address Notifications |
LB、LBN | List box control | List Box Messages、List Box Notifications |
LM | SysLink control | SysLink Control Messages |
LVM、LVN | List view control | List View Messages、List View Notifications |
MCM、MCN | Month calendar control | Month Calendar Messages、Month Calendar Notifications |
PBM | Progress bar | Progress Bar Messages |
PGM、PGN | Pager control | Pager Control Messages、Pager Control Notifications |
PSM、PSN | Property sheet | Property Sheet Messages、Property Sheet Notifications |
RB、RBN | Rebar control | Rebar Control Messages、Rebar Control Notifications |
SB、SBN | Status bar window | Status Bar Messages、Status Bar Notifications |
SBM | Scroll bar control | Scroll Bar Messages |
SMC | Shell menu | Shell Messages and Notifications |
STM、STN | Static control | Static Control Messages、Static Control Notifications |
TB、TBN | Toolbar | Toolbar Control Messages、Toolbar Control Notifications |
TBM、TRBN | Trackbar control | Trackbar Control Messages、Trackbar Control Notifications |
TCM、TCN | Tab control | Tab Control Messages、Tab Control Notifications |
TDM、TDN | Task dialog | Task Dialog Messages、Task Dialog Notifications |
TTM、TTN | Tooltip control | Tooltip Control Messages、Tooltip Control Notifications |
TVM、TVN | Tree-view control | Tree View Messages、Tree View Notifications |
UDM、UDN | Up-down control | Up-Down Messages、Up-Down Notifications |
WM | General |
|
常规窗口消息含盖很宽泛的信息和需求,包括鼠标和键盘输入,菜单和对话框输入,窗口创建和管理和动态数据交换等信息。
2 应用程序定义消息
一个应用程序可以创建消息以供自己的窗口使用或是与其他进程的窗口通信,当应用程序创建了消息,接收该消息的窗口函数必须对消息作出解释并提供适当处理。消息取值有如下约定:
- 系统为系统定义消息保留在0x0000至0x03FF(即 WM_USER – 1)的取值范围,应用程序不能为其私有消息使用这些值。
- 0x0400 (即 WM_USER) 至 0x7FFF 是有效的私有窗口窗口消息取值范围。
- 如果你的应用程序标记为4.0版本,可以为私有消息取值0x8000 (WM_APP)至0xBFF。
- 系统会在应用程序调用RegisterWindowMessage函数注册消息时返回一个0xC000至0xFFFF之间的消息标识值。由该函数返回的消息标识保证在整个系统中是唯一的,可以使用该函数解决当另一个程序使用相同标识进行注册时产生的冲突。
三 消息路由
系统使用两个方法将消息路由到一个窗口过程:第一个是寄送消息到一个先进先出的队列,该队列是由系统定义的暂存消息的内存对象,被称为消息队列;第二个方法是直接发送消息到一个窗口函数。寄送到消息队列的消息称为队列消息,主要是用户通过鼠标或键盘输入,比如WM_MOUSEMOVE, WM_LBUTTONDOWN, WM_KEYDOWN, 和WM_CHAR消息,其他消息包括定时器、绘制和退出消息: WM_TIMER, WM_PAINT, 和WM_QUIT。而其他绝大多数直接发送到窗口函数的消息称为非队列消息。
- 队列消息
- 非队列消息
1 队列消息
系统同一时刻可以显示任意数量的窗口,系统使用消息队列来将鼠标和键盘输入路由到适当窗口。系统会维护一个简单系统消息队列,以及为每个GUI线程维护一个对应的线程消息队列。为避免创建非GUI线程消息队列,所有的线程在最初创建时都没有消息队列,系统仅在线程首次调用指定的用户函数时才会为该线程创建消息队列,GUI函数的调用不会导致消息队列的创建。
每当用户移动鼠标,按下鼠标键或是敲击键盘时,鼠标或键盘的设备驱动程序就会将这些输入转换为消息并将它们放到系统消息队列中。系统从系统消息队列中一次取走一个消息,确定其目标窗口,然后寄送消息到目标窗口所在线程的消息队列。一个线程消息队列接收其线程所创建的所有窗口的鼠标和键盘消息。线程从其线程消息列表中取走消息然后发送到适当的窗口函数处理。除WM_PAINT 、WM_TIMER、WM_QUIT消息外,系统始终直接发送队列中剩下的消息(译注:此处队列应该为线程消息队列,剩下的消息指鼠标键盘的输入消息),这可以确保一个窗口按先进先出的顺序来接收其输入消息,而WM_PAINT、WM_TIMER、WM_QUIT消息则保持在队列中仅当队列中没有其他消息时才会被送到窗口函数。另外,同一窗口的多个重复的WM_PAINT 消息会被重组为一个单一的WM_PAINT 消息, 将客户区域中所有无效的部分合并为一个单一的区域。WM_PAINT消息的重组减少了窗口客户区内容的重绘次数。
系统通过填充MSG 结构并复制到队列的方式来寄送一个消息到一个线程消息队列,MSG 的信息包括:消息的目标窗口的句柄,消息的标识,两个消息参数,消息寄送的时间和鼠标指针的位置。线程可以通过PostMessage、PostThreadMessage函数寄送一个消息到它自己的消息队列,或另一个线程的消息队列。一个应用程序要通过GetMessage函数来从其消息队列中取走一个消息,如果只查看消息而不从队列中取出,可以使用PeekMessage函数,该函数使用消息的信息来填充MSG结构。在从消息队列中取出消息后,应用程序可以调用DispatchMessage函数引导系统将消息发送到一个窗口函数来处理。DispatchMessage函数会获取一个指向之前通过调用GetMessage或PeekMessage填充的MSG结构的指针。DispatchMessage 传递一个窗口句柄,一个消息标识,和两个消息参数到窗口函数,但不传递消息的寄送时间或鼠标指针的坐标。应用程序可以在处理消息时通过GetMessageTime、GetMessagePos函数来获取它们。线程可以在其消息队列为空时调用 WaitMessage函数将控制权转让给其他进程,该函数可以暂停进程,直到有新的消息送至消息队列。可以调用SetMessageExtraInfo来关联一个值到当前线程的消息队列,然后调用GetMessageExtraInfo来获取通过GetMessage或PeekMessage函数接收的最后一个消息值。
2 非队列消息
非队列消息直接被送到目标窗口函数,忽略系统消息队列和线程消息队列。通常,系统发送非队列消息来通知受事件影响的窗口。例如,当用户激活一个新的应用程序窗口时,系统会发送给该窗口若干消息,包括WM_ACTIVATE, WM_SETFOCUS和WM_SETCURSOR。这些消息通知该窗口:它已经变为活动窗口了,键盘输入会直接定向到该窗口,并且鼠标指针也已经位于窗口的边界之内。应用程序调用某系统函数时也会产生非队列消息,例如,系统会在应用程序调用SetWindowPos函数时发磅WM_WINDOWPOSCHANGED消息。发送非队列消息的函数有:BroadcastSystemMessage, BroadcastSystemMessageEx,SendMessage, SendMessageTimeout、SendNotifyMessage。
四 消息处理
应用程序必须从其消息队列中取出和自责消息,单线程应用程序通常在其WinMain函数中使用消息消息循环来取出并发送消息到适当的窗口函数去处理。多线程程序可以在每个线程中都包含一个消息循环。以下章节介绍消息循环如何工作,及窗口函数的的作用:
- 消息循环
- 窗口函数
1 消息循环
一个简单的消息循环由对以下三个函数的调用组成: 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函数结束其消息循环来响应WM_DESTROY消息。如果在调用GetMessage函数是为第二个参数指定了一个窗口句柄,则只有该句柄关联的窗口的消息才会从队列里取出。GetMessage 也可以过滤队列中的消息,只检索特定范围内的消息。有关过滤消息的更多信息可参见Message Filtering。
要从键盘接收字符输入的线程,其消息循环中必须包括TranslateMessage 。系统在用户每次按下一个键时产生虚拟键消息 (WM_KEYDOWN和WM_KEYUP)。一个虚拟键消息包含一个虚拟键编码以标识被按下的键而不是其字符值。 要得到其值(译:应该指字符值),消息循环就要通过TranslateMessage 函数,来将虚拟键消息转换为一个字符消息(WM_CHAR)并将其放回其消息队列中,在消息循环随后的迭代部分被取出并分派至窗口函数。
DispatchMessage函数发送消息到一个窗口函数,其窗口的句柄在MSG结构中指定. 如果窗口句柄是HWND_TOPMOST,则DispatchMessage 会将该消息发送到系统中所有的顶级窗口的窗口函数;如果是NULL,则 DispatchMessage 对该消息没有任何影响。
应用程序的主线程在初始化应用程序并至少创建一个窗口后开始其消息循环,开始之后,消息循环继续从其线程消息队列中取出消息并分派至适当窗口,该消息循环在GetMessage函数取出WM_QUIT消息时终止。一个消息队列只需要一个消息徨,即使一个应用程序有许多窗口,DispatchMessage会分派消息到适当窗口,这是因为队列中的消息都是一个MSG 结构,结构中包含了与消息关联的窗口句柄的。
你有多种方式修改一个消息循环,例如,可以从队列中只取出消息而不分派给窗口,这对应用程序只寄送消息而不指定一个窗口非常有用。还可以直接用GetMessage函数来跳过队列中的其他消息,只查找指定消息,这在消息队列中可以暂时忽略先进先出顺序。使用快捷键的应用程序必须能将键盘消息转换为命令消息,为此,应用程序的消息循环必须包含对TranslateAccelerator函数的调用。有关快捷键的更多信息可以参见键盘加速器。如果线程使用非模式对话框,消息循环要包括IsDialogMessage函数来确保对话框可以接收到键盘输入。
2 窗口函数
窗口函数是接收并处理要发送到窗口的所有消息的函数。每个窗口类都有一个窗口函数,并且用这种类创建的每一个窗口都是由使用相同窗口函数来响应消息。系统通过将消息数据作为参数传递的方式来将一个消息发送到一窗口函数。窗口函数随后针对该消息执行操作:检查消息标识,用消息参数中指定的信息处理消息。窗口函数通常不应忽略消息,如果其不处理某消息,则应将该消息送回系统进行默认处理,这可以通过调用DefWindowProc函数来完成,该函数会执行一个默认的操作,并返回一个消息结果。窗口函数随后必须将该结果值作为自己的消息结果返回。绝大多数窗口函数只处理有限的消息而通过调用DefWindowProc函数将其他消息传递给系统处理。
由于同一个窗口函数被属于同一类的所有窗口共享,可以为若干不能的窗口处理消息。为确定受某消息影响的指定窗口,一个窗口函数要检查随消息一起传递的窗口句柄。有关更多消息可参见窗口函数。
五 消息过滤器
应用程序可以通过GetMessage或PeekMessage函数指定一个消息筛选器来选择接收指定消息而忽略其他消息。所谓的过滤器就是一个消息标识的范围(由第一个和最后一个消息标识符指定),或一个窗口句柄,或两者一起。GetMessage 和PeekMessage 用消息过滤器来确定从队列中选择哪些消息。当应用程序必须查找队列中后来被送达的消息时非常有用,对于在处理寄送消息之前必须先处理硬件输入消息的应用程序也很实用。WM_KEYFIRST和WM_KEYLAST 两个标识常量可以用作过滤器的值来过滤所有的键盘消息;WM_MOUSEFIRST 和 WM_MOUSELAST 两个常量可以用作过滤所有鼠标消息的过滤器值。过滤消息的应用程序要确保符合过滤器的消息可以被寄送出去,例如,应用程序为一个不接收键盘输入的窗口过滤WM_CHAR 消息,而GetMessage函数不会返回,这事实上会挂起应用程序。
六 寄送和发送消息
任何一个应用程序都可以寄送和发送消息。像系统和应用程序,通过拷贝消息到消息队列来寄送消息或将消息数据作为参数传递给一个窗口函数来发送消息。要寄送消息,应用程序使用PostMessage函数,通过SendMessage,BroadcastSystemMessage, SendMessageCallback, SendMessageTimeout, SendNotifyMessage或SendDlgItemMessage函数来发送消息。
1 寄送消息
通常,应用程序寄送一个消息来通知特定的窗口去执行一个任务。PostMessage函数为消息创建一个 MSG结构,并复制到消息队列。应用程序的消息循环最终接收该消息并分派到指定窗口函数。应用程序可以寄送一个消息而不必指定窗口,如果应用程序在调用PostMessage函数时指定了Null作为窗口句柄,该消息会被寄送到与当前线程关联的队列中,由于没有指定窗口的句柄,应用程序必须在消息循环中对消息进行处理。这也是创建应用于整个应用程序而不是某个窗口的消息的一种方法。有时候你可能要寄送一个消息给系统中所有的顶级窗口。应用程序可以通过调用PostMessage函数并为窗口句柄参数指定HWND_TOPMOST参数来实现向所有顶级窗口寄送消息。一个普遍的编程误区认为PostMessage函数总是寄送消息,这在消息队列已满的时候是不成立的(译:此处队列应为线程消息队列)。应用程序应检查PostMessage 函数的返回值来确认消息是否已寄送出去,如果没有,应重新寄送!
2 发送消息
应用程序通常发送一个消息通知窗口函数立即执行一个任务。SendMessage函数发送消息至给定窗口的窗口函数,该函数会等待目标窗口的窗口函数完成处理,然后返回消息结果,父窗口和子窗口经常通过互相发送消息来相互联系。例如,一个拥有编辑控件作为其子窗口的父窗口可以给控件发送一个消息来设置其文本,而控件也可以通过发送消息到父窗口来通知文本的更改已完成。
SendMessageCallback函数也会发送消息至给定窗口的窗口函数,然而,函数会立即返回,当窗口函数处理消息后,系统调用指定指定的回调函数。有关回调函数更多消息请参见SendAsyncProc。
有时候你可能要发送一个消息到系统中的所有顶级窗口。例如,如果有程序更改的系统时间,则它必须发磅一个WM_TIMECHANGE消息将更改通知到所有顶级窗口。应用程序可以调用SendMessage函数并指定一个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,广播会立即中止并返回0.