嵌入式软件工程师&&太极拳
全部博文(548)
分类: 嵌入式
2012-02-20 13:00:04
Windows消息编程
韩耀旭
本文主要包括以下内容:
1、简单理解Windows的消息
2、通过一个简单的Win32程序理解Windows消息
3、通过几个Win32程序实例进一步深入理解Windows消息
4、队列消息和非队列消息
5、WM_COMMAND和WM_NOTIFY
6、MFC的消息映射
7、消息反射机制
1、简单理解Windows的消息
消息,就是指Windows发出的一个通知,告诉应用程序某个事情发生了。
举个例子来说,鼠标单击某应用程序的一个按钮。这时,Windows(操作系统)给应用程序发送这个消息,通知应用程序该按钮被点击,应用程序将进行相应反应。
消息一般用一个32位的数来标识,这个数唯一地标识这个消息。这些消息的标识符一般在头文件winuser.h 中定义,如:
#define WM_PAINT 0x000F
#define WM_QUIT 0x0012
其实消息本身是一个MSG结构。MSG结构定义如下:
typedef struct tagMSG {
HWND hwnd; //接受消息的窗口句柄
UINT message; //消息标识符
WPARAM wParam; //32位附加信息
LPARAM lParam; //32位附加信息
DWORD time; //消息创建的时间
POINT pt; //消息创建时鼠标在屏幕坐标系中的位置
} MSG;
也就是说,对于任何一个消息,都有一个MSG变量与之对应,该变量包含了消息的相关信息。而我们在一般情况下,只使用消息的消息标识符,该标识符也唯一地代表了这个消息。
举个例子来说,当我们收到一个字符消息的时候,message成员变量的值就是WM_CHAR,但用户到底输入的是什么字符,那么就由wParam和lParam来说明。wParam、lParam表示的信息随消息的不同而不同。
Windows操作系统已经给我们定义了大量的消息,这些消息我们称为系统消息。除了系统消息,我们还可以自己定义消息,即自定义消息。
值小于0x0400的消息都是系统消息,自定义消息一般都大于0x0400。
系统消息取值一般有如下规律,如表1:
范围 | 意义 |
0x0001——0x0087 |
主要是窗口消息 |
0x00A0——0x00A9 |
非客户区消息 |
0x0100——0x0108 |
键盘消息 |
0x0111——0x0126 |
菜单消息 |
0x0132——0x0138 |
颜色控制消息 |
0x0200——0x020A |
鼠标消息 |
0x0211——0x0213 |
菜单循环消息 |
0x0220——0x0230 |
多文档消息 |
0x03E0——0x03E8 |
DDE消息 |
0x0400 |
WM_USER |
0x0400——0x7FFF |
自定义消息 |
表1
在WINUSER.H中,我们有定义:
#define WM_USER 0x0400
对于自定义消息,我们一般采用WM_USER 加一个整数值的方法定义自定义消息,如:
#define WM_RECVDATA WM_USER + 1
如果您初次接触Windows编程,或是初次接触Windows消息,对于上述解释可能没有看懂,这也不要着急,后面的实例将会逐步带您对Windows的消息编程有一个了解。
2、通过一个简单的Win32程序理解Windows消息
例程1:一个简单的Win32程序代码(见附带源码 工程M1)
打开VC++ 6.0,新建一个Win32 Application,工程名为M1,在该工程添加C++ Source File,文件名为M1,在该文件中添加如下代码:
图1
图1的解释:
1、Windows操作系统有一个消息队列,它存放操作系统收到的消息。如:当按键被按下,键盘会发送一个消息到操作系统的消息队列。
2、操作系统把系统消息队列中的消息分派到各个应用程序的消息队列。如果它是第1个应用程序的消息,操作系统把它发给第1个应用程序,把它放在第1个应用程序的消息队列;如果它是第2个应用程序的消息,发送给第2个程序的消息队列。
3、应用程序的消息循环从自己的消息队列中取消息,取出的消息调用窗口过程函数进行处理。
4、PostMessage是寄送消息,函数执行后立即返回。寄送的消息是队列消息,放在程序的消息队列中排队处理。一般来说,新寄送的消息排在消息队列的末尾,这样可以保证窗口以先进先出的顺序处理消息。
SendMessage是发送消息,它发出的消息是非队列消息,直接调用窗口过程函数处理。SendMessage函数一直等消息处理完成后才返回。
我们有必要再专门学习一下SendMessage和PostMessage函数。
SendMessage的函数原型:
LRESULT SendMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam);
这个函数向窗口发送一条消息,一直等到消息被处理之后才返回。也就是说,接收消息的窗口的窗口函数立即被调用。函数的返回值由接收消息的窗口的窗口函数返回。
PostMessage的函数原型:
BOOL PostMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam);
该函数把一条消息放置到创建hWnd窗口的线程的消息队列中,该函数不等消息被处理就马上将控制返回。
从上面这两个函数,我们可以看出消息的发送方式和寄送方式的区别:被发送的消息会被立即处理,处理完毕后函数才返回;被寄送的消息不会被立即处理,他被放到一个先进先出的队列中,按次序等候处理,而且函数放置消息后立即返回。
以寄送方式发送的消息通常是与用户输入事件相对应的,因为这些事件不是十分紧迫,可以进行缓冲处理,例如鼠标、键盘消息都是寄送消息。应用程序调用系统函数,系统一般会发送非队列消息。例如,当程序调用SetWindowPos,系统会发送WM_WINDOWPOSCHANGED消息。
例程M4,测试消息队列的容量(见附带源码工程M4)
代码中已经作了注解,编译、运行程序,您就会发现消息队列的最大容量是10000。
例程M5,用记事本查看消息队列和窗口过程函数处理的消息
这个例程的出发点是利用记事本分别捕获消息队列中的消息和窗口过程函数处理过的消息。
该例程还演示了PostMessage和SendMessage的不同。
由于该例程相对复杂一些,例程中的注解也相对多一些。编译、运行程序,弹出如下窗口:
关闭该窗口,退出运行,检查M5例程所在的路径,您就会发现多了两个文件MessageQueue.txt和MessageWndProc.txt,MessageQueue.txt文件中记录的是应用程序M5从运行到关闭消息队列中处理过的消息;MessageWndProc.txt中记录的M5窗口过程函数处理过的消息。
打开MessageQueue.txt文件,如下图:
文件中记录了消息队列中的各个消息以及消息的ID号,其中有一条消息是WM_POSTMESSAGE,这说明PostMessage寄送的WM_POSTMESSAGE消息确实放到了消息队列中。
再打开MessageWndProc.txt文件,如下图:
文件中记录了窗口过程处理的各个消息和消息的ID号,其中有两条消息WM_POSTMESSAGE和WM_SENDMESSAGE,这说明了两个问题:WM_POSTMESSAGE消息从消息队列取出,再次派发到窗口过程函数处理;SendMessage发送的WM_SENDMESSAGE消息,没有经过消息队列,直接送到窗口过程函数处理。
5、WM_COMMAND和WM_NOTIFY
控件通知消息,是指这样一种消息,一个窗口内的控件发生了一些事情,需要通知父窗口。当用户与控件窗**互时,控件通知消息就会从控件窗口发送到它的主窗口,这种消息一般不是为了处理用户命令,而是为了让主窗口能够改变控件。
WM_COMMAND和WM_NOTIFY都是控件通知消息。
在最初的Windows 3.x中,还没有WM_NOTIFY,只存在WM_COMMAND消息,wParam参数中包含一个通知码和控件ID,lParam中包含控件句柄。这样一来,wParam和lParam都被填充了,没有额外的空间来传递一些其它信息,如鼠标按下的位置和时间。
为了解决这个问题,Windows 3.x就提出了一个解决策略,那就是给一些消息添加一些附加消息,比如控件自画用到的DRAWITEMSTRUCT等,这样,不同的消息附加的内容不同,结果是非常混乱。
在Win32中,微软又提出了一个更好的解决方案,引进了NMHDR结构。这个结构的引进把消息统一起来,利用它可以传递各种复杂的消息。
NMHDR结构内容如下:
NMHDR
{
HWND hWndFrom;//相当于原WM_COMMAND消息的lParam
UINT idFrom; //相当于原WM_COMMAND消息的wParam(LOWORD)
UINT code; //相当于原WM_COMMAND消息的wParam(HIWORD)通知码
}
使用这个结构,WM_NOTIFY还可以附带更多的信息,您可以定义一个更大的结构,这个结构的第一个元素就是NMHDR结构,在该元素的后面您还可以放置其它附加信息。由于在这个大结构中,第一个成员是NMHDR,这样一来,我们就可以利用指向NMHDR的指针来指向这个结构,不论后面有没有其它内容。
可见,WM_NOTIFY和WM_COMMAND相比,是一种更灵活的消息格式,lParam中放的是一个称为NMHDR结构的指针。在wParam中放的则是控件的ID。最初Windows 3.x就有的控件,如Edit,Combo,List,Button等,发送的控件通知消息的格式是WM_COMMAND;而后期的Win32通用控件,如List View,Image List,IP Address,Tree View,Toolbar等,发送的都是WM_NOTIFY控件通知消息。
另外,当用户选择菜单的一个命令项,也会发送WM_COMMAND消息。
当用户选择菜单的一个命令项或控件给父窗口发送通知消息,都可以使用WM_COMMAND消息。为了区分这两种情况,规定它们有以下区别,如表2:
消息来源
wParam (high word)
wParam (low word)
lParam
菜单
0
菜单标识符 (IDM_*)
0
控件
控件定义的通知码
控件ID
控件窗口的句柄
表2
例程M6,演示菜单发出WM_COMMAND消息和子控件发送WM_COMMAND消息的区别(见附带源码工程M6)
打开VC++ 6.0,新建Win32 Application工程M6,然后在该工程中新建C++ Source File,文件名为M6,M6的文件内容具体见例程M6。
在例程M6所在的路径打开M6文件夹,新建一个文本文档,如下图:
将“新建文本文档.txt”改名为“M6.rc”,如下图:
右键单击M6.rc,在弹出的快捷菜单中使用“写字板”打开,如下图:
添加的内容具体见M6.rc,保存后退出。编译、运行工程M6,弹出如下窗口:
分别单击“FirstButton”按钮和“Menu1”菜单,会弹出相应的提示消息框。
M6中对于WM_COMMAND消息的处理,源代码如下:
对于WM_COMMAND消息,因为菜单和子控件都能触发。我们首先判断lParam,如果lParam为0,是菜单触发的WM_COMMAND消息;如果lParam不为0,是子控件触发的WM_COMMAND控件通知消息。对于菜单触发的WM_COMMAND消息,我们再通过(LOWORD(wParam))(菜单的标识ID)判断是哪个菜单触发的消息;对于控件触发的WM_COMMAND消息,我们通过(LOWORD(wParam))(控件ID)知道是哪个控件触发的消息,而且通过(HIWORD(wParam))(控件定义的通知码)知道控件到底触发了什么消息。
本例程我们纯手工添加并编辑资源文件M6.rc,之所以这样做是为了让您了解资源文件的实质。实际编程中,您完全可以利用资源编辑器更加方便地添加、编辑资源文件,后面的例程将会演示说明。
例程M7,演示WM_NOTIFY控件通知消息(见附带源码 工程M7)
WM_NOTIFY消息是通用控件发送给其父窗口的消息,其中参数wParam 是发送消息的通用控件的ID,参数lParam 是一个指针,这个指针指向一个 NMHDR 结构,该结构包含了通知码和其它附加信息。
下面我们看结构NMHDR:
typedef struct tagNMHDR {
//发送消息的控件的句柄,相当于原WM_COMMAND消息的lParam
HWND hwndFrom;
//发送消息的控件的ID,相当于原WM_COMMAND消息的wParam(LOWORD)
UINT idFrom;
//通知码,也就是发送的具体消息,相当于原WM_COMMAND消息的wParam(HIWORD)通知码
UINT code;
} NMHDR;
打开VC++ 6.0,新建Win32 Application工程M7,然后在该工程中新建C++ Source File,文件名为M7,M7的文件内容具体见例程M7。
下面,我们利用资源编辑器添加资源。单击“文件”->“新建”,在“新建”对话框中选中“Resource Script”,文件名为“M7”,如下图:
单击“确定”,添加M7资源文件。
右击“M7.RC”文件夹,选中“Insert…”菜单项,如下图:
弹出“插入资源”对话框,
选中“Dialog”,点击“新建”按钮,新建一个对话框资源。
右击新建的“IDD_DIALOG1”,在属性对话框中将ID改为“IDC_DIALOG”,关闭属性框。
双击“IDC_DIALOG”,打开该对话框,调整至合适大小,在对话框上添加一个列表控件(List Control),将该列表控件的ID设置为IDC_LIST,如下图:
并且把列表控件改为“Report”类型,如下图:
编辑并运行程序,程序运行会弹出如下对话框:
分别用鼠标双击第一行或第二行,会弹出相应消息框。
程序代码都有详细注释,您可以阅读代码,细细体会WM_NOTIFY控件通知消息。
6、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的消息编程对用户来说,相对比较简单,在此不再使用实例演示。
7、消息反射机制
什么叫消息反射?
父窗口将控件发给它的通知消息,反射回控件进行处理(即让控件处理这个消息),这种通知消息让控件自己处理的机制叫做消息反射机制。
通过前面的学习我们知道,一般情况下,控件向父窗口发送通知消息,由父窗口处理这些通知消息。这样,父窗口(通常是一个对话框)会对这些消息进行处理,换句话说,控件的这些消息处理必须在父窗口类体内,每当我们添加子控件的时候,就要在父窗口类中复制这些代码。很明显,这对代码的维护和移植带来了不便,而且,明显背离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消息前面有一个等号,它表示该消息是反射消息,也就是说,前面有等号的消息是可以反射的消息。
消息反射函数代码如下:
在IDD_M8_DIALOG对话框中添加一个Edit控件,使用ClassWizard给该Edit控件添加一个CMyEdit类型的变量m_edit1,把Edit控件和CMyEdit关联起来。
编译,运行程序,观察运行效果。
就写这些吧,水平有限,希望能对您有所帮助。