Windows Messages
系统用一定格式的消息传递给窗口过程函数。消息是被系统和应用程序两者产生的。系统会在每一个输入事件产生消息:例如,当用户输入,移动鼠标,点击控件比如滚动条。系统也会对应用程序导致的系统变化产生反应消息,例如当应用程序改变系统字体资源池,或者重设窗口大小。应用程序直接对它的窗口产生消息来执行任务或者跟别的应用程序窗口通信。
系统对窗口过程函数发送带有四个参数的消息:窗口句柄,消息标识符,两个消息参数wParam,lParam。窗口句柄标识消息要发送到的窗口。系统用它来决定将消息发送到哪一个窗口过程函数。
消息标识符是一个命名的常量,它表明消息的企图。当窗口过程接受一个消息的时候,它用消息标识符来决定怎么处理这个消息。例如,消息标识符WM_PAINT告诉窗口过程函数窗口客户端区域发生了变化,它需要重新绘制。
当窗口过程处理一个消息的时候,消息参数用来指定数据或者数据的位置。这意味着消息参数的值依赖于消息。一个消息参数可以包含一个整数,填充的比特标志,一个指向包含额外信息的结构体的指针,他们通常设置为NULL。窗口过程函数必须检查消息标识符来决定怎么解释消息参数的值。
两种格式的消息:
系统sends或者posts一个系统定义消息当它和应用程序通信的时候。它用这些消息来控制应用程序的操作,提供输入和其他信息给应用程序来处理。应用程序也能sends或者posts系统定义的消息。应用程序通常使用这些消息来控制通过预定义窗口类产生的控件的操作。
每一个系统定义的消息都有一个独一的消息标识符和一个相关的符号常量,符号常量陈述消息的企图。例如:WM_PAINT常量表示请求窗口重绘。
符号常量指定系统定义的消息属于哪个类别。常量的前缀标示可以解释和处理这个消息的窗口类型。下面是前缀和他们相关的消息类别。
Prefix |
Message category |
ABM and ABN |
Application desktop toolbar |
ACM and ACN |
Animation control |
BCM, BCN, BM, and BN |
Button control |
CB and CBN |
ComboBox control |
CBEM and CBEN |
ComboBoxEx control |
CCM |
General control |
CDM |
Common dialog box |
DFM |
Default context menu |
DL |
Drag list box |
DM |
Default push button control |
DTM and DTN |
Date and time picker control |
EM and EN |
Edit control |
HDM and HDN |
Header control |
HKM |
Hot key control |
IPM and IPN |
IP address control |
LB and LBN |
List box control |
LM |
SysLink control |
LVM and LVN |
List view control |
MCM and MCN |
Month calendar control |
PBM |
Progress bar |
PGM and PGN |
Pager control |
PSM and PSN |
Property sheet |
RB and RBN |
Rebar control |
SB and SBN |
Status bar window |
SBM |
Scroll bar control |
SMC |
Shell menu |
STM and STN |
Static control |
TB and TBN |
Toolbar |
TBM and TRBN |
Trackbar control |
TCM and TCN |
Tab control |
TDM and TDN |
Task dialog |
TTM and TTN |
Tooltip control |
TVM and TVN |
Tree-view control |
UDM and UDN |
Up-down control |
WM |
General |
通用消息覆盖了很大范围的信息和请求,包括了鼠标和键盘的输入,菜单和对话框的输入,窗口的创建和管理,以及动态数据交互(DDE)。
应用程序可以创建消息来被自己的窗口使用或者用来和其他进程进行通信。如果应用程序创建自己的消息,那么接受它们的窗口过程函数必须解释这些消息并且提供适宜的处理。
消息标志值用法如下:
系统使用两种方法来路由消息给窗口过程函数:投递消息到一个先进先出被称作消息队列,这是系统定义的暂时存储消息的存储对象;直接向窗口过程投递发送消息。
投放到消息队列的消息被称作队列消息。这些主要是用户通过鼠标,键盘造成的,例如WM_MOUSEMOVE, WM_LBUTTONDOWN, WM_KEYDOWN,WM_CHAR 消息。其它队列消息包括timer,paint,quit消息:WM_TIMER,WM_PAINT,WM_QUIT。大多数消息是直接投递给窗口过程的,被称为非队列消息。
系统可以同时显示任意多个窗口。为了能让鼠标和键盘的输入路由到适宜的窗口,系统使用了消息队列。系统维护一个单独的系统消息队列以及每个GUI线程配一个指定的消息队列。为了避免对非GUI线程创建消息队列的开销,所有的线程最初创建的时候并没有一个消息队列。系统仅在线程第一次调用指定的user functions中的某一个的时才会为之创建一个线程指定的消息队列;不是GUI函数的调用导致了消息队列的创建。
无论何时用户移动鼠标,单击鼠标按钮,或者敲打键盘,鼠标或者键盘的驱动程序都会把这些输入转换成消息放并且放置到系统的消息队列中。系统从系统消息队列中一次移除一个消息,检查它们以来决定目标窗口,然后将它们投递创建目标窗口的线程的消息队列中,线程的消息队列为线程所创建的窗口接受所有的鼠标和键盘消息。线程从它自己的消息队列中移除消息并且指示系统把他们发送到适宜的窗口过程中处理。(我想这样做的目的主要是为了减轻系统消息队列的压力,把消息分散到对应的线程中,然后线程每次取出一个再发给系统,系统再发给对应的窗口过程函数。同时消息队列的作用是为了保存输入的数据,以免来不及处理)
除了WM_PAINT 消息,WM_TIMER 消息, WM_QUIT 消息,系统总是把消息投递到消息队列的末尾。这保证了窗口接受输入的消息时先进先出的序列。然而WM_PAINT 消息,WM_TIMER 消息, WM_QUIT 消息,是保持在队列中并且只有当队列中没有别的消息的时候才会被转发到窗口过程。另外多个针对同一个窗口的WM_PAINT消息会被合并成一个WM_PAINT消息,整顿所有的无效区域到一个单独的区域。合并WM_PAINT消息减少了窗口重绘客户区的次数。
系统通过填充一个MSG结构体并且复制它到消息队列中来实现把消息发送到线程的消息队列中。MSG中包含:消息所要指向的窗口句柄,消息标识符,两个消息参数wParam,lParam,(操作系统是16位的时候,每条消息附带2个参数,“WParam”和“LParma”,“WParam”是一个16位的数据,也就是“word”数据类型,所以我们叫它“W”;而“LParam”却是一个32位的数据,也就是“Long”数据类型,所以我们叫它“L”)。消息投递的时间,鼠标光标的位置。通过使用PostMessage或者PostThreadMessage函数,线程能投递消息到它自己的消息队列中或者另外一个线程的消息队列中。
通过GetMessage函数应用程序可以从消息队列中移除一个消息。为了审查一个消息但是不将其移除消息队列可以使用PeekMessage函数。这个函数用消息的信息来填充MSG结构体。
在将消息移除队列后,应用程序可以使用DispatchMessage 函数来使系统把消息发送到窗口过程去处理。DispatchMessage 函数有一个指向MSG的指针参数,这个MSG是被之前调用的GetMessage函数或者PeekMessage函数填充的。DispatchMessage 函数把窗口句柄,消息标识符,两个消息参数传递给窗口过程,但是它不传递消息投递的时间以及鼠标光标的位置。但是应用程序可以通过函数GetMessageTime和GetMessagePos来检索信息当处理一个消息的时候。
当消息队列没有消息的时候,应用程序可以使用WaitMessage函数来放弃控制权给别的线程。这个函数就会将线程挂起直到有消息被投放到线程的消息队列中。
你可以使用SetMessageExtraInfo 函数来把一个值关联到当前线程的消息队列中。然后通过调用GetMessageExtraInfo 函数得到和最新消息(通过GetMessage或者PeekMessage)关联的值。
非队列化消息直接被发送到目标窗口过程,绕过系统消息队列和线程消息队列。系统通常发送非队列消息来通知影响窗口的事件。例如,当用户激活了一个应用程序的窗口时,系统发送给窗口一系列的消息,包括WM_ACTIVATE, WM_SETFOCUS, and WM_SETCURSOR。这些消息通知被激活的窗口,键盘输入焦点指向的窗口,鼠标光标移动到边界内的窗口。非队列消息也会在当应用程序调用某些系统函数时候产生,例如,当程序调用SetWindowPos 函数来移动窗口后,系统会发送WM_WINDOWPOSCHANGED 消息。
一些函数函数会非队列消息,例如BroadcastSystemMessage, BroadcastSystemMessageEx, SendMessage, SendMessageTimeout,SendNotifyMessage.
The following sections describe how a message loop works andexplain the role of a window procedure:
程序必须移除和处理发送到线程消息队列中的消息。一个单线程程序通常在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函数来结束它自己的循环,通常作为是程序中主窗口的窗口过程函数的WM_DESTROY的反应。
如果你为GetMessage函数的第二个参数指定一个窗口句柄,则从消息队列中检索的消息只是指定窗口的。GetMessage 也能过滤队列中的消息,值检索指定范围内的消息。
一个线程的消息循环一定要包含TranslateMessage如果线程准备从键盘接受字符输入。每次用户按键的时候,系统产生虚拟键(virtual-key)消息(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函数不反应。这有效的挂起了程序。
应用程序可以投递和发送消息。就像系统一样,程序可以post一个消息通过把它复制到消息队列,或者sends一个消息通过把消息数据作为参数传递给窗口过程。Post一个消息,程序可以使用PostMessage函数。Send一个消息通过调用SendMessage, BroadcastSystemMessage, SendMessageCallback, SendMessageTimeout, SendNotifyMessage, or SendDlgItemMessage函数。
应用程序通常posts一个消息来通知指定的窗口执行一个任务。PostMessage函数为消息创建一个MSG结构体并且把消息发送到消息队列。程序的消息循环最终会检索这个消息并且派送它到适宜的窗口过程。
程序可以不指定窗口而post一个消息。如果窗口句柄指定为NULL,那么消息会被post到当前线程相关的消息队列中。因为没有窗口句柄被指定,所以程序必须在消息循环中处理这个消息。这是一个把创建的消息应用到整个程序的方法,而不是某一个指定的窗口。
有时候,你或许想把消息post到系统的所以顶层窗口。程序可以通过调用PostMessage函数并且把hwnd参数指定为HWND_TOPMOST来把消息post到所有的顶层窗口。
一个通常的编程错误是假定PostMessage函数总是posts一个消息。当消息队列满的时候这就不是真的了。程序应该检查PostMessage的返回值来决定消息是否被投递,如果没有的话重新投递。
程序通常sends一个消息去通知窗口过程去立即处理一个任务。SendMessage函数把消息传递给指定窗口相关的窗口过程。函数会一直等待直到窗口过程完成处理并且返回消息结果。父子窗口经常通过send消息来相互交流。例如一个有编辑框子窗口的父窗口可以通过相子窗口发送消息来改设置控件的文本。控件可以通过Send消息来通知父窗口因为用户而使文本发生的变化。
SendMessageCallback函数也会想给定窗口的窗口过程发送消息。但是函数是立即返回的。在窗口过程处理完消息后,系统调用指定的回调函数。
有时候,你想给所有的顶层窗口发送一个消息。例如,如果应用程序改变了时间,它必须通过发送WM_TIMECHANGE消息通知所有的顶层窗口关于这个改变。程序可以通过调用SendMessage函数并且制定hwnd参数为HWND_TOPMOST来实现向所有顶层窗口发送消息。你也可以给所有的应用程序发送一个广播消息通过BroadcastSystemMessage 函数并且把lpdwReclipients参数指定为BSM_APPLICATIONS。
通过使用InSendMessage或者InSendMessageEx函数,窗口过程可以确定它处理的消息是否是另一个线程发送的。当处理消息要依赖消息的根源时,这个功能是有用的。
Message Deadlocks
线程调用SendMessage函数去send消息给另一个线程的时候不能继续执行知道窗口过程返回消息。如果接受线程在处理消息的时候放弃了控制那么发送线程就不能继续执行,因为它一直在等待SendMessage函数的返回。如果接受线程和发送线程绑定的是同一个消息队列的话,这会导致应用程序死锁的发生。(发送线程A等待接受线程的返回,接受线程B等待对话框或者其他的反应结束,对话框等消息的处理又要用到消息循环,消息循环因为SendMessage在阻塞又不能执行,因此陷入了死锁)
Note that the receivingthread need not yield control explicitly; calling any of the followingfunctions can cause a thread to yield control implicitly.
注意到接受线程不需要显式的放弃控制;调用以下任何一个函数都会导致线程隐式的失去控制权。
为了避免潜在的你程序中潜在的死锁,考虑用SendNotifyMessage或者SendMessageTimeout函数。另外通过调用InSendMessage or InSendMessageEx函数,窗口过程可以确定收到的消息是否是另一个线程所发送。处理消息的时候在调用上面列表中任何一个函数之前,窗口过程应该先调用 InSendMessage or InSendMessageEx。如果函数TRUE,那么窗口过程必须在任何导致线程失去控制的函数之前调用函数ReplyMessage。
附注:
Window消息中引入虚拟键,虚拟码的原因:在最开始的时候是键盘的扫描码直接和按键相对应,但是这样做有弊端。如果这样,键盘发生变化,同一个键对应的扫描码可能不一样,这样如果在程序中针对扫描码的不同而做出不同反应的代码就会出问题。解决这一问题的方法就是引入虚拟键,这样在程序中直接对应虚拟键来处理消息,基本键盘格局发生变化也不会影响,因为那时候只要键盘的驱动没问题就可以了。
MSDN原文网页:http://msdn.microsoft.com/en-us/library/ms644927(v=vs.85).aspx