SendMessage()将指定的消息发送给指定的窗口,窗口接收到消息也有相应的行为发生。那么窗口接收到消息后的一系列行为是如何发生的?下面通过熟悉Windows的消息机制来理解消息处理背后的秘密。
成都创新互联公司专业为企业提供大理州网站建设、大理州做网站、大理州网站设计、大理州网站制作等企业网站建设、网页设计与制作、大理州企业网站模板建站服务,十余年大理州做网站经验,不只是建网站,更提供有价值的思路和整体网络服务。
01 DOS程序与Windows程序执行流程对比
Windows下的窗口应用程序都是基于消息机制的,操作系统与应用程序之间、应用程序与应用程序之间,大部分都是通过消息机制进行通信、交互的。要真正掌握Windows应用程序内部对消息的处理,必须分析实际的源代码。在编写一个基于消息的Windows应用程序前,先来比较DOS程序和Windows程序在执行时的流程。
1. DOS程序执行流程
在DOS下将编写完的程序进行执行,在执行时有较为清晰的流程。比如用C语言编写程序后,程序执行时的大致流程如图1所示。
图1 传统DOS程序执行流程
在图1中可以看出,DOS程序的流程是按照代码的顺序(这里的顺序并不是指程序控制结构中的顺序、分支和循环的意思,而是指程序运行的逻辑有明显的流程)和流程依次执行。大致步骤为:DOS程序从main()主函数开始执行(其实程序真正的入口并不是main()函数);执行的过程中按照代码编写流程依次调用各个子程序;在执行的过程中会等待用户的输入等操作;当各个子程序执行完成后,最终会返回main()主函数,执行main()主函数的return语句后,程序退出(其实程序真正的出口也并不是main()函数的return语句)。
2. Windows程序执行流程
DOS程序的执行流程比较简单,但是Windows应用程序的执行流程就比较复杂了。DOS是单任务的操作系统。在DOS中,通过输入命令,DOS操作系统会将控制权由Command.com转交给DOS程序从而执行。而Windows是多任务的操作系统,在Windows下同时会运行若干个应用程序,那么Windows就无法把控制权完全交给一个应用程序。Windows下的应用程序是如何工作的?首先看一下Windows应用程序内部的大致结构图,如图2所示。
图2 Windows应用程序执行原理图
图2可能看起来比较复杂,其实Windows应用程序的内部结构比该示意图更复杂。在实际开发Windows应用程序时,需要关注的部分主要是“主程序”和“窗口过程”两部分。但是从图2来看,主程序和窗口过程没有直接的调用关系,而在主程序和窗口过程之间有一个“系统程序模块”。“主程序”的功能是用来注册窗口类、获取消息和分发消息。而“窗口过程”中定义了需要处理的消息,“窗口过程”会根据不同的消息执行不同的动作,而不需要程序处理的消息则会交给默认的系统过程进行处理。
在“主程序”中,RegisterClassEx()函数会注册一个窗口类,窗口类中的字段中包含了“窗口过程”的地址信息,也就是把“窗口类”的信息(包括“窗口过程的地址信息”)告诉操作系统。然后“主程序”不断通过调用GetMessage()函数获取消息,再交由DispatchMessge()函数来分发消息。消息分发后并没有直接调用“窗口过程”让其处理消息,而是由系统模块查找该窗口指定的窗口类,通过窗口类再找到窗口过程的地址,最后将消息送给该窗口过程,由窗口过程处理消息。
02 一个简单的Windows应用程序
相对一个简单的DOS程序来说一个简单的Windows应用程序要很长。下面的例子中只实现了一个特别简单的Windows程序,这个程序在桌面上显示一个简单的窗口,它没有菜单栏、工具栏、状态栏,只是在窗口中输出一段简单的字符串。虽然程序如此简单,但是也要编写100行左右的代码。考虑到初学的朋友,这里将一部分一部分地逐步介绍代码中的细节,以减少代码的长度,从而方便初学者的学习。
1. Windows窗口应用程序的主函数——WinMain()
在DOS时代,或编写Windows下的命令行的程序,要使用C语言编写代码的时候都是从main()函数开始的。而在Windows下编写有窗口的程序时,要用C语言编写窗口程序就不再从main()函数开始了,取而代之的是WinMain()函数。
既然Windows应用程序的主函数是WinMain(),那么就从了解WinMain()函数的定义开始学习Windows应用程序的开发。WinMain()函数的定义如下:
- int WINAPI WinMain(
- HINSTANCE hInstance,
- HINSTANCE hPrevInstance,
- LPSTR lpCmdLine,
- int nCmdShow
- );
该函数的定义取自MSDN中,在看到WinMain()函数的定义后,很直观地会发现WinMain函数的参数比main()函数的参数变多了。从参数个数上来说,WinMain()函数接收的信息更多了。下面来看每个参数的含义。
hInstance是应用程序的实例句柄。保存在磁盘上的程序文件是静态的,当被加载到内存中时,被分配了CPU、内存等进程所需的资源后,一个静态的程序就被实例化为一个有各种执行资源的进程了。句柄的概念随上下文的不同而不同,句柄是操作某个资源的“把手”。当需要对某个实例化进程操作时,需要借助该实例句柄进行操作。这里的实例句柄是程序装入内存后的起始地址。实例句柄的值也可以通过GetModuleHandle()参数来获得(注意系统中没有GetInstanceHandle()函数,不要误以为是hInstance就会有GetInstance×××()类的函数)。
句柄这个词在开发Windows程序时是非常常见的一个词。“句柄”一词的含义随上下文的不同而所有改变。比如,磁盘上的程序文件被加载到内存中后,就创建了一个实例句柄,这个实例句柄是程序装入内存后的“起始地址”,或者说是“模块的起始地址”。
拿SendMessage()函数举例来说,句柄相当于一个操作的面板,对句柄发送的消息相当于面板上的各个开关按键,消息的附加数据,相当于给开关按键送的各种参数,这些参数根据按键的不同而不同。
hPrevInstance是同一个文件创建的上一个实例的实例句柄。这个参数是Win16平台下的遗留物,在Win32下已经不再使用了。
lpCmdLine是主函数的参数,用于在程序启动时给进程传递参数。比如在“开始”菜单的“运行”中输入“notepad c:\boot.ini”,这样就通过记事本打开了C盘下的boot.ini文件。C:\Boot.ini文件是通过WinMain()函数的lpCmdLine参数传递给notepad.exe程序的。
nCmdShow是进程显示的方式,可以是最大化显示、最小化显示,或者是隐藏等显示方式(如果是启动木马程序的话,启动方式当然要由自己进行控制)。
主函数的参数都介绍完了。编写Windows的窗口程序,需要主函数中应该完成哪些操作是下面要讨论的内容。
2. WinMain()函数中的流程
编写Windows下的窗口程序,在WinMain()主函数中主要完成的任务是注册一个窗口类,创建一个窗口并显示创建的窗口,然后不停地获取属于自己的消息并分发给自己的窗口过程,直到收到WM_QUIT消息后退出消息循环结束进程。这是主函数中程序的执行脉络,程序中将注册窗口类、创建窗口的操作封装为自定义函数。
代码如下:
- int WINAPI WinMain(
- HINSTANCE hInstance,
- HINSTANCE hPrevInstance,
- LPSTR lpCmdLine,
- int nCmdShow)
- {
- MSG Msg;
- BOOL bRet;
- // 注册窗口类
- MyRegisterClass(hInstance);
- // 创建窗口并显示窗口
- if ( !InitInstance(hInstance, SW_SHOWNORMAL) )
- {
- return FALSE;
- }
- // 消息循环
- // 获取属于自己的消息并进行分发
- while( (bRet = GetMessage(&Msg, NULL, 0, 0)) != 0 )
- {
- if ( bRet == -1 )
- {
- // handle the error and possibly exit
- break;
- }
- else
- {
- TranslateMessage(&Msg);
- DispatchMessage(&Msg);
- }
- }
- return Msg.wParam;
- }
在代码中,MyRegisterClass()和InitInstance()是两个自定义的函数,分别用来注册窗口类,创建窗口并显示更新创建的窗口。后面的消息循环部分用来获得消息并进行消息分发。它的流程如图2所示的“主程序”部分。
代码中主要是3个函数,分别是GetMessage()、TranslateMessage()和DispatchMessage()。这3个函数是Windows提供的API函数。GetMessage()的定义如下:
- BOOL GetMessage(
- LPMSG lpMsg,
- HWND hWnd,
- UINT wMsgFilterMin,
- UINT wMsgFilterMax
- );
该函数用来获取属于自己的消息,并填充MSG结构体。有一个类似于GetMessage()的函数是PeekMessage(),它可以判断消息队列中是否有消息,如果没有消息,可以主动让出CPU时间给其他进程。关于PeekMessage()函数的使用,请参考MSDN:
- BOOL TranslateMessage(CONST MSG *lpMsg);
该函数是用来处理键盘消息的。它将虚拟码消息转换为字符消息,也就是将WM_KEYDOWN消息和WM_KEYUP消息转换为WM_CHAR消息,将WM_SYSKEYDOWN消息和WM_SYSKEYUP消息转换为WM_SYSCHAR消息:
- LRESULT DispatchMessage(CONST MSG *lpmsg);
该函数是将消息分发到窗口过程中。
3. 注册窗口类的自定义函数
在WinMain()函数中,首先调用了MyRegisterClass()这个自定义函数,需要传递进程的实例句柄hInstance作为参数。该函数完成窗口类的注册,分为两步:第一步是填充WNDCLASSEX结构体,第二步是调用RegisterClassEx()函数进行注册。该函数相对简单,但是,该函数中稍微复杂的是WNDCLASSEX结构体的成员较多。
代码如下:
- ATOM MyRegisterClass(HINSTANCE hInstance)
- {
- WNDCLASSEX WndCls;
- // 填充结构体为 0
- ZeroMemory(&WndCls, sizeof(WNDCLASSEX));
- // cbSize 是结构体大小
- WndCls.cbSize = sizeof(WNDCLASSEX);
- // lpfnWndProc 是窗口过程地址
- WndCls.lpfnWndProc = WindowProc;
- // hInstance 是实例句柄
- WndCls.hInstance = hInstance;
- // lpszClassName 是窗口类类名
- WndCls.lpszClassName = CLASSNAME;
- // style 是窗口类风格
- WndCls.style = CS_HREDRAW | CS_VREDRAW;
- // hbrBackground 是窗口类背景色
- WndCls.hbrBackground = (HBRUSH)COLOR_WINDOWFRAME + 1;
- // hCursor 是鼠标句柄
- WndCls.hCursor = LoadCursor(NULL, IDC_ARROW);
- // hIcon 是图标句柄
- WndCls.hIcon = LoadIcon(NULL, IDI_QUESTION);
- // 其他
- WndCls.cbClsExtra = 0;
- WndCls.cbWndExtra = 0;
- return RegisterClassEx(&WndCls);
- }
在代码中,WNDCLASSEX结构体的成员都介绍了。WNDCLASSEX中最重要的字段是lpfnWndProc,它将保存的是窗口过程的地址。窗口过程是对各种消息进程处理的“汇集地”,也是编写Windows应用程序的重点部分。代码中的函数都比较简单,主要涉及LoadCursor()、LoadIcon()和RegisterClassEx()这3个函数。由于这3个函数使用简单,通过代码就可以进行理解,这里不做过多介绍。
注册窗口类(提到窗口类,你是否想到了FindWindow()函数的第一个参数呢?)的重点是在后面的代码中可以根据该窗口类创建该种类型的窗口。代码中,在定义窗口类时指定了背景色、鼠标指针、窗口图标等,那么使用该窗口类创建的窗口都具有相同的窗口类型。
4. 创建主窗口并显示更新
注册窗口类后,根据该窗口类创建具体的主窗口并显示和更新窗口。
代码如下:
- BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
- {
- HWND hWnd = NULL;
- // 创建窗口
- hWnd = CreateWindowEx(WS_EX_CLIENTEDGE,
- CLASSNAME,
- "MyFirstWindow",
- WS_OVERLAPPEDWINDOW,
- CW_USEDEFAULT, CW_USEDEFAULT,
- CW_USEDEFAULT, CW_USEDEFAULT,
- NULL, NULL, hInstance, NULL);
- if ( NULL == hWnd )
- {
- return FALSE;
- }
- // 显示窗口
- ShowWindow(hWnd, nCmdShow);
- // 更新窗口
- UpdateWindow(hWnd);
- return TRUE;
- }
在调用该函数时,需要给该函数传递实例句柄和窗口显示方式两个参数。这两个参数的第1个参数通过WinMain()函数的参数hInstance指定,第2个参数可以通过WinMain()函数的第3个参数指定,也可以进行自定义指定。程序中的调用代码如下:
- InitInstance(hInstance, SW_SHOWNORMAL);
在创建主窗口时调用了CreateWindowEx()函数,先来看看它的函数原型:
- HWND CreateWindowEx(
- DWORD dwExStyle,
- LPCTSTR lpClassName,
- LPCTSTR lpWindowName,
- DWORD dwStyle,
- int x,
- int y,
- int nWidth,
- int nHeight,
- HWND hWndParent,
- HMENU hMenu,
- HINSTANCE hInstance,
- LPVOID lpParam
- );
CreateWindowEx()中的第2个参数是lpClassName,由注释可以知道是已经注册的类名。这个已经注册的类名就是WNDCLASSEX结构体的lpszClassName字段。
5. 处理消息的窗口过程
按照如图2所示的流程,WinMain()主函数的部分已经都实现完成了。接下来看程序中关键的部分——窗口过程。从WinMain()主函数中看出,在WinMain()主函数中没有任何地方直接调用窗口过程,只是在注册窗口类时指定了窗口过程的地址。那么窗口类是由谁进行调用的呢?答案是由操作系统进行调用的。原因有二,首先窗口过程的地址是由系统维护的,注册窗口类时是将“窗口过程的地址”向操作系统进行注册。其次是除了应用程序本身会调用自己的窗口过程外,其他应用程序也会调用自己的窗口过程,比如前面的例子中调用SendMessage()函数发送消息后,需要系统调用目标程序的窗口过程来完成相应的动作。如果窗口过程由自己调用,那么窗口就要自己维护窗口类的信息,进程间消息的通信会非常繁琐,也会无形中增加系统的开销。
窗口过程的代码如下:
- LRESULT CALLBACK WindowProc(
- HWND hwnd,
- UINT uMsg,
- WPARAM wParam,
- LPARAM lParam)
- {
- PAINTSTRUCT ps;
- HDC hDC;
- RECT rt;
- char *pszDrawText = "Hello Windows Program.";
- switch (uMsg)
- {
- case WM_PAINT:
- {
- hDC = BeginPaint(hwnd, &ps);
- GetClientRect(hwnd, &rt);
- DrawTextA(hDC,
- pszDrawText, strlen(pszDrawText),&rt,
- DT_CENTER | DT_VCENTER | DT_SINGLELINE);
- EndPaint(hwnd, &ps);
- break;
- }
- case WM_CLOSE:
- {
- if ( IDYES == MessageBox(hwnd,
- "是否退出程序", "MyFirstWin", MB_YESNO) )
- {
- DestroyWindow(hwnd);
- PostQuitMessage(0);
- }
- break;
- }
- default:
- {
- return DefWindowProc(hwnd, uMsg, wParam, lParam);
- }
- }
- return 0;
- }
在WinMain()函数中,通过调用RegisterClassEx()函数进行了窗口类的注册,通过调用CreateWindowEx()函数创建了窗口,并且GetMessage()函数不停地获取消息,但是在主函数中没有对被创建的窗口做任何处理。那是因为真正对窗口行为的处理全部放在了窗口过程中。当WinMain()函数中的消息循环得到消息以后,通过调用DispatchMessage()函数将消息派发(实际不是由DispatchMessage()函数直接派发)给了窗口过程,从而由窗口过程对消息进行处理。
窗口过程的定义是按照MSDN上给出的形式进行定义的,MSDN上的定义形式如下:
- LRESULT CALLBACK WindowProc(
- HWND hwnd,
- UINT uMsg,
- WPARAM wParam,
- LPARAM lParam
- );
WindowProc是窗口过程的函数名,这个函数名可以随意改变,但是该窗口过程的函数名必须与WNDCLASSEX结构体中lpfnWndProc的成员变量的值一致。函数的第1个参数hwnd是窗口的句柄,第2个参数uMsg是消息值,第3个和第4个参数是对于消息值的附加参数。这4个参数的类型与SendMessage()函数的参数相对应。
上面WindowProc()窗口过程中只对两个消息进行了处理,分别是WM_PAINT和WM_CLOSE。这里为了演示因此只简单处理了两个消息。Windows中有上千种消息,那么多的消息不可能全部都由程序员自己去处理,程序员只处理一些程序中需要的消息,其余的消息就交给了DefWindowProc()函数进行处理。DefWindowProc()函数实际上是将消息传递给了操作系统,由操作系统来处理程序中没有处理的消息。比如,在调用CreateWindow()函数时,系统会发送消息WM_CREATE给窗口过程,但是这个消息可能对程序的功能并不需要进行特殊的处理,因此直接交由DefWindowProc()函数让系统进行处理。
DefWindowProc()函数的定义如下:
- LRESULT DefWindowProc(
- HWND hWnd,
- UINT Msg,
- WPARAM wParam,
- LPARAM lParam
- );
该函数的4个参数跟窗口过程的参数相同,只要将窗口过程的参数依次传递给DefWindowProc()函数就可以完成该函数的调用。在switch分支结构中的default位置直接调用DefWindowProc()函数就可以了。
WM_CLOSE消息是关闭窗口时发出的消息,在这个消息中需要调用DestoryWindow()函数来销毁窗口,并且调用PostQuitMessage()来退出消息循环,使程序退出。对于WM_PAINT消息,这里不进行介绍,涉及的几个API函数可以参考MSDN进行了解。
有的资料在介绍消息循环时会给出一个建议,就是把需要经常处理的消息放到程序靠上的位置,而将不经常处理的消息放到程序靠下的位置,从而提高程序的效率。其实,在窗口过程中往往会使用switch结构对消息进行判断(如果使用if和else结构进行消息的判断,那么常用的消息是要放到前面),而switch结构在编译器进行编译后会进行优化处理,从而大大提高程序的运行效率。
分享名称:网络安全编程:Windows消息机制实例
本文网址:http://www.shufengxianlan.com/qtweb/news15/301615.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联