Win32基本概念及程序基本框架
1.1. 学习目的及方法我们这里讲的Win32编程指的就是用使用C语言和原始的API编写Windows程序,这种方法能提供给您最佳的性能、最强大的功能和在发掘Windows特性方面最大的灵活性。可执行文件相对较小且运行时不要求外部链接库(自然,Windows DLL自身除外)。最重要的是,不管您最终以什么方式开发Windows应用程序,熟悉API会使您对Windows内部有更深入的了解。 虽然我认为学习古典的Windows程序设计对任何Windows程序写作者都是重要的,我没有必要建议使用C和API编写每个Windows应用程序。许多程序员喜欢轻松的开发环境,例如Visual Basic,Visual C#或者Borland Delphi(它结合了对象导向的Pascal版本)。这些环境使程序写作者将精力集中于应用程序的使用者接口和相关使用者接口对象的程序代码上。 Microsoft Visual C++和Microsoft Foundation Class Library(MFC)是功能非常强大的工具。MFC在一组C++对象类别中封装了许多Windows程序设计中的琐碎细节。还有Java,现在非常地流行。 显然,很难说哪种方法更有利于开发Windows应用程序。更主要的是,也许是应用程序自身的特性决定了所使用的工具。不管您最后实际上使用什么工具写作程序,学习Windows API将使您更深入地了解Windows工作的方式。Windows是一个复杂的系统,在API上增加一个程序写作层并未减少它的复杂性,仅仅是掩盖了它,早晚您会碰到它。了解API会给您更好的补救机会。 在原始的Windows API之上的任何软件层都必定将您限制在全部功能的一个子集内。您也许发现,例如,使用Visual Basic编写应用程序非常理想,然而它不允许您做一个或两个很简单的基本工作。在这种情况下,您将不得不使用原始的API呼叫。API定义了作为Windows程序写作者所需的一切。没有什么方法比直接使用API更万能的了。 MFC尤其问题百出。虽然它大幅简化了某些工作(例如OLE),我却经常发现要让它们按我所想的去工作时,会在其它特性(例如Document/View架构)上碰壁。MFC还不是Windows程序设计者所追求的灵丹妙药,很少有人认为它是一个好的对象导向设计的模型。MFC程序写作者从他们使用的对象类别定义如何工作中受益颇深,并会发现他们经常参考MFC原始码,搞懂这些原始码是学习Windows API的好处之一。而我们公司的许多项目则直接是使用这种最原始的开发方式去开发,比如游戏,还有Socket公司的许多项目。 下面讲一下学习Win32编程需要注意的几点: 1. 需要有好的C/C++基础。正所谓“磨刀不误砍柴工”,理解并能运用C++的各种特性,对以后的开发都会有很大的帮助。 2. 理解Windows的消息机制,窗口句柄和其他GUI句柄的含义和用途。 3. 训练自己在编写代码时不使用参考书而是使用Help Online。 4. 记住一些常用的消息名称和参数的意义。 5. 学会看别人的代码。 1.2. 基本概念介绍然后我们介绍一下Win32编程的几个重要的概念,Windows系统是一个消息驱动的OS,什么是消息呢?我很难说得清楚,也很难下一个定义,我下面从不同的几个方面讲解一下,希望大家看了后有一点了解。 1、消息的组成:一个消息由一个消息名称(UINT),和两个参数(WPARAM,LPARAM)。当用户进行了输入或是窗口的状态发生改变时系统都会发送消息到某一个窗口。例如当菜单转中之后会有WM_COMMAND消息发送,WPARAM的高字中(HIWORD(wParam))是命令的ID号,对菜单来讲就是菜单ID。当然用户也可以定义自己的消息名称,也可以利用自定义消息来发送通知和传送数据。 2、谁将收到消息:一个消息必须由一个窗口接收。在窗口的过程(WNDPROC)中可以对消息进行分析,对自己感兴趣的消息进行处理。例如你希望对菜单选择进行处理那么你可以定义对WM_COMMAND进行处理的代码,如果希望在窗口中进行图形输出就必须对WM_PAINT进行处理。 3、未处理的消息到那里去了:M$为窗口编写了默认的窗口过程,这个窗口过程将负责处理那些你不处理消息。正因为有了这个默认窗口过程我们才可以利用Windows的窗口进行开发而不必过多关注窗口各种消息的处理。例如窗口在被拖动时会有很多消息发送,而我们都可以不予理睬让系统自己去处理。 4、窗口句柄:说到消息就不能不说窗口句柄,系统通过窗口句柄来在整个系统中唯一标识一个窗口,发送一个消息时必须指定一个窗口句柄表明该消息由那个窗口接收。而每个窗口都会有自己的窗口过程,所以用户的输入就会被正确的处理。 1.3. Win32程序基本框架下面用一个实例来具体讲一下Win32程序基本框架,这个实例就是EVC++提供的Hello World程序,在新建WCE Pocket PC Application时即可创建这个例子程序,我们现在分块来解释一下这个程序: 1.3.1. WinMain()函数 WinMain()函数是应用程序开始执行时的入口点,通常也是应用程序结束任务退出时的出口点。它与DOS程序的main()函数起同样的作用,有一点不同的是,WinMain()函数必须带有四个参数,它们是系统传递给它的。WinMain()函数的原型如下: int PASCAL WinMain( HINSTANCE hInstance, //当前实例句柄 HINSTANCE hPrevInstance, //前一个实例句柄 LPSTR lpCmdLine, //命令行字符 int nCmdShow) //窗口显示方式 第一个参数hInstance,是标识该应用程序当前的实例的句柄。它是HINSTANCE类型,HINSTANCE是Handle of Instance的缩写,表示实例的句柄。hInstance是一个很关键的数据,它唯一的代表该应用程序,在后面初始化程序主窗口的过程中需要用到这个参数。 这里有两个概念,一个是实例,一个是句柄。实例代表的是应用程序执行的整个过程和方法,一个应用程序如果没有被执行,只是存在于磁盘上,那么就说它是没有被实例化的;只要一执行,则说该程序的一个实例在运行。句柄,顾名思义,指的是一个对象的把柄。在Windows中,有各种各样的句柄,它们都是32位的指针变量,用来指向该对象所占据的内存区。句柄的使用,可以极大的方便Windows管理其内存中的各种对象。 第二个参数是hPrevInstance,它是用来标识该应用程序的前一个实例句柄。对于基于Win32的应用程序来说,这个参数总是NULL。这是因为在Win95操作系统中,应用程序的每个实例都有各自独立的地址空间,即使同一个应用程序被执行了两次,在内存中也会为它们的每一个实例分配新的内存空间,所以一个应用程序被执行后,不会有前一个实例存在的可能。也就是说,hPrevInstance这个参数是完全没有必要的,只是为了提供与16位Windows的应用程序形式上的兼容性,才保留了这个参数。在以前的16位Windows环境下(如Windows3.2),hPrevInstance用来标识与hInstance相关的应用程序的前一个句柄。 第三个参数是lpCmdLine,是指向应用程序命令行参数字符串的指针。 最后一个参数是nCmdShow,是一个用来指定窗口显示方式的整数。这个整数值可以是SW_SHOW、SW_HIDE、SW_SHOWMAXIMIZED、SW_SHOWMINIMIZED等。 1.3.2. 注册窗口类 一个应用程序可以有许多窗口,但只有一个是主窗口,它是与该应用程序的实例句柄唯一关联的。上面的例程中,创建主窗口的函数是InitWindow()。 通常要对填充一个窗口类结构WNDCLASS,然后调用RegisterClass()对该窗口类进行注册。每个窗口都有一些基本的属性,如窗口边框、窗口标题栏文字、窗口大小和位置、鼠标、背景色、处理窗口消息函数的名称等等。注册的过程也就是将这些属性告诉系统,然后再调用CreateWindow()函数创建出窗口。这也就象你去裁缝店订做一件衣服,先要告诉店老板你的身材尺寸、布料颜色、以及你想要的款式,然后他才能为你做出一件让你满意的衣服。 在VC的帮助中,可以看到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; 在Win95和WinNT的具有新界面特性的系统中,为了支持新的窗口界面特性,还有一种扩展的窗口类型WNDCLASSEX,它的定义如下: typedef struct _WNDCLASSEX { UINT cbSize; //指定WNDCLASSEX结构的大小 UINT style; WNDPROC lpfnWndProc; int cbClsExtra; int cbWndExtra; HANDLE hInstance; HICON hIcon; HCURSOR hCursor; HBRUSH hbrBackground; LPCTSTR lpszMenuName; LPCTSTR lpszClassName; HICON hIconSm; //窗口的小图标 } WNDCLASSEX; WNDCLASS和WNDCLASSEX这两个结构基本上是一致的,只是WNDCLASSEX结构中多了cbSize和hIconSm这两个成员。WNDCLASS结构的各成员中,其注释后打了星号的表示该项应特别注意。 WNDCLASS结构的第一个成员style表示窗口类的风格,它往往是由一些基本的风格通过位的“或”操作(操作符位“|”)组合而成。下表列出了一些常用的基本窗口风格:风格 含义 CS_HREDRAW 如果窗口客户区宽度发生改变,重绘整个窗口 CS_VREDRAW 如果窗口客户区高度发生改变,重绘整个窗口 CS_DBLCLKS 能感受用户在窗口中的双击消息 CS_NOCLOSE 禁用系统菜单中的“关闭”命令 CS_OWNDC 为该窗口类的各窗口分配各自独立的设备环境 CS_CLASSDC 为该窗口类的各窗口分配一个共享的设备环境 CS_PARENTDC 指定子窗口继承其父窗口的设备环境 CS_SAVEBITS 把被窗口遮掩的屏幕图象部分作为位图保存起来。当该窗口被移动时,Windows使用被保存的位图来重建屏幕图象 在HelloWorld应用程序中,是按如下方式对WNDCLASS结构进行填充和注册的: wc.style = CS_VREDRAW | CS_HREDRAW; wc.lpfnWndProc = (WNDPROC)WinProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInstance; wc.hIcon = LoadIcon( hInstance, IDI_APPLICATION ); wc.hCursor = LoadCursor( NULL, IDC_ARROW ); wc.hbrBackground = GetStockObject(WHITE_BRUSH); wc.lpszMenuName = NULL; wc.lpszClassName = "HelloWorld"; 可以看到,wc.style被设为CS_VREDRAW | CS_HREDRAW,表示只要窗口的高度或宽度发生变化,都会重画整个窗口。 第二个成员lpfnWndProc的值为(WNDPROC)WinProc。表明该窗口类的消息处理函数是WinProc()函数。这里,要指定窗口的消息处理函数的远指针,输入消息处理函数的函数名称即可,必要时应该进行强制类型转换,将其转换成WNDPROC型。 接下来的cbClsExtra和wc.cbWndExtra在大多数情况下都会设为0。 然后的hInstance成员,给它的值是由WinMain()传来的应用程序的实例句柄,表明该窗口与该实例是相关联的。事实上,只要是注册窗口类,该成员的值始终是该程序的实例句柄,你应该象背书一样记住它。 下面的hIcon,是让你给这个窗口指定一个图标,调用 LoadIcon( hInstance, IDI_APPLICATION ),可以调用系统内部预先定义好的标志符为IDC_APPLICATION的图标作为该窗口的图标。 同样,调用LoadCursor( NULL, IDC_ARROW )为该窗口调用系统内部预先定义好的箭头型鼠标。 hbrBackground成员用来定义窗口的背景画刷颜色,也就是该窗口的背景色。调用GetStockObject(WHITE_BRUSH)可以获得系统内部预先定义好的白色画刷作为窗口的背景色。 上面的LoadIcon()、LoadCursor()、GetStockObject()都是Windows的API函数,它们的用法可以参看VC的帮助,这里就不多介绍了。 lpszMenuName成员的值我们给它NULL,表示该窗口将没有菜单。如果你想让你的窗口拥有菜单,就把lpszMenuName成员赋值为标志菜单资源的字符串。 WNDCLASS结构的最后一个成员lpszClassName是让你给这个窗口类起一个唯一的名称,因为Windows操作系统中有许许多多的窗口类,必须用一个独一无二的名称来代表它们。通常,你可以用你的程序名来命名这个窗口类的名称。这个名称将在创建窗口的CreateWindow()函数中用到。 填充完毕后,对于WNDCLASS结构,调用RegisterClass()函数进行注册;对于WNDCLASSEX结构,调用RegisterClassEx()函数进行注册,它们的原型分别如下: ATOM RegisterClass( CONST WNDCLASS *lpWndClass ); ATOM RegisterClassEx( CONST WNDCLASSEX *lpwcx ); 该函数如调用成功,则返回一个非0值,表明系统中已经注册了一个名为HelloWorld的窗口类。如果失败,则返回0。 1.3.3. 创建窗口 当窗口类注册完毕之后,并不会有窗口显示出来,因为注册的过程仅仅是为创建窗口所做的准备工作。实际创建一个窗口的是通过调用CreateWindow()函数完成的。窗口类中已经预先定义了窗口的一般属性,而CreateWindow()中的参数可以进一步指定一个窗口的更具体的属性,在HelloWorld程序中,是如下调用CreateWindow()函数来创建窗口的: hwnd = CreateWindow( "HelloWorld", //创建窗口所用的窗口类的名称* "一个基本的Win32程序", //窗口标题 WS_OVERLAPPEDWINDOW, //窗口风格,定义为普通型* 100, //窗口位置的x坐标 100, //窗口位置的y坐标 400, //窗口的宽度 300, //窗口的高度 NULL, //父窗口句柄 NULL, //菜单句柄 hInstance, //应用程序实例句柄* NULL ); //一般都为NULL CreateWindow()函数的参数的含义在上面的注释中已有介绍,注释后打了星号标记的参数应该着重注意,其它的参数都很简单,不多做介绍,可参看VC的帮助。 第一个参数是创建该窗口所使用的窗口类的名称,注意这个名称应与前面所注册的窗口类的名称一致。 第三个参数为创建的窗口的风格,下表列出了常用的窗口风格:风格 含义 WS_OVERLAPPEDWINDOW 创建一个层叠式窗口,有边框、标题栏、系统菜单、最大最小化按钮,是以下几种风格的集合:WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX, WS_MAXIMIZEBOX WS_POPUPWINDOW 创建一个弹出式窗口,是以下几种风格的集合: WS_BORDER,WS_POPUP,WS_SYSMENU。WS_CAPTION与WS_POPUPWINDOW风格必须一起使用才能使窗口菜单可见 WS_OVERLAPPED 创建一个层叠式窗口,它有标题栏和边框,与WS_TILED风格一样 WS_POPUP 该窗口为弹出式窗口,不能与WS_CHILD同时使用 WS_BORDER 窗口有单线边框 WS_CAPTION 窗口有标题栏 WS_CHILD 该窗口为子窗口,不能与WS_POPUP同时使用 WS_DISABLED 该窗口为无效,即对用户操作不产生任何反应 WS_HSCROLL 窗口有水平滚动条 WS_ICONIC 窗口初始化为最小化 WS_MAXIMIZE 窗口初始化为最大化 WS_MAXIMIZEBOX 窗口有最大化按钮 WS_MINIMIZE 与WS_MAXIMIZE一样 WS_MINIMIZEBOX 窗口有最小化按钮 WS_SIZEBOX 边框可进行大小控制的窗口 WS_SYSMENU 创建一个有系统菜单的窗口,必须与WS_CAPTION风格同时使用 WS_THICKFRAME 创建一个大小可控制的窗口,与WS_SIZEBOX 风格一样. WS_TILED 创建一个层叠式窗口,有标题栏 WS_VISIBLE 窗口为可见 WS_VSCROLL 窗口有垂直滚动条 程序中使用了WS_OVERLAPPEDWINDOW标志,它是创建一个普通窗口常用的标志。而在DirectX编程中,我们常用的是WS_POPUP,用这个标志创建的窗口没有标题栏和系统菜单,如果设定窗口为最大化,客户区可以占满整个屏幕,以满足DirectX编程的需要。 CreateWindow()函数后面的参数中,仍用到了该应用程序的实例句柄hInstance。 如果窗口创建成功,返回值是新窗口的句柄,否则返回NULL。 显示和更新窗口 窗口创建后,并不会在屏幕上显示出来,要真正把窗口显示在屏幕上,还得使用ShowWindow()函数,其原型如下: BOOL ShowWindow( HWND hWnd, int nCmdShow ); 参数hWnd指定要显示得窗口的句柄,nCmdShow表示窗口的显示方式,这里指定为从WinMain()函数的nCmdShow所传递而来的值。 由于ShowWindow()函数的执行优先级不高,所以当系统正忙着执行其它的任务时,窗口不会立即显示出来,此时,调用UpdateWindow()函数以可以立即显示窗口。其函数原型如下: BOOL UpdateWindow( HWND hWnd ); 消息循环 在Win32编程中,消息循环是相当重要的一个概念,看似很难,但是使用起来却是非常简单。在WinMain()函数中,调用InitWindow()函数成功的创建了应用程序主窗口之后,就要启动消息循环,其代码如下: while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } Windows应用程序可以接收以各种形式输入的信息,这包括键盘、鼠标动作 、记时器产生的消息,也可以是其它应用程序发来的消息等等。Windows系统自动监控所有的输入设备,并将其消息放入该应用程序的消息队列中。 GetMessage()函数则是用来从应用程序的消息队列中按照先进先出的原则将这些消息一个个的取出来,放进一个MSG结构中去。GetMessage()函数原型如下: BOOL GetMessage( LPMSG lpMsg, //指向一个MSG结构的指针,用来保存消息 HWND hWnd, //指定哪个窗口的消息将被获取 UINT wMsgFilterMin, //指定获取的主消息值的最小值 UINT wMsgFilterMax //指定获取的主消息值的最大值 ); GetMessage()将获取的消息复制到一个MSG结构中。如果队列中没有任何消息,GetMessage()函数将一直空闲直到队列中又有消息时再返回。如果队列中已有消息,它将取出一个后返回。MSG结构包含了一条Windows消息的完整信息,其定义如下: typedef struct tagMSG { HWND hwnd; //接收消息的窗口句柄 UINT message; //主消息值 WPARAM wParam; //副消息值,其具体含义依赖于主消息值 LPARAM lParam; //副消息值,其具体含义依赖于主消息值 DWORD time; //消息被投递的时间 POINT pt; //鼠标的位置 } MSG; 该结构中的主消息表明了消息的类型,例如是键盘消息还是鼠标消息等,副消息的含义则依赖于主消息值,例如:如果主消息是键盘消息,那么副消息中则存储了是键盘的哪个具体键的信息。 GetMessage()函数还可以过滤消息,它的第二个参数是用来指定从哪个窗口的消息队列中获取消息,其它窗口的消息将被过滤掉。如果该参数为NULL,则GetMessage()从该应用程序线程的所有窗口的消息队列中获取消息。 第三个和第四个参数是用来过滤MSG结构中主消息值的,主消息值在wMsgFilterMin和wMsgFilterMax之外的消息将被过滤掉。如果这两个参数为0,则表示接收所有消息。 当且仅当GetMessage()函数在获取到WM_QUIT消息后,将返回0值,于是程序退出消息循环。 TranslateMessage()函数的作用是把虚拟键消息转换到字符消息,以满足键盘输入的需要。DispatchMessage()函数所完成的工作是把当前的消息发送到对应的窗口过程中去。 开启消息循环其实是很简单的一个步骤,几乎所有的程序都是按照HelloWorld的这个方法。你完全不必去深究这些函数的作用,只是简单的照抄就可以了。 1.3.4. 消息处理函数 消息处理函数又叫窗口过程,在这个函数中,不同的消息将用switch语句分配到不同的处理程序中去。Windows的消息处理函数都有一个确定的样式,即这种函数的参数个数和类型以及其返回值的类型都有明确的规定。在VC的说明书中,消息处理函数的原型是这样定义的: LRESULT CALLBACK WindowProc( HWND hwnd, //接收消息窗口的句柄 UINT uMsg, //主消息值 WPARAM wParam, //副消息值 LPARAM lParam //副消息值 ); 如果你的程序中还有其它的消息处理函数,也都必须按照上面的这个样式来定义,但函数名称可以随便取。HelloWorld中的WinProc()函数就是这样一个典型的消息处理函数。 消息处理函数的四个参数是由GetMessage()函数从消息队列中获得MSG结构,然后分解后得到的。第二个参数uMsg和MSG结构中的message值是一致的,代表了主消息值。程序中用switch语句来将不同类型的消息分配到不同的处理程序中去。 WinProc()函数明确的处理了4个消息,分别是WM_KEYDOWN(击键消息)、WM_RBUTTONDOWN(鼠标右键按下消息)、WM_PAINT(窗口重画消息)、WM_DESTROY(销毁窗口消息)。 值得注意的是,应用程序发送到窗口的消息远远不止以上这几条,象WM_SIZE、WM_MINIMIZE、WM_CREATE、WM_MOVE等这样频频使用的消息就有几十条。为了减轻编程的负担,Windows的API提供了DefWindowProc()函数来处理这些最常用的消息,调用了这个函数后,这些消息将按照系统默认的方式得到处理。 因此,在switch_case语句中,只须明确的处理那些有必要进行特别响应的消息,把其余的消息交给DefWindowProc()函数来处理,是一种明智的选择,也是你必须做的一件事。 结束消息循环 当用户按Alt+F4或单击窗口右上角的退出按钮,系统就向应用程序发送一条WM_DESTROY的消息。在处理此消息时,调用了PostQuitMessage()函数,该函数会给窗口的消息队列中发送一条WM_QUIT的消息。在消息循环中,GetMessage()函数一旦检索到这条消息,就会返回FALSE,从而结束消息循环,随后,程序也结束。