Windows 消息传递机制详解
Windows是一个消息(Message)驱动系统。Windows的消息提供了应用程序之间、应用程序与Windows系统之间进行通信的手段。应用程序想要实现的功能由消息来触发,并且靠对消息的响应和处理来完成。必须注意的是,消息并非是抢占性的,无论事件的缓急,总是按照到达的先后派对,依次处理(一些系统消息除外),这样可能使一些实时外部事件得不到及时处理。
Windows的应用程序一般包含窗口(Window),它主要为用户提供一种可视化的交互方式,窗口是总是在某个线程(Thread)内创建的。Windows系统通过消息机制来管理交互,消息(Message)被发送,保存,处理,一个线程会维护自己的一套消息队列(Message Queue),以保持线程间的独占性。队列的特点无非是先进先出,这种机制可以实现一种异步的需求响应过程。
目录:
1、消息
2 、消息类型
3 、消息队列(Message Queues)
4 、队列消息(Queued Messages)和非队列消息(Non-Queued Messages)
5 、PostMessage(PostThreadMessage), SendMessage
6 、GetMessage, PeekMessage
7 、TranslateMessage, TranslateAccelerator
8、(消息死锁( Message Deadlocks)
9、BroadcastSystemMessage
10、消息的处理
11、MFC的消息映射
12、消息反射机制
1、消息
对于一个win32程序来说十分重要,它是一个程序运行的动力源泉。一个消息,是系统定义的一个32位的值,他唯一的定义了一个事件,向Windows发出一个通知,告诉应用程序某个事情发生了。例如,单击鼠标、改变窗口尺寸、按下键盘上的一个键 都会使Windows发送一个消息给应用程序。
消息本身是作为一个记录传递给应用程序的,这个记录中包含了消息的类型以及其他信息。例如,对于单击鼠标所产生的消息来 说,这个记录中包含了单击鼠标时的坐标。这个记录类型叫做MSG,MSG含有来自windows应用程序消息队列的消息信息,它在 Windows中声明如下:
typedef struct tagMsg
{
HWND hwnd; // 接受该消息的窗口句柄
UINT message; // 消息常量标识符,也就是我们通常所说的消息号
WPARAM wParam; // 32位消息的特定附加信息,确切含义依赖于消息值
LPARAM lParam; // 32位消息的特定附加信息,确切含义依赖于消息值
DWORD time; // 消息创建时的时间
POINT pt; // 消息创建时的鼠标/光标在屏幕坐标系中的位置
}MSG;
消息可以由系统或者应用程序产生。系统在发生输入事件时产生消息。举个例子, 当用户敲键, 移动鼠标或者单击控件。系统也产生消息以响应由应用程序带来的变化, 比如应用程序改变系统字体,改变窗体大小。应用程序可以产生消息使窗体执行任务,或者与其他应用程序中的窗口通讯。
2、消息类型
1) 系统定义消息(System-Defined Messages)
在SDK中事先定义好的消息,非用户定义的,其范围在[0x0000, 0x03ff]之间, 可以分为以下三类:
1> 窗口消息(Windows Message)
与窗口的内部运作有关,如创建窗口,绘制窗口,销毁窗口等。可以是一般的窗口,也可以是Dialog,控件等。 如:WM_CREATE, WM_PAINT, WM_MOUSEMOVE, WM_CTLCOLOR, WM_HSCROLL…
2> 命令消息(Command Message)
与处理用户请求有关, 如单击菜单项或工具栏或控件时, 就会产生命令消息。
WM_COMMAND, LOWORD(wParam)表示菜单项,工具栏按钮或控件的ID。如果是控件, HIWORD(wParam)表示控件消息类型
3> 控件通知(Notify Message)
控件通知消息, 这是最灵活的消息格式, 其Message, wParam, lParam分别为:WM_NOTIFY, 控件ID,指向NMHDR的指针。NMHDR包含控件通知的内容, 可以任意扩展。
2) 程序定义消息(Application-Defined Messages)
用户自定义的消息, 对于其范围有如下规定:
WM_USER: 0x0400-0x7FFF (ex. WM_USER+10)
WM_APP(winver> 4.0): 0x8000-0xBFFF (ex.WM_APP+4)
RegisterWindowMessage: 0xC000-0xFFFF
3、消息队列(Message Queues)
Windows中有两种类型的消息队列
1) 系统消息队列(System Message Queue)
这是一个系统唯一的Queue,设备驱动(mouse, keyboard)会把操作输入转化成消息存在系统队列中,然后系统会把此消息放到目标窗口所在的线程的消息队列(thread-specific message queue)中等待处理
2) 线程消息队列(Thread-specific Message Queue)
每一个GUI线程都会维护这样一个线程消息队列。(这个队列只有在线程调用GDI函数时才会创建,默认不创建)。然后线程消息队列中的消息会被送到相应的窗口过程(WndProc)处理.
注意: 线程消息队列中WM_PAINT,WM_TIMER只有在Queue中没有其他消息的时候才会被处理,WM_PAINT消息还会被合并以提高效率。其他所有消息以先进先出(FIFO)的方式被处理。
4、队列消息(Queued Messages)和非队列消息(Non-Queued Messages)
1)队列消息(Queued Messages)
消息会先保存在消息队列中,消息循环会从此队列中取消息并分发到各窗口处理 、如鼠标,键盘消息。
2) 非队列消息(NonQueued Messages)
消息会绕过系统消息队列和线程消息队列直接发送到窗口过程被处理
如: WM_ACTIVATE, WM_SETFOCUS, WM_SETCURSOR, WM_WINDOWPOSCHANGED
注意: postMessage发送的消息是队列消息,它会把消息Post到消息队列中; SendMessage发送的消息是非队列消息, 被直接送到窗口过程处理.
队列消息和非队列消息的区别:
从消息的发送途径来看,消息可以分成2种:队列消息和非队列消息。消息队列由可以分成系统消息队列和线程消息队列。系统消息队列由Windows维护,线程消息队列则由每个GUI线程自己进行维护,为避免给non-GUI现成创建消息队列,所有线程产生时并没有消息队列,仅当线程第一次调用GDI函数时系统才给线程创建一个消息队列。队列消息送到系统消息队列,然后到线程消息队列;非队列消息直接送给目的窗口过程。
对于队列消息,最常见的是鼠标和键盘触发的消息,例如WM_MOUSERMOVE,WM_CHAR等消息,还有一些其它的消息,例如:WM_PAINT、 WM_TIMER和WM_QUIT。当鼠标、键盘事件被触发后,相应的鼠标或键盘驱动程序就会把这些事件转换成相应的消息,然后输送到系统消息队列,由 Windows系统去进行处理。Windows系统则在适当的时机,从系统消息队列中取出一个消息,根据前面我们所说的MSG消息结构确定消息是要被送往那个窗口,然后把取出的消息送往创建窗口的线程的相应队列,下面的事情就该由线程消息队列操心了,Windows开始忙自己的事情去了。线程看到自己的消息队列中有消息,就从队列中取出来,通过操作系统发送到合适的窗口过程去处理。
一般来讲,系统总是将消息Post在消息队列的末尾。这样保证窗口以先进先出的顺序接受消息。然而,WM_PAINT是一个例外,同一个窗口的多个 WM_PAINT被合并成一个 WM_PAINT 消息, 合并所有的无效区域到一个无效区域。合并WM_PAIN的目的是为了减少刷新窗口的次数。
非队列消息将会绕过系统队列和消息队列,直接将消息发送到窗口过程,。系统发送非队列消息通知窗口,系统发送消息通知窗口。例如,当用户**一个窗口系统发送WM_ACTIVATE, WM_SETFOCUS, and WM_SETCURSOR。这些消息通知窗口它被**了。非队列消息也可以由当应用程序调用系统函数产生。例如,当程序调用SetWindowPos系统发送WM_WINDOWPOSCHANGED消息。一些函数也发送非队列消息,例如下面我们要谈到的函数。
5 、PostMessage(PostThreadMessage), SendMessage
PostMessage:把消息放到指定窗口所在的线程消息队列中后立即返回。 PostThreadMessage:把消息放到指定线程的消息队列中后立即返回。
SendMessage:直接把消息送到窗口过程处理, 处理完了才返回。
PostMessage(异步)和SendMessage(同步)的区别
a、 PostMessage 是异步的,SendMessage 是同步的。
PostMessage 只把消息放到队列,不管消息是不是被处理就返回,消息可能不被处理;
SendMessage等待消息被处理完了才返回,如果消息不被处理,发送消息的线程将一直处于阻塞状态,等待消息的返回。
b、 同一个线程内:
SendMessage 发送消息时,由USER32.DLL模块调用目标窗口的消息处理程序,并将结果返回,SendMessage 在同一个线程里面发送消息不进入线程消息队列;PostMessage 发送的消息要先放到消息队列,然后通过消息循环分派到目标窗口(DispatchMessage)。
c、不同线程:
SendMessage 发送消息到目标窗口的消息队列,然后发送消息的线程在USER32。DLL模块内监视和等待消息的处理结果,直到目标窗口的才处理返回,SendMessage在返回之前还需要做许多工作,如响应别的线程向它发送的SendMessage().PostMessge() 到别的线程的时候最好使用PostThreadMessage 代替。PostMessage()的HWND 参数可以为NULL,相当于PostThreadMessage() + GetCrrentThreadId.
d、系统处理消息。
系统只处理(marshal)系统消息(0–WM_USER),发送用户消息(用户自己定义)时需要用户自己处理。
使用PostMessage,SendNotifyMessage,SendMessageCallback等异步函数发送系统消息时,参数不可以使用指针,因为发送者不等待消息的处理就返回,接收者还没有处理,指针就有可能被释放了,或则内容变化了。
e、在Windows 2000/XP,每个消息队列最多只能存放一定数量的消息,超过的将不会被处理就丢掉。系统默认是10000;:[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows] USERPostMessageLimit
6 、GetMessage, PeekMessage
PeekMessage会立即返回 可以保留消息
GetMessage在有消息时返回 会删除消息
PeekMessage和GetMessage函数的主要区别有:
a. GetMessage的主要功能是从消息队列中“取出”消息,消息被取出以后,就从消息队列中将其删除;而PeekMessage的主要功能是“窥视”消息,如果有消息,就返回true,否则返回false。也可以使用PeekMessage从消息队列中取出消息,这要用到它的一个参数(UINT wRemoveMsg),如果设置为PM_REMOVE,消息则被取出并从消息队列中删除;如果设置为PM_NOREMOVE,消息就不会从消息队列中取出。
b. 如果GetMessage从消息队列中取不到消息,则线程就会被操作系统挂起,等到OS重新调度该线程时,两者的性质不同:使用GetMessage线程仍会被挂起,使用PeekMessage线程会得到CPU的控制权,运行一段时间。
c、GetMessage每次都会等待消息,直到取到消息才返回;而PeekMessage只是查询消息队列,没有消息就立即返回,从返回值判断是否取到了消息。
我们也可以说,PeekMessage是一个具有线程异步行为的函数,不管消息队列中是否有消息,函数都会立即返回。而GetMessage则是一个具有线程同步行为的函数,如果消息队列中没有消息的话,函数就会一直等待,直到消息队列中至少有一条消息时才返回。
如果消息队列中没有消息,PeekMessage总是能返回,这就相当于在执行一个循环,如果消息队列一直为空, 它就进入了一个死循环。GetMessage则不可能因为消息队列为空而进入死循环。
联系:
在Windows的内部,GetMessage和PeekMessage执行着相同的代码,Peekmessage和Getmessage都是向系统的消息队列中取得消息,并将其放置在指定的结构。
区别:
PeekMessage:有消息时返回TRUE,没有消息返回FALSE
GetMessage:有消息时且消息不为WM_QUIT时返回TRUE,如果有消息且为WM_QUIT则返回FALSE,没有消息时不返回。
GetMessage:取得消息后,删除除WM_PAINT消息以外的消息。
PeekMessage:取得消息后,根据wRemoveMsg参数判断是否删除消息。PM_REMOVE则删除,PM_NOREMOVE不删除。
The PeekMessage function normally does not remove WM_PAINT messages from the queue. WM_PAINT messages remain in the queue until they are processed. However, if a WM_PAINT message has a null update region, PeekMessage does remove it from the queue.
不能用PeekMessage从消息队列中删除WM_PAINT消息,从队列中删除WM_PAINT消息可以令窗口显示区域的失效区域变得有效(刷新窗口),如果队列中包含WM_PAINT消息程序就会一直while循环了。
7 、TranslateMessage, TranslateAccelerator
TranslateMessage: 把一个virtual-key消息转化成字符消息(character message),并放到当前线程的消息队列中,消息循环下一次取出处理。
TranslateAccelerator: 将快捷键对应到相应的菜单命令。它会把WM_KEYDOWN 或 WM_SYSKEYDOWN转化成快捷键表中相应的WM_COMMAND 或WM_SYSCOMMAND消息, 然后把转化后的 WM_COMMAND或WM_SYSCOMMAND直接发送到窗口过程处理, 处理完后才会返回。
8、(消息死锁( Message Deadlocks)
假设有线程A和B, 现在有以下下步骤
1) 线程A SendMessage给线程B, A等待消息在线程B中处理后返回
2) 线程B收到了线程A发来的消息,并进行处理, 在处理过程中,B也向线程A SendMessgae,然后等待从A返回。 因为此时, 线程A正等待从线程B返回, 无法处理B发来的消息, 从而导致了线程A,B相互等待, 形成死锁。多个线程也可以形成环形死锁。
可以使用 SendNotifyMessage或SendMessageTimeout来避免出现死锁。
9、BroadcastSystemMessage
我们一般所接触到的消息都是发送给窗口的, 其实, 消息的接收者可以是多种多样的,它可以是应用程序(applications), 可安装驱动(installable drivers), 网络设备(network drivers), 系统级设备驱动(system-level device drivers)等,
BroadcastSystemMessage这个API可以对以上系统组件发送消息。
10、消息的处理
接下来我们谈一下消息的处理,首先我们来看一下VC中的消息泵:
while(GetMessage(&msg, NULL, 0, 0))
{
if(!TranslateAccelerator(msg.hWnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
TranslateMessage(转换消息):
用来把虚拟键消息转换为字符消息。由于Windows对所有键盘编码都是采用虚拟键的定义,这样当按键按下时,并不得字符消息,需要键盘映射转换为字符的消息。
TranslateMessage函数
用于将虚拟键消息转换为字符消息。字符消息被投递到调用线程的消息队列中,当下一次调用GetMessage函数时被取出。当我们敲击键盘上的某个字符键时,系统将产生WM_KEYDOWN和WM_KEYUP消息。这两个消息的附加参数(wParam和lParam)包含的是虚拟键代码和扫描码等信息,而我们在程序中往往需要得到某个字符的ASCII码,TranslateMessage这个函数就可以将WM_KEYDOWN和WM_ KEYUP消息的组合转换为一条WM_CHAR消息(该消息的wParam附加参数包含了字符的ASCII码),并将转换后的新消息投递到调用线程的消息队列中。注意,TranslateMessage函数并不会修改原有的消息,它只是产生新的消息并投递到消息队列中。
也就是说TranslateMessage会发现消息里是否有字符键的消息,如果有字符键的消息,就会产生WM_CHAR消息,如果没有就会产生什么消息。
DispatchMessage(分派消息):
把 TranslateMessage转换的消息发送到窗口的消息处理函数,此函数在窗口注册时已经指定。
首先,GetMessage从进程的主线程的消息队列中获取一个消息并将它复制到MSG结构,如果队列中没有消息,则GetMessage函数将等待一个消息的到来以后才返回。如果你将一个窗口句柄作为第二个参数传入GetMessage,那么只有指定窗口的的消息可以从队列中获得。GetMessage也可以从消息队列中过滤消息只接受消息队列中落在范围内的消息。这时候就要利用GetMessage/PeekMessage指定一个消息过滤器。这个过滤器是一个消息标识符的范围或者是一个窗体句柄,或者两者同时指定。当应用程序要查找一个后入消息队列的消息是很有用。WM_KEYFIRST 和 WM_KEYLAST 常量用于接受所有的键盘消息。 WM_MOUSEFIRST 和 WM_MOUSELAST 常量用于接受所有的鼠标消息。
然后TranslateAccelerator判断该消息是不是一个按键消息并且是一个加速键消息,如果是,则该函数将把几个按键消息转换成一个加速键消息传递给窗口的回调函数。处理了加速键之后,函数TranslateMessage将把两个按键消息WM_KEYDOWN和WM_KEYUP转换成一个 WM_CHAR,不过需要注意的是,消息WM_KEYDOWN,WM_KEYUP仍然将传递给窗口的回调函数。
处理完之后,DispatchMessage函数将把此消息发送给该消息指定的窗口中已设定的回调函数。如果消息是WM_QUIT,则 GetMessage返回0,从而退出循环体。应用程序可以使用PostQuitMessage来结束自己的消息循环。通常在主窗口的 WM_DESTROY消息中调用。
11、MFC的消息映射
使用MFC编程时,消息发送和处理的本质和Win32相同,但是,它对消息处理进行了封装,简化了程序员编程时消息处理的复杂性,它通过消息映射机制来处理消息,程序员不必去设计和实现自己的窗口过程。
说白了,MFC中的消息映射机制实质是一张巨大的消息及其处理函数对应表。消息映射基本上分为两大部分:
在头文件(.h)中有一个宏DECLARE_MESSAGE_MAP(),它放在类的末尾,是一个public属性的;与之对应的是在实现部分(.cpp)增加了一个消息映射表,内容如下:
BEGIN_MASSAGE_MAP(当前类,当前类的基类)
//{{AFX_MSG_MAP(CMainFrame)
消息的入口项
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
但是仅是这两项还不足以完成一条消息,要是一个消息工作,必须还有以下3个部分去协作:
1、在类的定义中加入相应的函数声明;
2、在类的消息映射表中加入相应的消息映射入口项;
3、在类的实现中加入相应的函数体;
消息的添加
(1)、利用Class Wizard实现自动添加
在菜单中选择View -> Class Wizard**Class Wizard,选择Message Map标签,从Class name组合框中选取我们想要添加消息的类。在Object IDs列表框中,选取类的名称。此时,Messages列表框显示该类的可重载成员函数和窗口消息。可重载成员函数显示在列表的上部,以实际虚构成员函数的大小写字母来表示。其他为窗口消息,以大写字母出现。选中我们要添加的消息,单击Add Funtion按钮,Class Wizard自动将该消息添加进来。
有时候,我们想要添加的消息在Message列表中找不到,我们可以利用Class Wizard上Class Info标签以扩展消息列表。在该页中,找到Message Filter组合框,通过它可以改变首页中Messages列表框中的选项。
(2)、手动添加消息
如果Messages列表框中确实没有我们想要的消息,就需要我们手工添加:
1)在类的.h文件中添加处理函数的声明,紧接着在//}}AFX_MSG行之后加入声明,注意,一定要以afx_msg开头。
通常,添加处理函数声明的最好的地方是源代码中Class Wizard维护的表的下面,在它标记其领域的{{ }}括弧外面。这些括弧中的任何东西都有可能会被Class Wizard销毁。
2)接着,在用户类的.cpp文件中找到//}}AFX_MSG_MAP行,紧接在它之后加入消息入口项。同样,也放在{{ }}外面。
3)最后,在该文件中添加消息处理函数的实体。
对于能够使用Class Wizard添加的消息,尽量使用Class Wizard添加,以减少我们的工作量;对于不能使用Class Wizard添加的消息和自定义消息,需要手动添加。总体说来,MFC的消息编程对用户来说,相对比较简单,在此不再使用实例演示。
12、消息反射机制
什么叫消息反射?
父窗口将控件发给它的通知消息,反射回控件进行处理(即让控件处理这个消息),这种通知消息让控件自己处理的机制叫做消息反射机制。
通过前面的学习我们知道,一般情况下,控件向父窗口发送通知消息,由父窗口处理这些通知消息。这样,父窗口(通常是一个对话框)会对这些消息进行处理,换句话说,控件的这些消息处理必须在父窗口类体内,每当我们添加子控件的时候,就要在父窗口类中复制这些代码。很明显,这对代码的维护和移植带来了不便,而且,明显背离C++的对象编程原则。
从4.0版开始,MFC提供了一种消息反射机制(Message Reflection),可以把控件通知消息反射回控件。具体地讲,对于反射消息,如果控件有该消息的处理函数,那么就由控件自己处理该消息,如果控件不处理该消息,则框架会把该消息继续送给父窗口,这样父窗口继续处理该消息。可见,新的消息反射机制并不破坏原来的通知消息处理机制。
消息反射机制为控件提供了处理通知消息的机会,这是很有用的。如果按传统的方法,由父窗口来处理这个消息,则加重了控件对象对父窗口的依赖程度,这显然违背了面向对象的原则。若由控件自己处理消息,则使得控件对象具有更大的独立性,大大方便了代码的维护和移植。
实例M8:简单地演示MFC的消息反射机制。(见附带源码 工程M8)
开VC++ 6.0,新建一个基于对话框的工程M8。
在该工程中,新建一个CMyEdit类,基类是CEdit。接着,在该类中添加三个变量,如下:
private:
CBrush m_brBkgnd;
COLORREF m_clrBkgnd;
COLORREF m_clrText;
在CMyEdit::CMyEdit()中,给这三个变量赋初值:
{
m_clrBkgnd = RGB( 255, 255, 0 );
m_clrText = RGB( 0, 0, 0 );
m_brBkgnd.CreateSolidBrush(RGB( 150, 150, 150) );
}
打开ClassWizard,类名为CMyEdit,Messages处选中“=WM_CTLCOLOR”,您是否发现,WM_CTLCOLOR消息前面有一个等号,它表示该消息是反射消息,也就是说,前面有等号的消息是可以反射的消息。
消息反射函数代码如下:
HBRUSH CMyEdit::CtlColor(CDC* pDC, UINT nCtlColor)
{
// TODO: Change any attributes of the DC here
pDC->SetTextColor( m_clrText );//设置文本颜色
pDC->SetBkColor( m_clrBkgnd );//设置背景颜色
//请注意,在我们改写该函数的内容前,函数返回NULL,即return NULL;
//函数返回NULL将会执行父窗口的CtlColor函数,而不执行控件的CtlColor函数
//所以,我们让函数返回背景刷,而不返回NULL,目的就是为了实现消息反射
return m_brBkgnd; //返回背景刷
}
在IDD_M8_DIALOG对话框中添加一个Edit控件,使用ClassWizard给该Edit控件添加一个CMyEdit类型的变量m_edit1,把Edit控件和CMyEdit关联起来。
林炳文Evankaka原创作品。转载请注明出处http://blog.csdn.net/evankaka
在这里插入代码片
上一篇: jQuery 动画
下一篇: Kafka数据写入Hudi