Windows消息编程 本文主要包括以下内容: 1、简单理解Windows的消息 消息,就是指Windows发出的一个通知,告诉应用程序某个事情发生了。 #define WM_PAINT 0x000F
typedef struct tagMSG { 也就是说,对于任何一个消息,都有一个MSG变量与之对应,该变量包含了消息的相关信息。而我们在一般情况下,只使用消息的消息标识符,该标识符也唯一地代表了这个消息。
表1 在WINUSER.H中,我们有定义: #define WM_USER 0x0400 对于自定义消息,我们一般采用WM_USER 加一个整数值的方法定义自定义消息,如: #define WM_RECVDATA WM_USER + 1
2、通过一个简单的Win32程序理解Windows消息 //一个简单的Win32应用程序 //通过这个简单的实例讲解Windows消息是如何传递的 #include <windows.h> //声明窗口过程函数 LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM); //定义一个全局变量,作为窗口类名 TCHAR szClassName[] = TEXT("SimpleWin32"); //应用程序主函数 int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLine, int iCmdShow) { //窗口类 WNDCLASS wndclass; //当窗口水平方向的宽度和垂直方向的高度变化时重绘整个窗口 wndclass.style = CS_HREDRAW|CS_VREDRAW; //关联窗口过程函数 wndclass.lpfnWndProc = WndProc; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hInstance = hInstance;//实例句柄 wndclass.hIcon = LoadIcon(NULL,IDI_APPLICATION);//图标 wndclass.hCursor = LoadCursor(NULL,IDC_ARROW);//光标 wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);//画刷 wndclass.lpszMenuName = NULL;//菜单 wndclass.lpszClassName = szClassName;//类名称 //注册窗口类 if(!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("RegisterClass Fail!"), szClassName, MB_ICONERROR); return 0; } //建立窗口 HWND hwnd; hwnd = CreateWindow(szClassName,//窗口类名称 TEXT ("The Simple Win32 Application"),//窗口标题 WS_OVERLAPPEDWINDOW,//窗口风格,即通常我们使用的windows窗口样式 CW_USEDEFAULT,//指定窗口的初始水平位置,即屏幕坐标系的窗口的左上角的X坐标 CW_USEDEFAULT,//指定窗口的初始垂直位置,即屏幕坐标系的窗口的左上角的Y坐标 CW_USEDEFAULT,//窗口的宽度 CW_USEDEFAULT,//窗口的高度 NULL,//父窗口句柄 NULL,//窗口菜单句柄 hInstance,//实例句柄 NULL); ShowWindow(hwnd,iCmdShow);//显示窗口 UpdateWindow(hwnd);//立即显示窗口 //消息循环 MSG msg; while(GetMessage(&msg,NULL,0,0))//从消息队列中取消息 { TranslateMessage (&msg); //转换消息 DispatchMessage (&msg); //派发消息 } return msg.wParam; } //消息处理函数 //参数:窗口句柄,消息,消息参数,消息参数 LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { //处理感兴趣的消息 switch (message) { case WM_DESTROY: //当用户关闭窗口,窗口销毁,程序需结束,发退出消息,以退出消息循环 PostQuitMessage(0); return 0; } //其他消息交给由系统提供的缺省处理函数 return ::DefWindowProc (hwnd, message, wParam, lParam); }这是一个非常简单的Win32小程序,编译运行会显示一个窗口,关闭窗口程序会结束运行。 代码中已经做了简单注解,这里我们不作过多说明。我在这里再着重讲解一下消息循环部分。 //消息循环 MSG msg; while(GetMessage(&msg,NULL,0,0))//从消息队列中取消息 { TranslateMessage (&msg); //转换消息 DispatchMessage (&msg); //派发消息 } 这段代码是消息循环部分,它的作用是循环检测消息队列(不懂消息队列?没关系,后面会详细说明)中的消息并进行处理。这段代码涉及GetMessage,TranslateMessage,DispatchMessage这三个函数,相关函数还有PeekMessage,WaitMessage。在此,我们先对这五个函数简单讲解。 1、GetMessage 函数原型: BOOL GetMessage(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax); 参数: lpMsg:一个指向MSG结构的指针,该结构用于存放从消息队列里取出的消息。 返回值 GetMessage检索到WM_QUIT消息,返回值是零;其它情况,返回非零值。 函数功能: 这个API函数用来从消息队列中“摘取”一个消息,放到lpMsg所指的变量里。(注:如果所取窗口的消息队列中没有消息,则程序会暂停在GetMessage(…) 函数里,不会返回。) 如语句: while(GetMessage(&msg,NULL,0,0)) …… 2 、PeekMessage 函数原型: BOOL PeekMessage(LPMSG lpMsg,HWND hWnd,UINT wMsgFilterMin,UINT wMsgFilterMax,UINT wRemoveMsg); 参数: lpMsg、hWnd、wMsgFilterMin、wMsgFilterMax这四个参数的意义和GetMessage对应参数的意义相同,在此不再赘述。 返回值: 消息队列中有消息,返回值为TRUE;消息队列中没有消息,返回值为FALSE。 函数功能: PeekMessage()也是从消息队列中取消息,但它是GetMessage()不同,主要在以下两点: (一)、GetMessage()只能从消息队列中取走消息,也就是说,GetMessage()执行后,该消息将从消息队列中移除。 (二)、当消息队列中没有消息时,GetMessage()将会阻塞线程,等待消息;而PeekMessage()与GetMessage()不同,它执行后会立刻返回,消息队列中有消息时,返回值为TRUE;消息队列中没有消息时,返回值为FALSE。 3 、WaitMessage 函数原型: BOOL WaitMessage(VOID); 函数功能: 这个函数的作用是当消息队列中没有消息时,将控制权交给其它线程。该函数将会使线程挂起,直到消息队列中又有新消息。 4 、TranslateMessage 函数原型: BOOL TranslateMessage(CONST MSG*lpMsg); 参数: IpMsg:指向MSG结构的指针,该结构是函数GetMessage或PeekMessage从消息队列里取得的消息。 5、DispatchMessage 函数原型: LONG DispatchMessage(CONST MSG *lpmsg); 函数功能: GetMessage()从消息队列中取消息,对取出的消息进行转换(TranslateMessage),对于能够将虚拟键码转化成字符码的消息,会在消息队列里放一条WM_CHAR消息,最后将消息发送到相应的消息处理函数进行处理。循环执行这个处理过程,直到收到WM_QUIT消息,才退出循环,结束程序。 3、通过几个Win32程序实例进一步深入理解Windows消息 例程2:对比使用GetMessage和PeekMessage处理消息循环(见附带源码 工程M2) //消息循环 MSG msg; while(true) { if(PeekMessage(&msg,NULL,0,0,PM_REMOVE)) //从消息队列中取消息 { if(msg.message == WM_QUIT) break; TranslateMessage (&msg); //转换消息 DispatchMessage (&msg); //派发消息 } else WaitMessage(); } //End of while(true) 编译、运行工程M2,观察运行效果,可以看出,使用PeekMessage处理消息循环同样能够达到与GetMessage相同的效果。 4、队列消息和非队列消息 图1 图1的解释: 1、Windows操作系统有一个消息队列,它存放操作系统收到的消息。如:当按键被按下,键盘会发送一个消息到操作系统的消息队列。 我们有必要再专门学习一下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窗口的线程的消息队列中,该函数不等消息被处理就马上将控制返回。 关闭该窗口,退出运行,检查M5例程所在的路径,您就会发现多了两个文件MessageQueue.txt和MessageWndProc.txt,MessageQueue.txt文件中记录的是应用程序M5从运行到关闭消息队列中处理过的消息;MessageWndProc.txt中记录的M5窗口过程函数处理过的消息。 文件中记录了消息队列中的各个消息以及消息的ID号,其中有一条消息是WM_POSTMESSAGE,这说明PostMessage寄送的WM_POSTMESSAGE消息确实放到了消息队列中。 文件中记录了窗口过程处理的各个消息和消息的ID号,其中有两条消息WM_POSTMESSAGE和WM_SENDMESSAGE,这说明了两个问题:WM_POSTMESSAGE消息从消息队列取出,再次派发到窗口过程函数处理;SendMessage发送的WM_SENDMESSAGE消息,没有经过消息队列,直接送到窗口过程函数处理。 5、WM_COMMAND和WM_NOTIFY 控件通知消息,是指这样一种消息,一个窗口内的控件发生了一些事情,需要通知父窗口。当用户与控件窗口交互时,控件通知消息就会从控件窗口发送到它的主窗口,这种消息一般不是为了处理用户命令,而是为了让主窗口能够改变控件。 NMHDR 使用这个结构,WM_NOTIFY还可以附带更多的信息,您可以定义一个更大的结构,这个结构的第一个元素就是NMHDR结构,在该元素的后面您还可以放置其它附加信息。由于在这个大结构中,第一个成员是NMHDR,这样一来,我们就可以利用指向NMHDR的指针来指向这个结构,不论后面有没有其它内容。 消息来源 表2 例程M6,演示菜单发出WM_COMMAND消息和子控件发送WM_COMMAND消息的区别(见附带源码工程M6) 将“新建文本文档.txt”改名为“M6.rc”,如下图: 右键单击M6.rc,在弹出的快捷菜单中使用“写字板”打开,如下图: 添加的内容具体见M6.rc,保存后退出。编译、运行工程M6,弹出如下窗口: 分别单击“FirstButton”按钮和“Menu1”菜单,会弹出相应的提示消息框。 M6中对于WM_COMMAND消息的处理,源代码如下: case WM_COMMAND: { if(lParam == 0) { switch(LOWORD(wParam)) { case IDM_MENU1: MessageBox(NULL,"MENU1菜单被点击","M6",MB_OK); break; case IDM_EXIT: DestroyWindow(hwnd); break; } } else //处理子控件触发的WM_COMMAND控件通知消息 { //(LOWORD(wParam))是控件ID switch(LOWORD(wParam)) { case ButtonID1: if(HIWORD(wParam) == BN_CLICKED) { MessageBox(NULL,"按钮被点击","M6",MB_OK); } break; } } } break; 对于WM_COMMAND消息,因为菜单和子控件都能触发。我们首先判断lParam,如果lParam为0,是菜单触发的WM_COMMAND消息;如果lParam不为0,是子控件触发的WM_COMMAND控件通知消息。对于菜单触发的WM_COMMAND消息,我们再通过(LOWORD(wParam))(菜单的标识ID)判断是哪个菜单触发的消息;对于控件触发的WM_COMMAND消息,我们通过(LOWORD(wParam))(控件ID)知道是哪个控件触发的消息,而且通过(HIWORD(wParam))(控件定义的通知码)知道控件到底触发了什么消息。 下面我们看结构NMHDR: typedef struct tagNMHDR { 打开VC++ 6.0,新建Win32 Application工程M7,然后在该工程中新建C++ Source File,文件名为M7,M7的文件内容具体见例程M7。
分别用鼠标双击第一行或第二行,会弹出相应消息框。 6、MFC的消息映射 使用MFC编程时,消息发送和处理的本质和Win32相同,但是,它对消息处理进行了封装,简化了程序员编程时消息处理的复杂性,它通过消息映射机制来处理消息,程序员不必去设计和实现自己的窗口过程。 BEGIN_MASSAGE_MAP(当前类,当前类的基类) 消息的入口项 //}}AFX_MSG_MAP END_MESSAGE_MAP() 但是仅是这两项还不足以完成一条消息,要是一个消息工作,必须还有以下3个部分去协作:
(1)、利用Class Wizard实现自动添加 (2)、手动添加消息 对于能够使用Class Wizard添加的消息,尽量使用Class Wizard添加,以减少我们的工作量;对于不能使用Class Wizard添加的消息和自定义消息,需要手动添加。总体说来,MFC的消息编程对用户来说,相对比较简单,在此不再使用实例演示。 7、消息反射机制 消息反射机制为控件提供了处理通知消息的机会,这是很有用的。如果按传统的方法,由父窗口来处理这个消息,则加重了控件对象对父窗口的依赖程度,这显然违背了面向对象的原则。若由控件自己处理消息,则使得控件对象具有更大的独立性,大大方便了代码的维护和移植。 private: 在CMyEdit::CMyEdit()中,给这三个变量赋初值: { 打开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关联起来。 编译,运行程序,观察运行效果。 就写这些吧,水平有限,希望能对您有所帮助。 |