我是荔园微风,作为一名在IT界整整25年的老兵,今天说说Windows程序的运行机制。经常被问到MFC到底是一个什么技术,为了解释这个我之前还写过帖子,但是很多人还是不理解。其实这没什么,我在学生时代也被这个问题困绕过。而且那个时间学习资料没有那么丰富,网上也没有什么资料,周围也没有懂的人,那个时候理解MFC更困难。甚至在我看来,理解这个比理解人工神经网络更难。
我认为造成这种现象的根本原因就是没有搞清楚Windows程序的运行机制,因为不理解Windows程序的运行机制,所以给理解MFC带来了很大的困难。我决定带所有微软开发技术的初学者一起攻破这个问题,但是一篇文章肯定是讲不清楚的,我们要分好几章来说。需要你有足够的耐心,一起来吧。我们这次来搞清楚什么是Windows程序的消息。
程序入口?难道是以前C语言中说到的main()??你猜的有点接近。我们今天要说的就是WinMain()。所有了解过 Windows编程的程序员都知道在应用程序中有一个函数WinMain()。当Windows操作系统启动一个程序时,它调用的就是该程序的WinMain()函数。WinMain()是Windows程序的入口函数,与以前turbe c程序的入口点函数main()的作用相同,当WinMain()函数结束或返回时,Windows应用程序结束。
下面我们来创建一个窗口,并在该窗口中响应键盘及鼠标消息,程序实现的步骤为:先写出WinMain()函数的定义,然后创建一个窗口,再进行消息循环处理消息,最后编写窗口过程函数。为什么最后要编写窗口过程函数,这个我们等下再说。
首先,我们来定义WinMain(),因为只有先确定程序的入口,后面的事才有做的意义。定义时先要进行原型的声明。
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow
);
我学生时代当年买了一本VC的书,名字叫什么都忘了,那是2000年,当时那本书上完全不告诉我这四个变量是什么意思就直接开始写程序,我那个痛苦啊。真的是太过份了。那种无助感到现在都记得。我那个时候是个老实的学生,完全不知道什么叫愤怒。我当时看不懂这些变量我反而还怪自己领悟能力太差。现在回想起来真是.......如果我发明一种编程语言,我一定编他个十八个变量,然后再叫那本VC书的作者好好的来认认我写的这十八个变量是什么意思。你不说明一下,别人怎么知道!
好了,言归正传。我肯定不会像那本VC书的作者一样不负责任,我来慢慢给大家解释这些变更的意思。上面WinMain()函数的这些变量都是在系统调用WinMain()函数时,传递给应用程序的。
第一行先看WinMain函数前的修饰符WINAPI。大家可以利用我上一篇文章中提到的“转到定义”功能查看WINAPI的定义,WINAPI就是_stdcall。
第二行变量 hInstance 表示该程序当前运行的实例的句柄,句柄就是一个数值或者叫标识号,我在之前的文章中提过。当程序在Windows下运行时,它唯一标识运行中的实例。只有运行中的程序实例,才有实例句柄,如果没有装入内存,那不好意思,没有。一个应用程序可以运行多个实例,每运行一个实例,系统都会给该实例分配一个句柄值,并通过 hInstance变量传递给 WinMain()函数。注意注意,这和以前C语言编程中自己定义一个变量完全不同,C语言中是自己定义,并给予于赋值。而上面这个hInstance不是你自己赋值,而操作系统传过来的,传过来的数值具体是什么你暂时不用操心,先把下面的内容学好。唉,我当年就是太操心这个值是什么,结果本末倒置。
第三行变量 hPrevInstance 表示当前实例的前一个实例的句柄。我当年费尽心思去研究这个,结果后来一位学长告诉我,在Win32环境下这个变量是NULL,即在Win32环境下,这个参数不再起作用。我怒了,不起作用的你放第二行?这......
第四行变量 lpCmdLine是一个以空终止的字符串,指定传递给应用程序的命令行参数。我们就以windows文件系统举例:在C盘下有一个1.BMP文件,当我们用鼠标双击这个文件时将启动画图板程序(mspaint.exe),此时系统会将 C:\1.BMP作为命令行参数传递给画图板程序的 WinMain()函数,画图板程序在得到这个文件的全路径名后,就在窗口中显示该文件的内容。
第五行变量 nCmdShow指定程序的窗口应该如何显示,例如最大化、最小化、隐藏等。这个参数的值由该程序的调用者所指定,应用程序通常不需要去理会这个参数的值。我又崩溃了,又是一个我研究了好久的变量。
大家是不是有点感觉了,也就是windows操作系统里所有的程序,注意,是所有的应用程序都有自己的WinMain()函数,当你启动这个程序时,这个程序的WinMain()函数就要开始工作了。怎么工作呢,上面WinMain()函数就要接收外面传来的值,其中有一些值还是操作系统根据用户的操作来给予的。
那有的时候我们要模拟操作系统传递参数给应用程序的效果,那怎么办?要在Visual Studio 开发环境中向应用程序传递参数,可以单击菜单【项目】→【WinMain属性】,在左侧配置属性窗口中选择“调试”,在右侧窗口的“命令参数”编辑框中输入你想传递给应用程序的参数。
2.4.2窗口的创建
创建一个完整的窗口,需要经过下面几个操作步骤:
0
设计一个窗口类;
2注册窗口类;
③创建窗口;
4
显示及更新窗口。
下面将分别介绍创建窗口的过程。完整的例程请参见例子代码ch01目录下WinMain。
1.设计一个窗口类
一个完整的窗口具有许多特征,包括光标(鼠标进入该窗口时的形状)、图标、背景色等。窗口的创建过程类似于汽车的制造过程。我们在生产一个型号的汽车之前,首先要对该型号的汽车进行设计,在图纸上画出汽车的结构图,设计各个零部件,同时还要给该型号的汽车取一个响亮的名字,例如“奥迪A6”。在完成设计后,就可以按照“奥迪A6”
这个型号生产汽车了。
类似地,在创建一个窗口前,也必须对该类型的窗口进行设计,指定窗口的特征。当然,在我们设计一个窗口时,不像汽车的设计这么复杂,因为Windows已经为我们定义好了一个窗口所应具有的基本属性,我们只需要像考试时做填空题一样,将需要我们填充的部分填写完整,一种窗口就设计好了。在Windows中,要达到做填空题的效果,只能通过结构体来完成,窗口的特征就是由WNDCLASS结构体来定义的。WNDCLASS结构体的定义如下(请读者自行参看MSDN):
typedef struct WNDCLASS{
UINT
style;
WNDPROC
lpfnWndProc;
int
cbClsExtra;
13
cbWndExtra;
int
HANDLE
hInstance;
HICON
hIcon;
HCURSOR
hCursor;
HBRUSH
hbrBackground;
LPCTSTR
1pszMenuName;
LPCTSTR
lpszClassName;
}WNDCLASS;
下面对该结构体的成员变量做一个说明。
第一个成员变量style指定这一类型窗口的样式,常用的样式如下:
CS_HREDRAW
当窗口水平方向上的宽度发生变化时,将重新绘制整个窗口。当窗口发生重绘时,口中的文字和图形将被擦除。如果没有指定这一样式,那么在水平方向上调整窗口宽度时将不会重绘窗口。
CS_VREDRAW
当窗口垂直方向上的高度发生变化时,将重新绘制整个窗口。如果没有指定这一样式,那么在垂直方向上调整窗口高度时,将不会重绘窗口。
CS_NOCLOSE
禁用系统菜单的Close命令,这将导致窗口没有关闭按钮。
CS DBLCLKS
当用户在窗口中双击鼠标时,向窗口过程发送鼠标双击消息。
style成员其他值MSDN。
否
知识点在WinUser.h中,以CS_开头的类样式(Class Style)标识符被定义为16位的常量,这些常量都只有某1位为1。在Visual Studio开发环境中,利用“转到定义”
功能,可以看到CS_VREDRAW=0x0001、CS_HREDRAW=0x0002、CS_DBLCLKS=0x0008和CS_NOCLOSE=0x0200,读者将这些16进制数转换为2进制数,就可以发现它们都只有1位为1,并且为1的位各不相同。用这种方式定义的标识符称为“位标志”,我们可以使用位运算操作符来组合使用这些样式。例如,要让窗口在水平和垂直尺寸发生变化时发生重绘,我们可以使用位或(|)操作符将CS_HREDRAW和CS_VREDRAW 组合起来,如 style=CS_HREDRAW |CS_VREDRAW。假如有一个变量具有多个样式,而我们并不清楚该变量都有哪些样式,现在我们想要去掉该变量具有的某个样式,那么可以先对该样式标识符进行取反(~)操作,然后再和这个变量进行与(&)操作即可实现。例如,要去掉先前的style变量所具有的CS_VREDRAW样式,可以编写代码:style-style&~CS_VREDRAW。
在Windows程序中,经常会用到这种位标志标识符,后面我们在创建窗口时用到的窗口样式,也属于位标志标识符。
第二个成员变量lpfnWndProc是一个函数指针,指向窗口过程函数,窗口过程函数是一个回调函数。回调函数不是由该函数的实现方直接调用的,而是在特定的事件或条件发14
生时由另外一方调用的,用于对该事件或条件进行响应。回调函数实现的机制如下。
(1)定义一个回调函数。
(2)提供函数实现的一方在初始化的时候,将回调函数的函数指针注册给调用者。
(3)当特定的事件或条件发生的时候,调用者使用函数指针调用回调函数对事件进行处理。
针对Windows的消息处理机制,窗口过程函数被调用的过程如下。
(1)在设计窗口类的时候,将窗口过程函数的地址赋值给lpfnWndProc成员变量。
(2)调用RegisterClass(&wndclass)注册窗口类,那么系统就有了我们所编写的窗口过程函数的地址。
(3)当应用程序接收到某一窗口的消息时,调用 DispatchMessage(&msg)将消息回传给系统。系统则利用先前注册窗口类时得到的函数指针,调用窗口过程函数对消息进行处理。
一个Windows程序可以包含多个窗口过程函数,一个窗口过程总是与某一个特定的窗口类相关联(通过WNDCLASS结构体中的lpfnWndProc成员变量指定),基于该窗口类创建的窗口使用同一个窗口过程。
lpfnWndProc 成员变量的类型是 WNDPROC,我们在 Visual Studio开发环境中使用“转到定义”功能,可以看到WNDPROC的定义:
typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);
在这里又出现了两个新的数据类型LRESULT和CALLBACK,再次使用“转到定义”,可以看到它们实际上是long和_stdcall。
从WNDPROC的定义可以知道,WNDPROC实际上是函数指针类型。
注意:WNDPROC被定义为指向窗口过程函数的指针类型,窗口过程函数的格式必须与WNDPROC相同。
知识点在函数调用过程中,会使用栈。_stdcall与_cdecl是两种不同的函数调用约定,定义了函数参数入栈的顺序,由调用函数还是被调用函数将参数弹出栈,以及产生函数修饰名的方法。关于这两个调用约定的详细信息,读者可参看MSDN。对于参数个数可变的函数,例如printf,使用的是_cdecl调用约定,Win32的API函数都遵循stdcall调用约定。在 Visual Studio 开发环境中,默认的编译选项是_cdecl,对于那些需要_stdcall调用约定的函数,在声明时必须显式地加上_stdcall。在 Windows程序中,回调函数必须遵循_stdcall调用约定,所以我们在声明回调函数时要使用CALLBACK。
使用 CALLBACK 而不是_stdcall的原因是为了告诉我们这是一个回调函数。
WNDCLASS结构体第三个成员变量 cbClsExtra: Windows为系统中的每一个窗口类管理一个WNDCLASS结构。在应用程序注册一个窗口类时,它可以让 Windows系统为WNDCLASS结构分配和追加一定字节数的附加内存空间,这部分内存空间称为类附加内存,由属于这种窗口类的所有窗口所共享,类附加内存空间用于存储类的附加信息。
15
Windows系统把这部分内存初始化为0。该参数没有使用,直接设置为0即可。
第四个成员变量cbWndExtra:Windows系统为每一个窗口管理一个内部数据结构注册一个窗口类时,应用程序能够指定一定字节数的附加内存空间,称为窗口附加内存在创建这类窗口时,Windows系统就为窗口的结构分配和追加指定数目的窗口附加内存空间,应用程序可用这部分内存存储窗口特有的数据。Windows系统把这部分内存初始化为0。如果应用程序用WNDCLASS结构注册对话框(用资源文件中的CLASS伪指令创建),则必须给DLGWINDOWEXTRA设置这个成员。该参数没有使用,直接设置为0即可。
第五个成员变量hInstance指定包含窗口过程的程序的实例句柄。
第六个成员变量hIcon指定窗口类的图标句柄。这个成员变量必须是一个图标资源的句柄,如果这个成员为NULL,那么系统将提供一个默认的图标。
在为hIcon变量赋值时,可以调用LoadIcon函数来加载一个图标资源,返回系统分配给该图标的句柄。该函数的原型声明如下所示:
HICON LoadIcon( HINSTANCE hInstance, LPCTSTR lpIconName)Loadlcon函数不仅可以加载 Windows系统提供的标准图标到内存中,还可以加载由用户自己制作的图标资源到内存中,并返回系统分配给该图标的句柄,请参看MSDN关于LoadIcon的解释。但要注意的是,如果加载的是系统的标准图标,那么第一个参数必须为NULL。
LoadIcon的第二个参数是LPCTSTR类型,利用“转到定义”功能将会发现它实际被定义成CONST CHAR*,即指向字符常量的指针,而图标的ID是一个整数。对于这种情况我们需要用MAKEINTRESOURCE宏把资源ID标识符转换为需要的LPCTSTR类型。
知识点在VC+开发中,对于自定义的菜单、图标、光标、对话框等资源,都保存在资源脚本(通常扩展名为.rc)文件中。在Visual Studio开发环境中,要访问资源文件,可以在解决方案资源管理中找到你的项目,在项目下有一个“资源文件”文件夹,你将看到以树状列表形式显示的资源项目。在任何一种资源上双击鼠标左键,将打开资源编辑器。在资源编辑器中,你可以以“所见即所得”的方式对资源进行编辑。资源文件本身是文本文件格式,如果你了解资源文件的编写格式,那么也可以直接使用文本编辑器对资源进行编辑。
在VC++中,资源是通过标识符(ID)来标识的,同一个ID可以标识多个不同的资源。资源的ID实质上是一个整数,在“resource.h”中定义为一个宏。我们在为资源指定ID的时候,应该养成一个良好的习惯,即在“ID”后附加特定资源英文名称的首字母,例如,菜单资源为 IDM_XXX(M表示Menu),图标资源为 IDI_XXX(I表示 Icon),按钮资源为IDB_XXX(B表示 Button)。采用这种命名方式,我们在程序中使用资源ID时,可以一目了然。
WNDCLASS结构体第七个成员变量 hCursor 指定窗口类的光标句柄。这个成员变量必须是一个光标资源的句柄,如果这个成员为NULL,那么无论何时鼠标进入应用程序窗口中,应用程序都必须明确地设置光标的形状。
在为hCursor变量赋值时,可以调用LoadCursor函数来加载一个光标资源,返回系统
分配给该光标的句柄。该函数的原型声明如下所示:
HCURSOR LoadCursor(HINSTANCE hInstance, LPCTSTR lpCursorName);
LoadCursor 函数除了加载的是光标外,其使用方法与LoadIcon函数一样。
第八个成员变量 hbrBackground指定窗口类的背景画刷句柄。当窗口发生重绘时,系统使用这里指定的画刷来擦除窗口的背景。我们既可以为 hbrBackground成员指定一个画刷的句柄,也可以为其指定一个标准的系统颜色值。关于hbrBackground成员的详细说明,请参看MSDN。
我们可以调用 GetStockObject 函数来得到系统的标准画刷。GetStockObject函数的原型声明如下所示:
HGDIOBJ GetStockObject( int fnObject);
参数fnObject指定要获取的对象的类型,关于该参数的取值,请参看MSDN。
GetStockObject函数不仅可以用于获取画刷的句柄,还可以用于获取画笔、字体和调色板的句柄。由于GetStockObject函数可以返回多种资源对象的句柄,在实际调用该函数前无法确定它返回哪一种资源对象的句柄,因此它的返回值的类型定义为HGDIOBJ,在实际使用时,需要进行类型转换。例如,我们要为hbrBackground成员指定一个黑色画刷的句柄,可以调用如下:
wndclass. hbrBackground=(HBRUSH)GetStockObject(BLACK_BRUSH);
当窗口发生重绘时,系统会使用这里指定的黑色画刷擦除窗口的背景。
第九个成员变量lpszMenuName是一个以空终止的字符串,指定菜单资源的名字。如果你使用菜单资源的ID号,那么需要用MAKEINTRESOURCE宏来进行转换。如果将lpszMenuName成员设置为NULL,那么基于这个窗口类创建的窗口将没有默认的菜单。
要注意,菜单并不是一个窗口,很多初学者都误以为菜单是一个窗口。
第十个成员变量lpszClassName是一个以空终止的字符串,指定窗口类的名字。这和汽车的设计类似,设计一款新型号的汽车,需要给该型号的汽车取一个名字。同样的,设计了一种新类型的窗口,也要为该类型的窗口取个名字,这里我们将这种类型窗口命名为“sunxin2019”,后面将看到如何使用这个名称。
2.注册窗口类
在设计完汽车后,需要报经国家有关部门审批,批准后才能生产这种类型的汽车。同样地,在设计完窗口类(WNDCLASS)后,需要调用RegisterClass函数对其进行注册,在注册成功后,才可以创建该类型的窗口。注册函数的原型声明如下:
ATOM RegisterClass(CONST WNDCLASS *lpWndClass);
该函数只有一个参数,即上一步骤中所设计的窗口类对象的指针。
3.创建窗口
在设计好窗口类并且将其成功注册之后,就可以用CreateWindow函数产生这种类型17
的窗口了。CreateWindow函数的原型声明如下:
HWND CreateWindow(
LPCTSTR lpClassName,
// pointer to registered class name
LPCTSTR lpWindowName,
// pointer to window name
DWORD dwStyle,
// window style
// horizontal position of window
int x,
int y,
/ vertical position of window
int nwidth,
// window width
int nHeight,
// window height
HWND hWndParent,
// handle to parent or owner window
HMENU hMenu,
// handle to menu or child-window identifier
HANDLE hInstance,
// handle to application instance
LPVOID lpParam
// pointer to window-creation data
参数 lpClassName指定窗口类的名称,即我们在步骤1设计一个窗口类中为 WNDCLASS的lpszClassName成员指定的名称,在这里应该设置为“sunxin2019”,表示要产生“sunxin2019”这一类型的窗口。产生窗口的过程是由操作系统完成的,如果在调用CreateWindow 函数之前,没有用 RegisterClass函数注册过名称为“sunxin2019”的窗口类型,操作系统将无法得知这一类型窗口的相关信息,从而导致创建窗口失败。
参数lpWindowName指定窗口的名字。如果窗口样式指定了标题栏,那么这里指定的窗口名字将显示在标题栏上。
参数dwStyle指定创建的窗口的样式。就好像同一型号的汽车可以有不同的颜色一样,同一型号的窗口也可以有不同的外观样式。要注意区分WNDCLASS中的style成员与CreateWindow 函数的dwStyle参数,前者是指定窗口类的样式,基于该窗口类创建的窗口都具有这些样式,后者是指定某个具体的窗口的样式。
在这里,我们可以给创建的窗口指定WS_OVERLAPPEDWINDOW 这一类型,该类型的定义为:
#define WS OVERLAPPEDWINDOW (WS_OVERLAPPEDWS_CAPTION
WS SYSMENU
WS THICKFRAME
WS MINIMIZEBOX
WS MAXIMIZEBOX)
可以看到,WS_OVERLAPPEDWINDOW是多种窗口类型的组合,其原理和前面知识点所讲的内容是一致的。下面是这几种常用窗口类型的说明。
■WS_OVERLAPPED:产生一个层叠的窗口,一个层叠的窗口有一个标题栏和一个边框。
WS_CAPTION:创建一个有标题栏的窗口。
■WS_SYSMENU:创建一个在标题栏上带有系统菜单的窗口,要和 WS_CAPTION类型一起使用。
18