在传统的C程序中,我们调用fopen函数打开文件。这个库函数最终调用操作系统(提供的函数)来打开文件。而在Windows中,不仅用户程序可以调用系统的API函数,反过来,系统也会调用用户程序,这个调用是通过消息来进行的。
Windows程序设计是一种完全不同于传统的DOS方式的程序设计方法。它是一种事件驱动方式的程序设讨模式,主要是基于消息的。例如,当用户在窗口中画图的时候,按下鼠标左键,此时,操作系统会感知到这个事件。于是将这个事件包装成一个消息(这就是事件和消息的关系),投递到应用程序的消息队列中,然后应用程序从消息队列中取出消息并进行响应。在这个处理过程中,操作系统也会给应用程序“发送消息”。所谓“发送消息”实际上是操作系统调用程序中-个专门负责处理消息的函数,这个函数称为窗口过程。也就是说,窗口过程函数是由操作系统发给应用程序的。
1)消息的概念和表示
消息(Message)指的就是Windows 操作系统发给应用程序的一个通告,它告诉应用程序某个特定的事件发生了。比如,用户单击鼠标或按键都会引发Windows 系统发送相应的消息。最终处理消息的是应用程序的窗口函数,如果程序不负责处理的话系统将会作出默认处理。从数据结构的角度来说,消息是一个结构体,它包含了消息的类型标识符以及其他的一些附加信息。系统定义的结构体MSG用于表示消息,MSG 具有如下定义形式:
typedef struct tagMSG { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; #ifdef _MAC DWORD lPrivate; #endif } MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;
第一个成员变量hwnd表示消息所属的窗口。我们通常开发的程序都是窗门应用程序,一个消息一般都是与某个窗口相关联的。在Windows程序中,用HWND类型的变量来标识窗口。
第二个成员变量message指定了消息的标识符。在Windows中,消息是由一个数值来表示的,不同的消息对应不同的数值。但是由于数值不便于记忆,所以Windows将消息对应的数值定义为WM_XXX宏(WM是Window Message的缩写)的形式,XXX对应某种消息的英文拼写的大写形式。
第三、第四个成员变量wParam和lParam,用于指定消息的附加信息。例如,当我们收到一个字符消息的时候,message成员变量的值就是WM_CHAR,但用户到底输入的是什么字符,那么就由wParam和lParam来说明。wParam,lParam表示的信息随消息的不同而不同。如果想知道这两个成员变量具体表示的信息,可以在MSDN中关于某个具体消息的说明文档查看到。读者可以在VC++的开发环境中通过go to definition查看一下WPARAM和LPARAM这两种类型的定义,可以发现这两种类型实际上就是unsigned int和long。
最后两个变量分别表示消息投递到消息队列中的时间和鼠标的当前位置。
2)消息是分类
(1)Windows不同消息的取值范围:
(2)消息分类
标准消息(WM_XXX)
标准windows消息由窗口和视图来处理,这类消息通常包含有用于确定如何对消息进行处理的一些参数,通常由WM为前缀,比如窗口创建WM_CREATE,窗口销毁WM_DESTROY等。所有派生自CWnd 的类才有资格接收标准消息。
控件通知消息(WM_NOTIFY )
控件通知消息包括按下按钮或输入字符等事件消息。同标准Windows消息一样,控件通知消息由窗口和视图处理。比如下拉列表框选项的改变CBN_SELCHANGE 和树形控件的TVN_SELCHANGED 消息都是通告消息。Window 9x 版及以后的新控件通告消息不再通过WM_COMMAND 传送,而是通过WM_NOTIFY 传送, 但是老控件的通告消息, 比如CBN_SELCHANGE 还是通过
WM_COMMAND 消息发送。
命令消息(WM_COMMAND)
命令消息包含来自用户界面对象,比如菜单项的选择,工具栏按钮点击等发出该消息。所有派生自CCmdTarget 的类都有能力接收WM_COMMAND 消息。命令消息与其他消息的处理不同,它可被更广泛的对象(如文档,文档模块,应用程序模块、窗口和视图等)处理。如果某条命令直接影响到某个对象,则由该对象来处理这条命令。
自定义消息
利用MFC 编程,可以使用自定义消息,其取值范围必须大于WM_USER。。使用自定义消息需要遵循一定的步骤,并需要自己编写消息响应函数。
消息ID只是一个整数,Windows系统预定义了很多消息ID,以不同的前缀来划分,比如WM_*,CB_*等等。具体见下表:
二 消息队列
Windows系统本身会维护一个唯一的消息队列,以便于发送给各个线程,这是系统内部的实现方式。而对于线程来说,每个线程可以拥有自己的消息队列,它和线程一一对应。在线程刚创建时,消息队列并不会被创建,而是当GDI的函数调用发生时,indows系统才认为有必要为线程创建消息队列。消息队列包含在一个叫THREADINFO的结构中,有四个队列:
1)Sent Message Queue
2)Posted Message Queue
3)Visualized Input Queue
4)Reply Message Queue
之所以维护多个队列,是因为不同消息的处理方式和处理顺序是不同的。
每一个Windows应用程序开始执行后,系统都会为该程序创建一个消息队列,这个消息队列用来存放该程序创建的窗口的消息。例如,当我们按下鼠标左键的时候,将会产生WM_LBUITONDOWN消息,系统会将这个消息放到窗口所属的应用程序的消息队列中,等待应用程序的处理。Windows将产生的消息依次放到消息队列中,而应用程序则通过一个消息循环不断地从消息队列中取出消息,并进行响应。这种消息机制,就是Windows程序运行的机制。
三 消息循环
窗口由线程创建,一个线程可以创建多个窗口。窗口可由CreateWindow()函数创建,但前提是需要提供一个已注册的窗口类(Window Class),每一个窗口类在注册时需要指定一个窗口处理函数(Window Procedure),这个函数是一个回调函数,就是用来处理消息的。而由一个线程来创建对应于不同的窗口类的窗口是可以的。由此可见,只要注册多个窗口类,每个窗口都可以拥有自己的消息处理函数,而同时,他们属于同一个线程。
在设计窗口,注册窗口,创建窗口,显示窗口、更新窗口后,我们需要编写一个消息循环,不断地从消息队列中取出消息,并进行响应。要从消息队列中取出消息,我们需要调用GetMessage()函数,该函数的原型声明如下:
BOOL GetMessage( LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax );
参数lpMsg指向一个消息(MSG)结构体。GetMessage从线程的消息队列中取出的消息信息将保存在该结构体对象中。
参数hWnd指定接收属于哪一个窗口的消息。通常我们将其设置为NULL,用于接收属于调用线程的所有窗口的窗口消息。
参数wMsgFilterMin指定要获取的消息的最小值,通常设为0。
参数wMsgFilterMax指定要获取的消息的最大值。如果wMsgFilterMin和wMsgFilterMax都设置为0,则接收所有消息。
GetMessage函数接收到除WM_QUIT外的消息均返回非零值。对于WM_QUIT 消息,该函数返回零。如果出现了错误,该函数返回-1。
一个典型的消息循环如下所示(注意这里没有处理GetMessage出错的情况):
while(GetMessage(&msg, NULL, 0, 0 ) != FALSE) { TranslateMessage(&msg); DispatchMessage(&msg); }
TranslateMessage函数用于将虚拟键消息转换为字符消息。字符消息被投递到调用线程的消息队列中,当下一次调用GetMessage函数时被取出。当我们敲击键盘上的某个字符键时,系统将产生WM_KEYDOWN和WM_KEYUP消息。这两个消息的附加参数(wParam和lParam)包含的是虚拟键代码和扫描码等信息,而我们在程序中往往需要得到某个字符的ASCII码,TranslateMessage这个函数就可以将WM_KEYDOWN和WM_KEYUP消息的组合转换为一条WM_CHAR 消息(该消息的wParam附加参数包含了字符的ASCII码),并将转换后的新消息投递到调用线程的消息队列中。注意,TranslateMessage函数并不会修改原有的消息,它只是产生新的消息并投递到消息队列中。
DispatchMessage函数分派一个消息到窗口过程,由窗口过程函数对消息进行处理。DispachMessage实际上是将消息回传给操作系统,由操作系统调用窗口过程函数对消息进行处理(响应)。这里有一个灵活性,消息从队列拿出之后,也可以不分发,进行一些别的特殊操作。
GetMessage()的处理流程如下:
1) 处理Sent Message Queue里的消息,这些消息主要是由其他线程的SendMessage()发送,因为他们不能直接调用本线程的处理函数,而本线程调用 SendMessage()时会直接调用处理函数。一旦调用GetMessage(),所有的Sent Message都会被处理掉,并且GetMessage()不会返回;
2)处理Posted Message Queue里的消息,这里拿到一个消息后,GetMessage()将它拷贝到MSG结构中并返回TRUE。注意有三个消息WM_QUIT, WM_PAINT, WM_TIMER会被特殊处理,他们总是放在队列的最后面,直到没有其他消息的时候才被处理,连续的WM_PAINT消息甚至会被合并成一个以提高效率。从后面讨论的这三个消息的发送方式可以看出,使用Send或Post消息到队列里情况不多。
3)处理QS_QUIT开关,这个开关由PostQuitMessage()函数设置,表示线程需要结束。这里为什么不用Send或Post一个 WM_QUIT消息呢?据称:一个原因是处理内存紧缺的特殊情况,在这种情况下Send和Post很可能失败;其次是可以保证线程结束之前,所有Sent 和Posted消息都得到了处理,这是因为要保证程序运行的正确性,或者数据丢失?不得而知。如果QS_QUIT打开,GetMessage()会填充一个WM_QUIT消息并返回FALSE。
4) 处理Virtualized Input Queue里的消息,主要包括硬件输入和系统内部消息,并返回TRUE;
5)再次处理Sent Message Queue,来自MSDN却没有解释。难道在检查2、3、4步骤的时候可能出现新的Sent Message?或者是要保证推后处理后面两个消息;
6)处理QS_PAINT开关,这个开关只和线程拥有的窗口的有效性(Validated)有关,不受WM_PAINT的影响,当窗口无效需要重画的时候这个开关就会打开。当QS_PAINT打开的时候,GetMessage()会返回一个WM_PAINT消息。处理QS_PAINT放在后面,因为重绘一般比较慢,这样有助于提高效率;
7)处理QS_TIMER开关,和QS_PAINT类似,返回WM_TIMER消息,之所以它放在QS_PAINT之后是因为其优先级更低,如果Timer消息要求重绘但优先级又比Paint高,那么Paint就没有机会运行了。
如果GetMessage()中任何消息可处理,GetMessage()不会返回,而是将线程挂起,也就不会占用CPU时间了。类似的WaitMessage()函数也是这个作用。
下面这个消息处理流程图:
还有一个PeekMessage(),其原型为:
BOOL PeekMessage( LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMsg );它的处理方式和GetMessage()一样,只是多了一个参数wRemoveMsg,可以指定是否移除队列里的消息。最大的不同应该是,当没有消息可处理时,PeekMessage()不是挂起等待消息的到来,而是立即返回FALSE。
窗口内的消息的路由是怎样的?窗口和其控件的关系是什么?
一个窗口(Window)可以有一个Parent属性,对一个Parent窗口来说,属于它的窗口被称为子窗口(Child Window)。控件(Control)或对话框(Dialog)也是窗口,他们一般属于某个父窗口。所有的窗口都有自己的句柄(HWND),消息被发送时,这个句柄就已经被指定了。所以当子窗口收到一个消息时,其父窗口不会也收到这个消息,除非子窗口手动的转发。
参考资料:
1、Windows的消息机制
2、Windows消息机制(Windows Messaging)