博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Windows系统消息机制的详细理解!
阅读量:4119 次
发布时间:2019-05-25

本文共 10047 字,大约阅读时间需要 33 分钟。

写在前面的话:

     很多书在讲Windows消息机制的时候都讲的比较简单或者比较模糊。我花了2天时间,把微软官方的讲解理解了一下,写成下列的文章,希望可以帮助到一些需要帮助的人!

 

消息和消息队列概述

    基于windows的应用程序是基于事件驱动的。他们不会显式地调用函数获得输入,而是等待操作系统把输入传给应用程序。操作系统把所有针对应用程序的输入都会传给应用程序内的每个窗口,每个窗口都有一个叫做窗口过程的函数,在任何时候只要窗口有输入,操作系统就会调用这个窗口过程函数。窗口过程函数处理完输入就会把控制权转给操作系统。

Windows XP:假如顶层窗口停止消息响应超过几秒钟,系统就会认为窗口不会响应了。在这种情况下,系统会把这个窗口隐藏掉并且用一个具有相同Z坐标、位置、大小和可视化属性的克隆窗口替换停止响应的窗口。而且这个窗口还可以让用户移动,改变大小甚至关闭这个应用程序。然而,这些只是一种无奈的补救行为,因为应用程序实际上没有响应。在调试模式下,操作系统就不会创建克隆窗口。

一、Windows消息

    系统传送输入给窗口过程是通过消息的形式。消息可以是系统产生,也可以是应用程序产生。对每一个输入事件,系统都会产生一个消息。例如,当用户敲击键盘,移动鼠标或者单击像滚动条这样的控件的时候,系统都会产生消息。当应用程序导致系统发生改变的时候,系统也会产生消息,例如当应用程序改变了系统字体资源库或者改变了自己窗口大小的时候,系统也会产生消息以便于适应这种改变。当应用程序想让他自己的窗口完成某个任务或者想和其他应用程序的窗口进行通讯的时候,应用程序也可以产生消息。

    系统发送给窗口过程的消息中包含了6个参数。其中前四个分别是窗口句柄,消息标识符和两个数值,这两个数值一般被称为消息参数。窗口句柄表示这个消息的目标窗口,系统使用窗口句柄来确定哪一个窗口过程函数会接收这个消息。

    消息标识符是代表这个消息的作用的一个常量名称。当窗口过程接受到一个消息的时候,他会使用消息标识符来决定如何处理这个消息。例如,如果消息标识符是WM_PAINT,意思就是告诉窗口过程函数,窗口的客户区已经发生了改变,必须重新绘制了。

    消息参数指定了窗口过程函数在处理消息过程中可能用到的数据或者数据的存放位置。这连个参数的含义和具体的取值要根据消息而定。消息参数可以包含一个整数,也可以是一组位标志,或者是指向结构的指针,结构里面可能是附加的一些数据,等等。如果消息不需要使用到参数,那么这些参数数据会被设置为NULL。窗口过程函数会检查消息标识符,根据不同的消息标识符来决定符合解释消息参数。

二、消息的类型

  1)系统定义消息

       当系统要和应用程序通讯的时候,他就会发出或者投递一个系统定义消息,他使用这些消息来控制应用程序的操作和为应用程序提供输入或者其他信息。应用程序也可以发动和投递系统定义消息,应用程序一般使用这些消息来控制那些通过注册窗口类而创建的控件窗口的动作。

       每一个系统定义消息都有一个唯一的消息标识符和相对应的符号常量(在系统SDK的头文件中定义的),这些标识符和常量代表着消息意图。例如,WM_PAINT就是一个消息的符号常量,他代表消息意图就是让窗口重画客户区。符号常量也指定了系统定义消息属于哪种类别。常量的前缀代表着可以解释处理消息的窗口的类型,下面列出系统定义消息的前缀所代表的消息类别的清单。

Prefix Message category

ABM    Application desktop toolbar 

BM     Button control 

CB     Combo box control 

CBEM   Extended combo box control

CDM    Common dialog box 

DBT    Device 

DL     Drag list box 

DM     Default push button control 

DTM    Date and time picker control

EM     Edit control 

HDM    Header control 

HKM    Hot key control

IPM    IP address control

LB     List box control 

LVM    List view control 

MCM    Month calendar control

PBM    Progress bar 

PGM    Pager control

PSM    Property sheet 

RB     Rebar control

SB     Status bar window 

SBM    Scroll bar control 

STM    Static control 

TB     Toolbar 

TBM    Trackbar 

TCM    Tab control 

TTM    Tooltip control 

TVM    Tree-view control 

UDM    Up-down control

WM     General window 

其中普通窗口消息占信息和请求的大部分,包括鼠标和键盘输入消息,菜单和对话框输入消息,窗口创建建和管理,动态数据交换(DDE)。

  2)应用程序定义消息

      应用程序可以创建消息以供他自己的窗口或者和其他进程的窗口通讯而用。如果应用程序创建了自己的消息,那么接收这个消息的窗口过程函数必须能解释这个消息并提供适当的处理过程。

      应用程序定义消息的消息标识符取值定义规则应该遵循如下原则

      A.系统对0x00000x03ffWM_USER-1)都保留为系统定义消息,因此应用程序不能使用这些值来定义应用程序定义消息。

      B.取值范围为0x0400(WM_USER)-0x7FFF,可以用来定义自定义消息的消息标识符取值。

      C.假如你的应用程序是在4.0上的系统运行,那么你可以使用0x8000(WM_APP)-0xBFFF范围的值作为私有消息的标识符取值。

      D.当应用程序调用RegisterWindowMessage函数来注册一个消息的时候,系统会返回一个消息标识符给你,范围在0xC000-0xFFF之间。这个函数返回的标识符可以确保在整个系统是唯一的。这个函数的使用可以防止和其他应用程序使用的自定义消息发生冲突。

三、消息传递

    系统使用两个方法来传递消息给窗口过程:一种是投递到消息队列,还有一种就是投递消息到系统定义的一个内存对象临时存储,并且直接把这个消息发送给窗口过程(这个方法不会进入消息队列)。

    需要投递到消息队列的消息称为队列化消息。他们主要是用户通过键盘或者鼠标输入,例如WM_MOUSEMOVE,WM_LBUTTONDOWN,WM_KEYDOWNWM_CHAR消息。其他队列化消息还包括时钟,绘制和退出消息:WM_TIMER,WM_PAINT,WM_QUIT.

    其他直接发送给窗口过程的消息称为非队列化消息。

   (1)队列化消息

      系统在某个时刻可以显示任意数量的窗口。为了把鼠标和键盘输入传递给对应的窗口,系统使用了消息队列的机制。

      系统维护着一个系统级的消息队列同时为每个GUI线程维护着一个线程级的消息队列。为了避免为那些非GUI线程也创建消息队列导致的系统开销,所有线程在最初创建的时候是没有消息队列的。只有当线程首次调用用户用户函数或者图形设备接口函数的时候系统才会为线程创建一个消息队列。

      只要用户移动鼠标,单击鼠标按钮或者敲击键盘的时候,设备驱动程序都会为鼠标或者键盘把输入转换成消息,并且把这个消息放在系统消息队列中。系统一次一个的从系统消息队列中取出消息,分析确定目标窗口,接着把这个消息投递到创建目标窗口的线程消息队列中。线程的消息队列为这个线程创建的窗口接收所有的鼠标和键盘消息。线程从他的消息队列中取出消息,让系统发送这个消息给对应的窗口过程函数进行处理。

      系统总是把消息投递到队列的尾部,这样可以确保窗体遵循先入先出的顺序接收到输入消息,但是WM_PAINT,WM_TIMER,WM_QUIT这几个消息是例外的。这三个消息会被一直保留在队列中,直到队列队列中已经没有其他消息之后才会传送给窗口过程函数。另外,多个具有相同目标窗口的WM_PAINT消息会被合并成一个消息,把所有客户区域无效的部分都合并成一个区域,WM_PAINT的合并能够减少窗体重画客户区域的次数。

     系统投递消息到线程消息队列的方式是先填写一个MSG结构体,然后把这个结构体COPY到消息队列中。在MSG中的信息包括:消息目标窗口的句柄,消息标识符,两个消息参数,消息投递的时间,鼠标光标的位置。线程也可以投递消息到他自己的消息队列或者其他线程的消息队列,使用PostMessage或者PostThreadMessage函数。

     应用程序通过调用GetMessage函数从他的消息队列中取出一个消息,如果想分析一个消息,但是又不想从消息队列中移除,那么可以使用PeekMessage函数,这个函数会根据消息的内容重新给你填写一个MSG返回给你,队列中的消息不受影响。

     当从消息队列中取出消息之后,应用程序可以使用DispatchMessage函数触发系统把这个消息发送给窗口过程函数进行处理。DispatchMessage函数需要输入一个指向MSG结构的指针(当然这个MSG结构可以是以前GetMessage或者PeekMessage获得的)。DispatchMessage会传送窗口句柄,消息标识符,两个消息参数给窗口过程函数,但是他不会传送时间和鼠标位置信息。应用程序在处理消息的时候可以通过调用GetMessageTimeGetMessagePos函数获得这两个信息。

     当消息队列中没有消息的时候,线程可以使用WaitMessage函数来把控制权让给其他线程。这个函数可以把当前线程挂起,直到线程消息队列中有新的消息被放置进来,那么线程才会继续执行。

     你可以调用SetMessageExtraInfo函数来把一个数值和当前线程的消息队列进行关联。然后调用GetMessageExtraInfo函数来获得这个关联值,这个关联值实际上就是通过GetMessage或者PeekMessage函数获得的最后一条消息的关联值。

2)非队列化消息

     非队列化消息会绕过系统消息队列和线程消息队列直接发动给目标窗口过程函数,一般系统发送非队列化消息是为了通知受到事件影响的窗体。例如,当用户激活了一个新的应用程序窗口,系统会发送一系列的消息,包括WM_ACTIVATE,WM_SETFOCUS,WM_SETCURSOR。这些消息通知窗口他已经被激活了,键盘输入会直接发给窗口,并且鼠标光标已经在窗口范围内移动了。非队列化消息也可能是应用程序调用某个系统函数导致的结果,例如,应用程序调用了SetWindowPos函数移动窗口之后,系统就会发出一个WM_WINDOWPOSCHANGED消息,这个消息也是非队列化的。

还有一些能发送非队列化消息的函数有:BroadcastSystemMessage, BroadcastSystemMessageEx, SendMessage, SendMessageTimeout, and SendNotifyMessage.

四、消息处理

    应用程序必须对投递在他的线程队列中的消息进行取出和处理。单线程的应用程序通常在WinMain函数使用一个消息循环来取出消息,发送消息给对应的窗口过程函数来对消息进行处理。多线程的应用程序在每个创建窗口的线程中都可以包含一个消息循环。

  (1)消息循环

     一个简单的消息循环一般是需要调用三个函数:GetMessage,TranslateMessage,DispatchMessage

    标准写法如下:

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);

    }

}

很多人常犯的一个错误是把循环判断写成:while(GetMessage( &msg, NULL, 0, 0 )),这样写是不严谨的。因为GetMessage可能返回-10,非0三种状态。-1表示函数调用出错,如果这样写,当返回-1的时候也会进入消息循环,而里面无法对返回值为-1的情况作出错误处理,会导致出错。

 

GetMessage函数从消息队列中取出消息并且填充到一个MSG结构中.这个函数除非遇到WM_QUIT消息,就会返回0FALSE),如果不是就返回一个非0的值。在一个单线程的应用程序中,要关闭应用程序的第一步一般都是终止消息循环。应用程序也可以通过使用PostQuitMessage函数来终止自己的消息循环,一般在应用程序的主窗口的窗口过程函数中对WM_DESTROY消息的处理就是采用这种方式。

 

如果你在使用GetMessage函数的时候,指定了第二个参数的窗口句柄,那么就只会取出以指定窗口句柄为目标的消息,所以GetMessage也可以过滤消息队列中的消息的,可以只是取出那些指定范围内的消息。

 

如果线程要从键盘上接收字符输入的话,线程消息循环中就必须要包括TranslateMessage函数。当用户在键盘上按一个键的时候,系统会产生虚拟键消息(WM_KEYDOWNWM_KEYUP)。虚拟键消息包含了你按下的键的虚拟标识码,但是虚拟标识码不是表示键的ASCII码,要获得ASCII码,消息循环必须采用TranslateMessage函数把虚拟键消息转换成字符消息(WM_CHAR)并且把这个字符消息重新放回到应用程序的消息队列。然后这个字符消息会按照输入的顺序被取出来,并且发给窗口过程函数进行处理。

 

DispatchMessage函数会将消息发送给MSG结构中指定的窗口句柄所表示的窗口过程函数。如果窗口句柄的值是HWND_TOPMOST,那么这个函数就会把该消息发送给系统中的所有顶层窗口的窗口过程函数。假如窗口句柄的值是NULL,那么该函数对消息就不做任何处理。

 

应用程序的主线程在初始化和创建至少一个窗口之后开始启动他的消息循环。一旦开始,消息循环就会一直持续从消息队列中取出消息并且把它们发送到对应的窗口。当GetMessage函数取出WM_QUIT消息之后,消息循环就会终止了。

 

即使应用程序包含着多个窗口,也只需要一个消息循环。DispatchMessage总是会把消息发送给正确的窗口,因为在队列中的每个消息都是一个MSG结构,而这个结构里面包含了这个消息所属的窗口的句柄。

 

你可以通过各种方式去修改消息循环。例如,你可以从消息队列中取出消息,但是不把消息发送给窗口。这在某些应用程序投递不指定窗口的消息的时候很有用。你也可以采用GetMessage只取出指定的消息,让其他消息继续保留在队列中,这对于你有时候必须临时要绕过先入先出顺序的时候比较有用。

 

如果应用程序要使用加速键的话,必须要能够把键盘消息转换成命令消息。要实现这个,应用程序的消息循环必须包含一个TranslateAccelerator函数的调用。

 

假如线程使用了非模态的对话框的话,消息循环必须包含IsDialogMessage函数调用使得对话框能够接收键盘输入。

 

   2)、窗口过程

 

窗口过程实际上是一个函数,他负责接收和处理发送给窗口的消息。每一个窗口类都有一个窗口过程,而且使用相同类创建的所有窗体对象都使用一个窗体过程来响应消息。

 

系统通过给窗口过程传送消息结构体作为参数的方式来把消息传送给窗口过程。窗口过程根据消息来完成一个对应的动作。他检查消息标识符,对消息进行处理,处理的时候有可能会用到消息参数中定义的信息。

窗口过程一般不会忽略消息,如果他不打算处理某个消息,他必须把这个消息发回系统而作为缺省操作,窗口过程是通过调用DefWindowProc函数来实现这个动作的,这个函数完成一个缺省动作并且返回一个消息作为结果。窗口过程必须返回这个值作为他的消息结果,大部分窗口过程只是处理少数几个消息并且把其他消息传送给系统,通过调用DefWindowProc函数。

因为窗口过程是被属于同一个类的所有窗口共享的,所以他能为几个不同的窗口处理消息。为了识别受消息影响的特定窗体,窗口过程可以检查窗口句柄来识别。

 

3)消息过滤

应用程序在调用GetMessage或者PeekMessage函数的时候,是可以选择特定消息的,这就称为消息过滤。过滤器是通过设置函数中的消息标识符范围(从开始到结束某个范围)或者窗口句柄来实现的,当然同时设置也可以的。消息过滤机制对于想搜寻消息队列寻找在队列后面的消息很有用,如果应用程序必须优先处理输入(硬件)消息,消息过滤也比较有用。

WM_KEYFIRSTWM_KEYLAST常量可以用来作为对所有键盘消息的过滤范围值;WM_MOUSEFIRSTWM_MOUSELAST常量可以用来作为所有鼠标消息的过滤范围值。

任何过滤消息的应用程序必须确保满足过滤条件的那些消息能被投递。例如,如果一个应用程序为WM_CHAR消息设置过滤,那么这个窗口就不会接受任何键盘输入,GetMessage函数不会返回任何键盘消息,这相当于挂起了应用程序。

4)投递和发送消息

任何应用程序都可以投递和发送消息。和系统的机制一样,投递消息是把消息结构COPY到消息队列中,发送消息是给窗口过程传递一个消息参数的方式进行。为了投递消息,应用程序使用PostMessage函数。发送消息,一般是用SendMessageBroadcastSystemMessage, SendMessageCallback, SendMessageTimeout, SendNotifyMessage, or SendDlgItemMessage 函数。

A)投递消息

     应用程序一般是投递一个消息来通知指定窗口完成某个任务。PostMessage创建一个MSG结构,并COPY到消息队列中。应用程序的消息循环最终都会取出消息并且发送给对应的窗口过程。

     如果PostMessage中消息没有指定窗口句柄(为NULL),那么消息会被投递到当前线程的消息队列中。因为没有窗口句柄,所以应用程序必须处理这个消息,这是创建一个供整个应用程序使用的消息的一个方法。

     如果PostMessage中窗口句柄为HWND_TOPMOST,那么就表示将消息投递给所有的顶层窗口。

     一般程序常见的一个错误是总认为PostMessage函数投递一定会成功,其实当消息队列队列满之后就不成立了。所以应用程序应该检查PostMessage函数的返回值来确定消息是否投递成功,如果不成功,就应该重新投递。

B)发送消息

    应用程序一般发送一个消息来通知窗口过程立刻完成某个任务。通过SendMessage函数发送消息对应的窗口过程。这个函数会等到窗口过程物理完毕之后才返回消息结果。父窗口和子窗口经常使用这个发送消息的方式互相通讯。例如,一个父亲窗体里面有一个编辑控件作为他的子窗体,父窗体可以通过发动一个消息给编辑控件方式来设置控件的文本。而控件也可以通过给父窗体发回一个消息的方式来通知父窗体用户改变了控件文本。

    SendMessageCallback函数也会发送一个消息给指定窗口,但是这个函数是立刻返回的。在窗口过程处理完毕消息之后,系统会调用指定的回调函数。

    有时候,你可能会想发送一个消息给所有系统里面的顶层窗口。例如,如果应用程序改变了系统时间,他必须通知所有顶层窗口时间发生了改变,方式就是发一个WM_TIMECHANGE消息。应用程序也可以通过调用SendMessage发送一个消息给所有顶层窗口(指定窗口句柄为HWND_TOPMOST)。还可以调用BroadcastSystemMessage函数广播这个消息给所有的应用程序(在pdwRecipients参数中设定为BSM_APPLICATIONS)。

    通过使用 InSendMessage InSendMessageEx 函数,窗口过程可以判断是否正在处理其他线程发来的消息。这个能力当需要根据消息来源而决定如何处理的时候非常有用。

五、消息死锁

如果一个线程通过调用SendMessage函数给其他线程发送消息,那么这个线程要等到窗口处理函数处理完之后,SendMessage函数才会返回,这个线程才能继续执行。假如接收线程在处理消息过程中让出来控制权,那么发送方线程就不能继续执行,因为他需要等待SendMessage函数返回。加入接收线程被挂接到了和发送线程相同的消息队列上,这就有可能导致应用程序死锁。

注意接收线程不一定只有显式的让出控制权,调用下列任意一个函数都会隐式的导致控制权的让出。

DialogBox

DialogBoxIndirect

DialogBoxIndirectParam

DialogBoxParam

GetMessage

MessageBox

PeekMessage

SendMessage

为了避免应用程序永久死锁,应该考虑使用SendNotifyMessage 或者 SendMessageTimeout 函数。否则的话,窗口过程应该通过调用InSendMessage or InSendMessageEx 函数来检查是否有其他线程发过来的消息,如果有的话,应该在调用上面那些可能让出控制权的函数之前先处理消息。如果函数返回真,那么窗口过程必须在让出控制权之前调用ReplyMessage函数,以便于让对方能够继续执行,从而避免死锁。

 

六、广播消息

消息广播简化了系统中需要向多个接收者发送消息的情况。应用程序要广播消息,可以使用BroadcastSystemMessage 函数,定义好消息的接收者就是了。这个时候只需要定义一个或者多个接收者类型就行了。这些类型指的是应用程序,安装型驱动程序,网络驱动程序和系统级的设备驱动程序,系统会发送广播消息给每个指定类型的所有成员。

系统发送广播消息一般是在系统级设备驱动程序或者相关组建内发生变化的时候而做出的响应。驱动程序或者相关组件也可以广播消息给应用程序和其他组件来通知他们发生了改变。例如,当用户在软驱里面插入磁盘的时候,软驱的驱动程序就会广播一个消息,相关组件就会对这个消息作出反应。

系统广播消息的接收方接受的顺序是:系统级设备驱动程序,网络驱动程序,安装性驱动程序和应用程序。也就是说,如果系统级设备驱动作为接收方,他总是最先有机会接收到消息并作出响应。

在给定的接收类型中,没有哪个驱动程序能保证一定会在别的驱动程序之前接受消息。也就是说,发给指定驱动程序的消息必须要有一个全局唯一消息标识符,从而避免其他驱动程序无意中处理他。

你可以通过对SendMessage, SendMessageCallback, SendMessageTimeoutSendNotifyMessage函数中的窗口句柄设置为HWND_BROADCAST来吧消息广播给所有的顶层窗口。

应用程序会通过他们的顶层窗口的窗口过程函数来接收消息。消息不会发给子窗口。服务可以通过窗口过程或者服务控制处理程序来接收消息。

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

七、查询消息

    你可以定义自己的自定义消息用来在你的应用程序和系统组件之间进行协作活动。加入你创建了你自己的安装性驱动程序或者系统级设备驱动程序的时候特别有用。你的自定义消息可以运载信息在你的驱动程序和你的应用程序之间进行交互。

    要想查询接收方执行给定动作的权限,可以使用查询消息。你可以在调用BroadcastSystemMessage函数的时候设置dwFlags参数为BSF_QUERY来产生你自己的查询消息。没一个查询消息接收方都必须为这个函数返回True并且把消息发送给下一个接收者。如果任何一个接收方返回BROADCAST_QUERY_DENY,这个广播过程就会立刻终止并且函数返回一个0值。

 

注释:你可以创建安装型驱动程序来广播和处理消息。一个安装型的驱动程序是一个动态连接库,他导出了 DriverProc 函数。这个驱动程序通过 DriverProc 函数来接收消息和通过使用BroadcastSystemMessage来广播消息。安装性驱动一般是用来支持多媒体设备,例如声卡,但是也可以用在其他设备和其他目的上。

 

 

 

 

    

 

 

 

     

 

 

 

   

 

转载地址:http://hdcpi.baihongyu.com/

你可能感兴趣的文章
C++模板
查看>>
【C#】如何实现一个迭代器
查看>>
【C#】利用Conditional属性完成编译忽略
查看>>
DirectX11 光照演示示例Demo
查看>>
VUe+webpack构建单页router应用(一)
查看>>
Node.js-模块和包
查看>>
(python版)《剑指Offer》JZ01:二维数组中的查找
查看>>
Spring MVC中使用Thymeleaf模板引擎
查看>>
PHP 7 的五大新特性
查看>>
深入了解php底层机制
查看>>
PHP中的stdClass 【转】
查看>>
XHProf-php轻量级的性能分析工具
查看>>
OpenCV gpu模块样例注释:video_reader.cpp
查看>>
OpenCV meanshift目标跟踪总结
查看>>
就在昨天,全球 42 亿 IPv4 地址宣告耗尽!
查看>>
听说玩这些游戏能提升编程能力?
查看>>
如果你还不了解 RTC,那我强烈建议你看看这个!
查看>>
沙雕程序员在无聊的时候,都搞出了哪些好玩的小玩意...
查看>>
Mysql复制表以及复制数据库
查看>>
matplotlib.pyplot.plot()参数详解
查看>>