林炳文Evankaka原创作品。转载请注明出处http://blog.csdn.net/evankaka
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)表示菜单项。工具栏button或控件的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的目的是为了降低刷新窗体的次数。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvRXZhbmtha2E=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />
非队列消息将会绕过系统队列和消息队列,直接将消息发送到窗体过程。。系统发送非队列消息通知窗体。系统发送消息通知窗体。比如,当用户激活一个窗体系统发送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 Funtionbutton,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的消息反射机制。(见附带源代码 projectM8)
开VC++ 6.0,新建一个基于对话框的projectM8。
在该project中。新建一个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原创作品。