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()函数的定义如下:
该函数的定义取自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消息后退出消息循环结束过程。这是主函数中程序的执行脉络,将注册窗口类和创建窗口的操作包装成自定义函数。
代码如下:
在代码中,MyRegisterClass()和InitInstance()是两个自定义函数,分别用于注册窗口类别、创建窗口和显示更新窗口。以下信息循环部分用于获取和分发信息。其流程如图2所示“主程序”部分。
代码中主要有三个函数,即GetMessage()、TranslateMessage()和DispatchMessage()。这三个函数是Windows提供的API函数。GetMessage()定义如下:
该函数用于获取自己的信息并填写MSG结构体。有一个类似。GetMessage()函数是PeekMessage(),它可以判断消息队列中是否有消息,如果没有消息,可以主动放弃CPU其他过程的时间。PeekMessage()函数的使用,请参考MSDN:
该函数用于处理键盘信息。它将虚拟码信息转换为字符信息,即WM_KEYDOWN消息和WM_KEYUP消息转换为WM_CHAR消息,将WM_SYSKEYDOWN消息和WM_SYSKEYUP消息转换为WM_SYSCHAR消息:
该函数将信息分发到窗口。
3. 注册窗口类的自定义函数
在WinMain()函数中首先调用MyRegisterClass()这个自定义函数,需要传递进程的实例句柄hInstance作为参数。完成窗口类注册的函数分为两个步骤:第一步是填充WNDCLASSEX第二步是调用结构体RegisterClassEx()注册函数。函数比较简单,但是函数有点复杂WNDCLASSEX结构成员较多。
代码如下:
在代码中,WNDCLASSEX介绍了结构成员。WNDCLASSEX最重要的字段是lpfnWndProc,它将保存窗口过程的地址。窗口过程处理各种新闻过程“汇集地”,也是编写Windows应用程序的关键部分。代码中的函数相对简单,主要涉及LoadCursor()、LoadIcon()和RegisterClassEx()这三个函数。由于这三个函数使用简单,可以通过代码理解,这里不做太多介绍。
注册窗口类(说到窗口类,你有没有想过?FindWindow()函数的第一个参数是什么?)重点是根据后面的代码创建这种类型的窗口。在代码中,在定义窗口类别时,指定了背景颜色、鼠标指针、窗口图标等,因此使用该窗口类别创建的窗口具有相同的窗口类型。
4.创建主窗并显示更新
注册窗口类后,根据窗口类创建具体的主窗,并显示和更新窗口。
代码如下:
调用此函数时,需要将实例句柄和窗口显示两个参数传递给该函数。这两个参数的第一个参数通过WinMain()函数参数hInstance第二个参数可以通过指定WinMain()函数的第三个参数也可以自定义。程序中的调用代码如下:
在创建主窗时调用CreateWindowEx()函数,先看它的函数原型:
CreateWindowEx()第二个参数是lpClassName,注释可以知道是注册类名。这个注册类名是WNDCLASSEX结构体的lpszClassName字段。
5. 处理新闻的窗口过程
按照图2所示的流程,WinMain()主函数的部分已经完成。接下来,看看程序的关键部分——窗口过程WinMain()主函数中可以看到,WinMain()主函数中没有直接调用窗口过程的地方,但在注册窗口类别时指定了窗口过程地址。那么谁调用了窗口类别呢?答案是由操作系统调用的。有两个原因。首先,窗口过程地址由系统维护。注册窗口类别时“窗口过程地址”注册到操作系统。其次,除了应用程序本身会调用自己的窗口过程外,其他应用程序也会调用自己的窗口过程,如前面的例子SendMessage()函数发送消息后,系统需要调用目标程序的窗口过程来完成相应的动作。如果窗口过程是自己调用的,窗口需要自己维护窗口信息。过程中信息的通信会很繁琐,无形中会增加系统的成本。
窗口过程代码如下:
在WinMain()通过调用函数RegisterClassEx()通过调用窗口类注册函数CreateWindowEx()函数创建窗口,GetMessage()函数不断获取信息,但在主函数中没有对创建的窗口进行任何处理。那是因为窗口行为的真正处理都放在了窗口的过程中。WinMain()函数中的消息循环得到消息后,通过调用DispatchMessage()函数发送消息(实际上不是由于DispatchMessage()函数直接发送到窗口过程中,从而通过窗口过程处理信息。
窗口过程的定义是基于MSDN定义上给出的形式,MSDN上述定义形式如下:
WindowProc是窗口过程的函数名,可以随意更改,但必须与窗口过程的函数名相匹配WNDCLASSEX结构体中lpfnWndProc成员变量值一致。函数的第一个参数hwnd是窗口的句柄,第二个参数uMsg是新闻值,第三和第四个参数是新闻值的附加参数。这四个参数的类型和SendMessage()函数参数对应。
上面WindowProc()窗口过程中只处理了两个消息,即WM_PAINT和WM_CLOSE。为了演示,这里只简单处理了两个消息。Windows有成千上万的新闻,所以很多新闻不能由程序员自己处理,程序员只处理程序中需要的一些新闻,其余的新闻交给DefWindowProc()处理函数。DefWindowProc()函数实际上是将信息传递给操作系统,由操作系统处理程序中未处理的信息。例如,在调用中CreateWindow()函数时,系统会发送消息WM_CREATE给窗口过程,但这个消息可能不需要特殊处理程序的功能,所以直接交给窗口DefWindowProc()系统处理函数。
DefWindowProc()函数的定义如下:
该函数的4个参数跟窗口过程的参数相同,只要将窗口过程的参数依次传递给DefWindowProc()函数可以调用。switch在分支结构中default直接调用位置DefWindowProc()函数就够了。
WM_CLOSE消息是关闭窗口时发出的消息,需要在此消息中调用DestoryWindow()销毁窗口并调用函数PostQuitMessage()退出消息循环,退出程序。WM_PAINT这里不介绍新闻,涉及几个API可参考函数MSDN进行了解。
在介绍新闻周期时,一些信息会给出一个建议,即将需要经常处理的信息放在程序的顶部,而不经常处理的信息放在程序的底部,以提高程序的效率。事实上,它经常用于窗口switch结构判断消息(如果使用)if和else结构判断新闻,所以常用的新闻是放在前面),switch编译器编译后会优化结构,从而大大提高程序的运行效率。