我是荔园微风,作为一名在IT界整整25年的老兵,今天说说Windows程序的运行机制。经常被问到MFC到底是一个什么技术,为了解释这个我之前还写过帖子,但是很多人还是不理解。其实这没什么,我在学生时代也被这个问题困绕过。而且那个时间学习资料没有那么丰富,网上也没有什么资料,周围也没有懂的人,那个时候理解MFC更困难。甚至在我看来,理解这个比理解人工神经网络更难。
我认为造成这种现象的根本原因就是没有搞清楚Windows程序的运行机制,因为不理解Windows程序的运行机制,所以给理解MFC带来了很大的困难。我决定带所有微软开发技术的初学者一起攻破这个问题,但是一篇文章肯定是讲不清楚的,我们要分好几章来说。需要你有足够的耐心,一起来吧。我们这次来搞清楚什么是Windows程序的窗口机制。
窗口机制?别急,我们先从WinMain()说起。所有了解过 Windows编程的程序员都知道在应用程序中有一个函数WinMain()。当Windows操作系统启动一个程序时,它调用的就是该程序的WinMain()函数。WinMain()是Windows程序的入口函数,与以前turbo 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。
这里我要解释一下什么叫_stdcall,在C语言中,如果我们有一个函数:int function(int a,int b)调用时只要用result = function(1,2)这样的方式就可以使用这个函数。但是,当高级语言被编译成计算机可以识别的机器码时,就会产生一个问题:在CPU中计算机没有办法知道一个函数调用需要多少个、什么样的参数,也没有硬件可以保存这些参数。也就是说,计算机不知道怎么给这个函数传递参数,传递参数的工作必须由函数调用者和函数本身来协调。为此,计算机提供了一种被称为栈的数据结构来支持参数传递。栈是一种先进后出的数据结构,栈有一个存储区、一个栈顶指针。栈顶指针指向堆栈中第一个可用的数据项(被称为栈顶)。用户可以在栈顶上方向栈中加入数据,这个操作被称为压栈(Push),压栈以后,栈顶自动变成新加入数据项的位置,栈顶指针也随之修改。用户也可以从堆栈中取走栈顶,称为弹出栈(Pop),弹出栈后,栈顶下的一个元素变成栈顶,栈顶指针随之修改。函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在堆栈中取得数据,并进行计算。函数计算结束以后,或者调用者、或者函数本身修改栈,使堆栈恢复原状。在参数传递中,有两个很重要的问题必须得到明确说明:当参数个数多于一个时,按照什么顺序把参数压入堆栈函数调用后,由谁来把堆栈恢复原状。
在高级语言中,通过函数调用约定来说明这两个问题。常见的调用约定有:stdcall,cdecl,fastcall,thiscall,naked call。其中stdcall很多时候被称为pascal调用约定,因为pascal是早期很常见的一种教学用计算机程序设计语言,其语法严谨,使用的函数调用约定就是stdcall。在Microsoft C++系列的C/C++编译器中,常常用PASCAL宏来声明这个调用约定,类似的宏还有WINAPI和CALLBACK。stdcall调用中像int __stdcall function(int a,int b)这样的函数调用意味着:1)参数从右向左压入堆栈,2)函数自身修改堆栈 3)函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸。以上述这个函数为例,参数b首先被压栈,然后是参数a,函数调用function(1,2)调用处翻译成汇编语言将变成:先push 2 第二个参数入栈,然后是push 1 第一个参数入栈。
第二行变量 hInstance 表示该程序当前运行的实例的句柄,句柄就是一个数值或者叫标识号,我在之前的文章中提过。当程序在Windows下运行时,它唯一标识运行中的实例。只有运行中的程序实例,才有实例句柄,如果没有装入内存,那不好意思,没有。一个应用程序可以运行多个实例,每运行一个实例,系统都会给该实例分配一个句柄值,并通过 hInstance变量传递给 WinMain()函数。注意注意,这和以前C语言编程中自己定义一个变量完全不同,C语言中是自己定义,并给予于赋值。而上面这个hInstance不是你自己赋值,而操作系统传过来的,传过来的数值具体是什么你暂时不用操心,先把下面的内容学好。唉,我当年就是太操心这个值是什么,结果本末倒置。
第三行变量 hPrevInstance 表示当前实例的前一个实例的句柄。我当年费尽心思去研究这个,结果后来一位学长告诉我,在Win32环境下这个变量是NULL,即在Win32环境下,这个参数不再起作用。不起作用的为什么放第二行?这......
第四行变量 lpCmdLine是一个以空终止的字符串,指定传递给应用程序的命令行参数。我们就以windows文件系统举例:在C盘下有一个1.BMP文件,当我们用鼠标双击这个文件时将启动画图板程序(mspaint.exe),此时系统会将 C:\1.BMP作为命令行参数传递给画图板程序的 WinMain()函数,画图板程序在得到这个文件的全路径名后,就在窗口中显示该文件的内容,也就是这幅图1.BMP。
第五行变量 nCmdShow指定程序的窗口应该如何显示,例如最大化、最小化、隐藏等。这个参数的值由该程序的调用者所指定,应用程序通常不需要去理会这个参数的值。我又崩溃了,又是一个我研究了好久的变量。
大家是不是有点感觉了,也就是windows操作系统里所有的应用程序,注意,是所有的应用程序都有自己的WinMain()函数,当你启动这个程序时,比如双击了这个程序在桌面的图标,这个程序的WinMain()函数就要开始工作了。怎么工作呢,应用程序的WinMain()函数就要接收程序以外传来的值,其中有一些值还是操作系统根据用户的操作来给予的。
那有的时候我们要模拟操作系统传递参数给应用程序的效果,那怎么办?要在Visual Studio 2022开发环境中向应用程序传递参数,可以单击项目名称再右键点【属性】,在左侧配置属性窗口中选择“调试”,在右侧窗口的“命令参数”编辑框中输入你想传递给应用程序的参数。
上面说完了 WinMain()函数,下面来说说如可创建一个完整的窗口,需要经过下面几个操作步骤:先设计窗口、再注册窗口、接着创建窗口、最后显示窗口。为什么这么烦,我在之前的文章中已经打过一个比方。就像一群小伙打算创业,刚开始有了初步构思决定开个公司(想建立窗口机制),就要先去有关部门注册公司以示合法。这里也一样需要注册这个新的窗口类以示在系统内合法。注册窗口类后,这个注册身份还得依附一个实体上。接下来小伙子们就要去找办公地点,而程序员要为窗口类找一个可以依附的窗口,所以就有了创建窗口。公司有了办公地点这个实体后,小伙子们就开始营业了,并且时刻关注本领域的销售市场变化,好让辛苦成立的公司运行下去。下面介绍创建窗口的过程。
一、设计窗口
首先要设计窗口,一个完整的窗口应具有光标、图标、背景色等特征,那我们怎么做这些呢?不用担心,Windows已经为我们定义好了一个窗口所应具有的基本属性,我们只需要填写一些值,窗口就设计好了。在Windows中,通过结构体来完成这个工作,窗口的特征就是由WNDCLASS结构体来定义的。WNDCLASS结构体的定义如下:
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指定这一类型窗口的样式风格,常用的样式如下:
CS_HREDRAW:当窗口水平方向上的宽度发生变化时,将重新绘制整个窗口。当窗口发生重绘时,口中的文字和图形将被擦除。如果没有指定这一样式,那么在水平方向上调整窗口宽度时将不会重绘窗口。CS_VREDRAW:当窗口垂直方向上的高度发生变化时,将重新绘制整个窗口。如果没有指定这一样式,那么在垂直方向上调整窗口高度时,将不会重绘窗口。CS_NOCLOSE:禁用系统菜单的Close命令,这将导致窗口没有关闭按钮。CS_DBLCLKS:当用户在窗口中双击鼠标时,向窗口过程发送鼠标双击消息。
第二个成员变量lpfnWndProc是一个函数指针,指向窗口过程函数,那什么叫窗口过程函数,在Windows操作系统里,当窗口显示之后,它就可以接收到系统源源不断地发过来的消息,然后窗口就需要处理这些消息,因此就需要一个函数来处理这些消息,这就是窗口过程函数。在API里定义了一个函数为回调函数,当系统需要向窗口发送消息时,就会调用窗口给出的回调函数WindowProc,如果WindowProc函数不处理这个消息,就可以把它转向DefWindowProc函数来处理,这是系统的默认消息处理函数。当你按下菜单,或者点击窗口时,窗口需要运行这个消息处理函数。简单的说,就是窗口消息处理函数。
窗口过程函数是一个回调函数。什么又是回调函数?回调函数就是一个被作为参数传递的函数。在C语言中,回调函数只能使用函数指针实现。回调函数的使用可以大大提升编程的效率,这使得它在现代编程中被非常多地使用。同时,有一些需求必须要使用回调函数来实现。因为可以把调用者与被调用者分开,所以调用者不关心谁是被调用者。它只需知道存在一个具有特定原型和限制条件的被调用函数。简而言之,回调函数就是允许用户把需要调用的函数的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。回调函数在实际中有许多作用。假设有这样一种情况:我们要编写一个库,它提供了某些排序算法的实现(如冒泡排序、快速排序、希尔排序等等),为了能让库更加通用,不想在函数中嵌入排序逻辑,而让使用者来实现相应的逻辑;或者,能让库可用于多种数据类型(int、float、string),此时,可以使用函数指针,并进行回调。回调可用于通知机制。
回调函数不是由该函数的实现方直接调用的,而是在特定的事件或条件发生时由另外一方调用的,用于对该事件或条件进行响应。回调函数实现的机制:先定义一个回调函数。接着提供函数实现的一方在初始化的时候,将回调函数的函数指针注册给调用者。然后当特定的事件或条件发生的时候,调用者使用函数指针调用回调函数对事件进行处理。
针对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的时候,我们知道在函数调用过程中会使用栈这个数据结构。_stdcall与_cdecl是两种不同的函数调用约定,定义了函数参数入栈的顺序,由调用函数还是被调用函数将参数弹出栈,以及产生函数修饰名的方法。对于参数个数可变的函数,例如printf,使用的是_cdecl调用约定,Win32的API函数都遵循_stdcall调用约定。在 Visual Studio 开发环境中,默认的编译选项是_cdecl,对于那些需要_stdcall调用约定的函数,在声明时必须显式地加上_stdcall。在 Windows程序中,回调函数必须遵循_stdcall调用约定。我们在声明回调函数时要使用CALLBACK。为什么呢?使用 CALLBACK 而不是_stdcall的原因是为了告诉看代码的人这是一个回调函数。
第三个成员变量 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)
Loadlcon函数不仅可以加载 Windows系统提供的标准图标到内存中,还可以加载由用户自己制作的图标资源到内存中,并返回系统分配给该图标的句柄。但要注意的是,如果加载的是系统的标准图标,那么第一个参数必须为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)。
第七个成员变量 hCursor 。指定窗口类的光标句柄。这个成员变量必须是一个光标资源的句柄,如果这个成员为NULL,那么无论何时鼠标进入应用程序窗口中,应用程序都必须明确地设置光标的形状。在为hCursor变量赋值时,可以调用LoadCursor函数来加载一个光标资源,返回系统分配给该光标的句柄。该函数的原型声明如下所示:
HCURSOR LoadCursor(HINSTANCE hInstance, LPCTSTR lpCursorName);
第八个成员变量 hbrBackground。指定窗口类的背景画刷句柄。当窗口发生重绘时,系统使用这里指定的画刷来擦除窗口的背景。我们既可以为 hbrBackground成员指定一个画刷的句柄,也可以为其指定一个标准的系统颜色值。我们可以调用 GetStockObject 函数来得到系统的标准画刷。GetStockObject函数的原型声明如下所示:
HGDIOBJ GetStockObject( int fnObject);
参数fnObject指定要获取的对象的类型。GetStockObject函数不仅可以用于获取画刷的句柄,还可以用于获取画笔、字体和调色板的句柄。由于GetStockObject函数可以返回多种资源对象的句柄,在实际调用该函数前无法确定它返回哪一种资源对象的句柄,因此它的返回值的类型定义为HGDIOBJ,在实际使用时,需要进行类型转换。例如,要为hbrBackground成员指定一个黑色画刷的句柄,可以调用如下:
wndclass. hbrBackground=(HBRUSH)GetStockObject(BLACK_BRUSH);
当窗口发生重绘时,系统会使用这里指定的黑色画刷擦除窗口的背景。
第九个成员变量lpszMenuName。是一个以空终止的字符串,指定菜单资源的名字。如果你使用菜单资源的ID号,那么需要用MAKEINTRESOURCE宏来进行转换。如果将lpszMenuName成员设置为NULL,那么基于这个窗口类创建的窗口将没有默认的菜单。
第十个成员变量lpszClassName。是一个以空终止的字符串,指定窗口类的名字。设计了一种新类型的窗口,也要为该类型的窗口取个名字,取什么名字由你定,要不我们先取名为“microsoft”。
多说一句,上面的设计窗口中,CS_开头的类样式(Class Style)标识符被定义为16位的常量,这些常量都只有某1位为1。在Visual Studio 2022开发环境中,利用“转到定义”功能,可以看到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程序中,经常会用到这种位标志标识符,后面我们在创建窗口时用到的窗口样式,也属于位标志标识符。
二、注册窗口
在设计完窗口类WNDCLASS后,需要调用RegisterClass函数对其进行注册,在注册成功后,才可以创建该类型的窗口。注册函数的原型声明如下:
ATOM RegisterClass(CONST WNDCLASS *lpWndClass);
该函数只有一个参数,即上一步骤中所设计的窗口类对象的指针。这一步较为简单。我们看下一步。
三、创建窗口
在设计好窗口类并且将其成功注册之后,就可以用CreateWindow函数产生这种类型的窗口了。CreateWindow函数的原型声明如下:
HWND CreateWindow(
LPCTSTR lpClassName,
LPCTSTR lpWindowName,
DWORD dwStyle,
int x,
int y,
int nwidth,
int nHeight,
HWND hWndParent,
HMENU hMenu,
HANDLE hInstance,
LPVOID lpParam
);
第一个参数 lpClassName。指定窗口类的名称,即我们在设计一个窗口类中为 WNDCLASS的lpszClassName成员指定的名称,在这里应该设置为“microsoft”,表示要产生“microsoft”这一类型的窗口。产生窗口的过程是由windows操作系统完成的,如果在调用CreateWindow 函数之前,没有用 RegisterClass函数注册过名称为“microsoft”的窗口类型,windows操作系统将无法得知这一类型窗口的相关信息,从而导致创建窗口失败。
第二个参数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是多种窗口类型的组合,下面是这几种常用窗口类型的说明。WS_OVERLAPPED:产生一个层叠的窗口,一个层叠的窗口有一个标题栏和一个边框。WS_CAPTION:创建一个有标题栏的窗口。WS_SYSMENU:创建一个在标题栏上带有系统菜单的窗口。WS_THICKFRAME:创建一个具有可调边框的窗口。WS_MINIMIZEBOX:创建一个具有最小化按钮的窗口。WS_MAXIMIZEBOX:创建一个具有最大化按钮的窗口。使用WS_OVERLAPPEDWINDOW类型的窗口可以说是windows操作系统中最常见的一种窗口了。
第四、五、六、七个参数分别是x、y、nwidth.、nHeight。分别指定窗口左上角的x坐标、y坐标、窗口的宽度和高度。如果参数x被设为CW_USEDEFAULT,那么系统为窗口选择默认的左上角坐标并忽略y参数。如果参数nWidth被设为CW_USEDEFAULT,那么系统为窗口选择默认的宽度和高度,参数nHeight被忽略。
第八个参数hWndParent。指定被创建窗口的父窗口句柄。窗口之间可以有父子关系,子窗口必须具有WS_CHILD样式。对父窗口的操作同时也会影响到子窗口。如果父窗口销毁,那么子窗口在父窗口被销毁之前销毁。如果父窗口隐藏,那么子窗口在父窗口被隐藏之前隐藏,子窗口只有在父窗口可见时可见。如果父窗口移动,那么子窗口跟父窗口客户区一起移动。如果父窗口显示,那么子窗口在父窗口显示之后显示。
第九个参数hMenu。指定窗口菜单的句柄。
第十个参数hInstance。指定窗口所属的应用程序实例的句柄。
第十一个参数 lpParam。作为 WM_CREATE消息的附加参数 IParam 传入的数据指针。在创建多文档界面的客户窗口时,lpParam必须指向CLIENTCREATESTRUCT结构体。多数窗口将这个参数设置为NULL。
如果窗口创建成功,CreateWindow函数将返回系统为该窗口分配的句柄,否则,返回NULL。注意,在创建窗口之前应先定义一个窗口句柄变量来接收创建窗口之后返回的句柄值。
四、显示及更新窗口
在窗口创建之后,我们要让它显示出来,调用函数ShowWindow来显示窗口,该函数的原型声明如下所示:
BOOL ShowWindow(
HWND hWnd,
int nCmdShow
);
ShowWindow 函数有两个参数。
第一个参数 hWnd。就是在上一步骤中成功创建窗口后返回的那个窗口句柄。
第二个参数nCmdShow。指定了窗口显示的状态,常用的有以下几种。SW_HIDE:隐藏窗口并激活其他窗口。SW_SHOW:在窗口原来的位置以原来的尺寸激活和显示窗口。SW_SHOWMAXIMIZED:激活窗口并将其最大化显示。SW_SHOWMINIMIZED:激活窗口并将其最小化显示。SW_SHOWNORMAL:激活并显示窗口。如果窗口是最小化或最大化的状态,则系统将其恢复到原来的尺寸和大小。应用程序在第一次显示窗口的时候应该指定此标志。
在调用 ShowWindow 函数之后,我们紧接着调用 UpdateWindow 来刷新窗口,UpdateWindow函数的原型声明如下:
BOOL UpdateWindow(
HWND hWnd
);
其参数hWnd指的是创建成功后的窗口的句柄。UpdateWindow 函数通过发送一个WM_PAINT 消息来刷新窗口, UpdateWindow将 WM_PAINT消息直接发送给了窗口过程函数进行处理,而没有放到消息队列里。
好了,现在宣布,一个窗口终于创建完成。很累啊,我现在有点羡慕人工智能专业的同学了。
各位小伙伴,我们就说到这里,下次我们再深入研究windows程序的运行机制。
作者简介:荔园微风,1981年生,高级工程师,浙大工学硕士,软件工程项目主管,做过程序员、软件设计师、系统架构师,早期的Windows程序员,Visual Studio忠实用户,C/C++使用者,是一位在计算机界学习、拼搏、奋斗了25年的老将,经历了UNIX时代、桌面WIN32时代、Web应用时代、云计算时代、手机安卓时代、大数据时代、ICT时代、AI深度学习时代、智能机器时代,我不知道未来还会有什么时代,只记得这一路走来,充满着艰辛与收获,愿同大家一起走下去,充满希望的走下去。