创建一个完整的窗口,需要经过下面几个操作步骤:
设计一个窗口类;
注册窗口类;
创建窗口;
显示及更新窗口。
下面的四个小分节将分别介绍创建窗口的过程。完整的例程请参见光盘中的例子代码Chapter1目录下WinMain。
一个完整的窗口具有许多特征,包括光标(鼠标进入该窗口时的形状)、图标、背景色等。窗口的创建过程类似于汽车的制造过程。我们在生产一个型号的汽车之前,首先要对该型号的汽车进行设计,在图纸上画出汽车的结构图,设计各个零部件,同时还要给该型号的汽车取一个响亮的名字,例如“奥迪A6”。在完成设计后,就可以按照“奥迪A6”这个型号生产汽车了。
类似地,在创建一个窗口前,也必须对该类型的窗口进行设计,指定窗口的特征。当然,在我们设计一个窗口时,不像汽车的设计这么复杂,因为Windows已经为我们定义好了一个窗口所应具有的基本属性,我们只需要像考试时做填空题一样,将需要我们填充的部分填写完整,一种窗口就设计好了。在Windows中,要达到作填空题的效果,只能通过结构体来完成,窗口的特征就是由WNDCLASS结构体来定义的。WNDCLASS结构体的定义如下(请读者自行参看MSDN):
typedef struct _WNDCLASS {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
} WNDCLASS;
下面对该结构体的成员变量做一个说明。
第一个成员变量style指定这一类型窗口的样式,常用的样式如下:
n CS_HREDRAW
当窗口水平方向上的宽度发生变化时,将重新绘制整个窗口。当窗口发生重绘时,窗口中的文字和图形将被擦除。如果没有指定这一样式,那么在水平方向上调整窗口宽度时,将不会重绘窗口。
n CS_VREDRAW
当窗口垂直方向上的高度发生变化时,将重新绘制整个窗口。如果没有指定这一样式,那么在垂直方向上调整窗口高度时,将不会重绘窗口。
n CS_NOCLOSE
禁用系统菜单的Close命令,这将导致窗口没有关闭按钮。
n CS_DBLCLKS
当用户在窗口中双击鼠标时,向窗口过程发送鼠标双击消息。
style成员的其他取值请参阅MSDN。
知识点 在Windows.h中,以CS_开头的类样式(Class Style)标识符被定义为16位的常量,这些常量都只有某1位为1。在VC++开发环境中,利用goto definition功能,可以看到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是一个函数指针,指向窗口过程函数,窗口过程函数是一个回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方调用的,用于对该事件或条件进行响应。回调函数实现的机制是:
(1)定义一个回调函数。
(2)提供函数实现的一方在初始化的时候,将回调函数的函数指针注册给调用者。
(3)当特定的事件或条件发生的时候,调用者使用函数指针调用回调函数对事件进行处理。
针对Windows的消息处理机制,窗口过程函数被调用的过程如下:
(1)在设计窗口类的时候,将窗口过程函数的地址赋值给lpfnWndProc成员变量。
(2)调用RegsiterClass(&wndclass)注册窗口类,那么系统就有了我们所编写的窗口过程函数的地址。
(3)当应用程序接收到某一窗口的消息时,调用DispatchMessage(&msg)将消息回传给系统。系统则利用先前注册窗口类时得到的函数指针,调用窗口过程函数对消息进行处理。
一个Windows程序可以包含多个窗口过程函数,一个窗口过程总是与某一个特定的窗口类相关联(通过WNDCLASS结构体中的lpfnWndProc成员变量指定),基于该窗口过程。
lpfnWndProc成员变量的类型是WNDPROC,我们在VC++开发环境中使用goto definition功能,可以看到WNDPROC的定义:
typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);
在这里又出现了两个新的数据类型LRESULT和CALLBACK,再次使用goto definition,可以看到它们实际上是long和__stdcall。
从WNDPROC的定义可以知道,WNDPROC实际上是函数指针类型。
注意:WNDPROC被定义为指向窗口过程函数的指针类型,窗口过程函数的格式必须与WNDPROC相同。
知识点 在函数调用过程中,会使用栈。__stdcall与__cdecl是两种不同的函数调用约定,定义了函数参数入栈的顺序,由调用函数还是被调用函数将参数弹出栈,以及产生函数修饰名的方法。关于这两个调用约定的详细信息,读者可参看MSDN。对于参数个数可变的函数,例如printf,使用的是__cdecl调用约定,Win32的API函数都遵循__stdcall调用约定。在VC++开发环境中,默认的编译选项是__cdecl,对于那些需要__stdcall调用约定的函数,在声明时必须显式地加上__stdcall。在Windows程序中,回调函数必须遵循__stdcall调用约定,所以我们在声明回调函数时要使用CALLBACK。使用CALLBACK而不是__stdcall的原因是为了告诉我们这是一个回调函数。注意,在Windows 98和Windows 2000下,声明窗口过程函数时,即使不使用CALLBACK也不会出错,但在Windows NT4.0下,则会出错。
WNDCLASS结构体第三个成员变量cbClsExtra:Windows为系统中的每一个窗口类管理一个WNDCLASS结构。在应用程序注册一个窗口类时,它可以让Windows系统为WNDCLASS结构分配和追加一定字节数的附加内存空间,这部分内存空间称为类附加内存,由属于这种窗口类的所有窗口所共享,类附加内存空间用于存储类的附加信息。Windows系统把这部分内存初始化为0。一般我们将这个参数设置为0。
第四个成员变量cbWndExtra:Windows系统为每一个窗口管理一个内部数据结构,在注册一个窗口类时,应用程序能够指定一定字节数的附加内存空间,称为窗口附加内存。在创建这类窗口时,Windows系统就为窗口的结构分配和追加指定数目的窗口附加内存空间,应用程序可用这部分内存存储窗口特有的数据。Windows系统把这部分内存初始化为0。如果应用程序用WNDCLASS结构注册对话框(用资源文件中的CLASS伪指令创建),必须给DLGWINDOWEXTRA设置这个成员。一般我们将这个参数设置为0。
第五个成员变量hInstance指定包含窗口过程的程序的实例句柄。
第六个成员变量hIcon指定窗口类的图标句柄。这个成员变量必须是一个图标资源的句柄,如果这个成员为NULL,那么系统将提供一个默认的图标。
在为hIcon变量赋值时,可以调用LoadIcon函数来加载一个图标资源,返回系统分配给该图标的句柄。该函数的原型声明如下所示:
HICON LoadIcon( HINSTANCE hInstance, LPCTSTR lpIconName)
LoadIcon函数不仅可以加载Windows系统提供的标准图标到内存中,还可以加载由用户自己制作的图标资源到内存中,并返回系统分配给该图标的句柄,请参看MSDN关于LoadIcon的解释。但要注意的是,如果加载的是系统的标准图标,那么第一个参数必须为NULL。
LoadIcon的第二个参数是LPCTSTR类型,利用goto definition命令将会发现它实际被定义成CONST CHAR*,即指向字符常量的指针,而图标的ID是一个整数。对于这种情况我们需要用MAKEINTRESOURCE宏把资源ID标识符转换为需要的LPCTSTR类型。
知识点 在VC++中,对于自定义的菜单、图标、光标、对话框等资源,都保存在资源脚本(通常扩展名为.rc)文件中。在VC++开发环境中,要访问资源文件,可以单击左边项目视图窗口底部的ResourceView选项卡,你将看到以树状列表形式显示的资源项目。在任何一种资源上双击鼠标左键,将打开资源编辑器。在资源编辑器中,你可以以“所见即所得”的方式对资源进行编辑。资源文件本身是文本文件格式,如果你了解资源文件的编写格式,也可以直接使用文本编辑器对资源进行编辑。
在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是一个以空终止的字符串,指定窗口类的名字。这和汽车的设计类似,设计一款新型号的汽车,需要给该型号的汽车取一个名字。同样的,设计了一种新类型的窗口,也要为该类型的窗口取个名字,这里我们将这种类型窗口的命名为“sunxin2006”,后面将看到如何使用这个名称。
在设计完汽车后,需要报经国家有关部门审批,批准后才能生产这种类型的汽车。同样地,设计完窗口类(WNDCLASS)后,需要调用RegisterClass函数对其进行注册,注册成功后,才可以创建该类型的窗口。注册函数的原型声明如下:
ATOM RegisterClass(CONST WNDCLASS *lpWndClass);
该函数只有一个参数,即上一步骤中所设计的窗口类对象的指针。
设计好窗口类并且将其成功注册之后,就可以用CreateWindow函数产生这种类型的窗口了。CreateWindow函数的原型声明如下:
HWND CreateWindow(
LPCTSTR lpClassName, // pointer to registered class name
LPCTSTR lpWindowName, // pointer to window name
DWORD dwStyle, // window style
int x, // horizontal position of window
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成员指定的名称,在这里应该设置为“sunxin2006”,表示要产生“sunxin2006”这一类型的窗口。产生窗口的过程是由操作系统完成的,如果在调用CreateWindow函数之前,没有用RegisterClass函数注册过名称为“sunxin2006”的窗口类型,操作系统将无法得知这一类型窗口的相关信息,从而导致创建窗口失败。
参数lpWindowName指定窗口的名字。如果窗口样式指定了标题栏,那么这里指定的窗口名字将显示在标题栏上。
参数dwStyle指定创建的窗口的样式。就好像同一型号的汽车可以有不同的颜色一样,同一型号的窗口也可以有不同的外观样式。要注意区分WNDCLASS中的style成员与CreateWindow函数的dwStyle参数,前者是指定窗口类的样式,基于该窗口类创建的窗口都具有这些样式,后者是指定某个具体的窗口的样式。
在这里,我们可以给创建的窗口指定WS_OVERLAPPEDWINDOW这一类型,该类型的定义为:
#define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED | /
WS_CAPTION | /
WS_SYSMENU | /
WS_THICKFRAME | /
WS_MINIMIZEBOX | /
WS_MAXIMIZEBOX)
可以看到,WS_OVERLAPPEDWINDOW是多种窗口类型的组合,其原理和前面知识点所讲的内容是一致的。下面是这几种常用窗口类型的说明。
n WS_OVERLAPPED:产生一个层叠的窗口,一个层叠的窗口有一个标题栏和一个边框。
n WS_CAPTION:创建一个有标题栏的窗口。
n WS_SYSMENU:创建一个在标题栏上带有系统菜单的窗口,要和WS_CAPTION类型一起使用。
n WS_THICKFRAME:创建一个具有可调边框的窗口。
n WS_MINIMIZEBOX:创建一个具有最小化按钮的窗口,必须同时设定WS_ SYSMENU类型。
n WS_MAXIMIZEBOX:创建一个具有最大化按钮的窗口,必须同时设定WS_ SYSMENU类型。
使用WS_OVERLAPPEDWINDOW类型的窗口如图1.1所示。
CreateWindow函数的参数x,y,nWidth,nHeight分别指定窗口左上角的x,y坐标,窗口的宽度,高度。如果参数x被设为CW_USEDEFAULT,那么系统为窗口选择默认的左上角坐标并忽略y参数。如果参数nWidth被设为CW_USEDEFAULT,那么系统为窗口选择默认的宽度和高度,参数nHeight被忽略。
参数hWndParent指定被创建窗口的父窗口句柄。在1.2节中已经介绍了,窗口之间可以有父子关系,子窗口必须具有WS_CHILD样式。对父窗口的操作同时也会影响到子窗口,表1.1列出了对父窗口的操作如何影响子窗口。
表1.1 对父窗口的操作对子窗口的影响
父 窗 口 |
子 窗 口 |
销毁 |
在父窗口被销毁之前销毁 |
隐藏 |
在父窗口被隐藏之前隐藏,子窗口只有在父窗口可见时可见 |
移动 |
跟随父窗口客户区一起移动 |
显示 |
在父窗口显示之后显示 |
参数hMenu指定窗口菜单的句柄。
参数hInstance指定窗口所属的应用程序实例的句柄。
参数lpParam:作为WM_CREATE消息的附加参数lParam传入的数据指针。在创建多文档界面的客户窗口时,lpParam必须指向CLIENTCREATESTRUCT结构体。多数窗口将这个参数设置为NULL。
如果窗口创建成功,CreateWindow函数将返回系统为该窗口分配的句柄,否则,返回NULL。注意,在创建窗口之前应先定义一个窗口句柄变量来接收创建窗口之后返回的句柄值。
(1)显示窗口
窗口创建之后,我们要让它显示出来,这就跟汽车生产出来后要推向市场一样。调用函数ShowWindow来显示窗口,该函数的原型声明如下所示:
BOOL ShowWindow(
HWND hWnd, // handle to window
int nCmdShow // show state
);
ShowWindow函数有两个参数,第一个参数hWnd就是在上一步骤中成功创建窗口后返回的那个窗口句柄;第二个参数nCmdShow指定了窗口显示的状态,常用的有以下几种。
n SW_HIDE:隐藏窗口并激活其他窗口。
n SW_SHOW:在窗口原来的位置以原来的尺寸激活和显示窗口。
n SW_SHOWMAXIMIZED:激活窗口并将其最大化显示。
n SW_SHOWMINIMIZED:激活窗口并将其最小化显示。
n SW_SHOWNORMAL:激活并显示窗口。如果窗口是最小化或最大化的状态,系统将其恢复到原来的尺寸和大小。应用程序在第一次显示窗口的时候应该指定此标志。
关于nCmdShow参数的详细内容请参见MSDN。
(2)更新窗口
在调用ShowWindow函数之后,我们紧接着调用UpdateWindow来刷新窗口,就好像我们买了新房子,需要装修一下。UpdateWindow函数的原型声明如下:
BOOL UpdateWindow(
HWND hWnd // handle to window
);
其参数hWnd指的是创建成功后的窗口的句柄。UpdateWindow函数通过发送一个WM_PAINT消息来刷新窗口,UpdateWindow将WM_PAINT消息直接发送给了窗口过程函数进行处理,而没有放到我们前面所说的消息队列里,请读者注意这一点。关于WM_PAINT消息的作用和窗口过程函数,后面我们将会详细讲解。
到此,一个窗口就算创建完成了。