Windows游戏编程大师技巧(二)

    技术2022-05-11  104

    第二章 Windows编程模型   Windows编程就像去看牙科医生:明知道对你有好处,但就是没有人乐意去。是不是这 样?在本章中,我将要使用“禅”的方法——或者换句话说,就是深入浅出地向你介绍Wi ndows编程基础。我可不能保证在阅读了本章后你就会“去见牙科医生”,但是我也保证你 会比以往更喜欢Windows编程。下面是本章的内容:   ·Windows的历史   ·Windows的基本风格   ·Windows的类   ·创建Windows   ·Windows事件句柄   ·事件驱动编程和事件循环   ·打开多个窗口 Windows的历史   读者可能会因为我要解放你的思想而感到十分恐惧(特别是钟情于DOS的顽固分子)。让 我们迅速浏览一下Windows的发展历程以及与游戏发展的关系,好吗? 早期的Windows版本   Windows的发展始于Windows1.0版本。这是Microsoft公司在商业视窗操作系统的第一 次尝试,当然是一个非常失败的产品。Windows1.0完全建立在DOS基础上(这就是一个错误 ),不能执行多任务,运行速度很慢,看上去也差劲。它的外观可能是其失败的最重要原因 。除了讽刺以外,问题还在于Windows1.1与那个时代的80286计算机(或更差劲的8086)所能 提供的相比需要更高的硬件、图像和声音性能。   然而,Microsoft稳步前进,很快就推出了Windows2.0。我记得获得Windows2.0的测试 版时我正在软件出版公司工作。在会议室中,挤满了公司的行政官员和董事长(像往常一样 ,他正拿着一不鸡尾酒)。我们运行Windows 2.0测试演示版,装载了多个应用程序,看上 去似乎还在工作。但是,那IMB推出了PM。PM看上去要好得多,它是建立在比Windows2.0先 进得多的操作系统OS/2的基础上的,而Windows 2.0依然是建立在DOS基础上的视窗管理器 。那天会议室中的结论是“不错,但还不是一个可行的操作系统,如果我们仍然留恋在DO S上,那我还能有鸡尾酒喝吗?” Windows 3.x   1990年,终于发生了翻天覆地的变化,因为Windows 3.0出世了,而且其表现的确非常 出色!尽管它仍然赶不上Mac OS的标准,但是谁还在意呢?(真正的程序员都憎恨Mac)。软 件开发人员终于可以在PC机上创建迷人的应用程序了,而商用应用程序也开始脱离DOS。这 成了PC机的转折点,终于将Mac完全排除在商用程序之外了,而后也将其挤出台式机出版业 。(那时,Apple公司每5分钟就推出一种新硬件)。   尽管Windows3.0工作良好,却还是存在许多的问题、软件漏洞,但从技术上说它已是 Windows2.0之后的巨大突破,有问题也是在所难免。为了解决这些问题,Microsoft推出了 Windows3.1,开始公关部和市场部打算称之为Windows4.0,但是,Microsoft决定只简单地 称之为Windows3.1,因为它不足以称之为升级的换代版本。它还没有做到市场部广告宣传 的那样棒。   Windows3.1非常可靠。它带有多媒体扩展以提供音频和视频支持,而且它还是一个出 色的、全面的操作系统,用户能够以统一的方式来工作。另外,还存在一些其他版本。如 可以支持网络的Windows3.11(适用于工作的Windows)。唯一的问题是Windows3.1仍然是一 个DOS应用程序,运行DOS扩展器上。 Windows95   另一方面,游戏编程行业还在唱“DOS永存!”的赞歌,而我则已经开始热衷于使用W indows3.1。但是,1995年世界开始冷却——Windows95终于推出。它是一个真正32位的、 多任务、多线程的操作系统。诚然,其中还残存一些16位代码,但在极大程度上,Window s95是PC机的终极度开发和发布平台。   (当然,Windows NT 3.0也同时推出,但是NT对于大多数用户来讲还是不可用的,因此 这里也就不再赘述)。   当Windows95推出后,我才真正开始喜欢Windows编程。我一直痛恨使用Windows1.0、 2.0、3.0和3.1来编程,尽管随着每一种版本的推出,这种憎恨都越来越少。当Windows95 出现时,它彻底改变了我的思想,如同其他被征服的人的感觉一样——它看上去非常酷! 那正是我所需要的。   提示:游戏编程行业中最重要的事情是游戏表现如何,游戏的画面如何,同时还要尽 可能减轻审阅人的工作。   因此几乎一夜间,Windows95就改变了整个计算机行业。的确,目前还有一些公司仍然 在使用Windows3.1(你能相信吗?),但是Windows95使得基于Intel的PC成为除游戏之外  的所有应用程序的选择。不错,尽管游戏程序员知道DOS退出游戏编程行业只是个时间的问 题了,但是DOS还是它们的核心。   1996年,Microsoft公司发布了Game SDK(游戏软件开发工具包),这基本上就是Direc tX的第一个版本。这种技术仅能在Window95环境下工作,但是它的确太慢了,甚至竞争不 过DOS游戏(如DOOM和Duke Nukem等)。游戏开发人员继续使用DOS32来开发游戏,但是他们 知道DirectX具有足够快的速度,从而能使游戏能够流畅地运行在PC机上已为时不远。   到了3.0版,DirectX的速度在同样计算机上已经和DOS32一样快了。到了5.0版,Dire ctX已经相当完美,实现了该技术最初的承诺。现在要意识到:Win32/DirectX是PC机上开 发游戏的唯一方式。这是历史的选择。 Windows 98   1998年中期,Windows 98推出了。这至多是一个改进的版本,而不像Windows95那样是 一个换代的产品,但毫无疑问它也占有很重要的地位。Windows98像一辆旧车改装的高速汽 车——实际上它是一头皮毛圆润光滑、脚力持久、飞奔跳动的驴。它是全32位的,能够支 持你想做的所有事情,并具有无限扩充能力。它很好地集成了DirectX、3D图形、网络以及 Internet。   Windows98和Window95相比也非常稳定。当然Windows98仍然经常死机,但可以相信的 是,这比Windows95少了许多。对即插即用支持得很好,并且能够很好地运行——这只是个 时间问题。 Windows NT   现在我们来讨论一下Windows NT。在本书编写期间,Windows NT正在推出5.0版本。我 所能说的是,它最终将取代Windows 9X成为每个人的操作系统选择。NT要比Windows9X严谨 得多;而且绝大多数游戏程序员都将在NT上开发游戏,这将使Windows 9X退出历史舞台。 Windows NT 5.0最酷的是它完全支持即插即用和Win32/DirectX,因此使用DirectX为Wind ows 9X编写的应用程序可以在WindowsNT5.0或更高版本上运行。这可是个好消息。因为从 历史上看,编写PC游戏的开发人员现在具有最大的市场潜力。   那么最低标准是什么呢?如果你使用DirectX(或其他工具)编写了一个Win32应用程序 ,它完全可以在Windows 95、98、和NT 5.0或更高版本上运行。这可是件好事情。因此你 在本书中所学到的任何东西至少可以应用到三种操作系统上,也可以运行于安装NT和Dire ctX的其他计算机上,如DEC的Alphas。还有Windows CE——DirectX和Win32衍生的运行于 其他系统上的操作系统。 Windows的基本风格:Win9X/NT   和DOS不同,Windows是一个多任务的操作系统,允许许多应用程序和更小的程序同时 运行,可以最大限度的发挥硬件的性能。这表明Windows是一个共享的环境——一个应用程 序不可能独占整个系统。尽管Windows 95、98、和NT很相似,但仍然存在许多技术上的差 别。但是就我们所涉及的,不可能去详细归纳。这里所参照的Windows机器一般是指Win9X /NT或Windows环境。让我们开始吧! 多任务和多线程   如我所说,Windows允许不同的应用程序以轮流的方式同时执行,每一个应用程序都占 用一段很短的时间段来运行,下一个应用程序轮换运行。如图2.1所示,CPU由几个不同的 应用程序以轮流的方式共享。判断出下一个运行的应用程序、分配给每个应用程序的时间 量是调度程序的工作。   调度程序可以非常简单——每个应用程序分配固定的运行时间,也可以非常复杂—— 将应用程序设定为不同的优先级和抢先性或低优先级的事件。就WinX/NT而言,调度程序采 用基于优先级的抢先占用方式。这就意味着一些应用程序要比其他的应用程序占用处理器 更多的时间,但是如果一个应用程序需要CPU处理的话,在另一任务运行的同时,当前的任 务可以被锁定或抢先占用。   但是不要对此有太多的担心,除非你正在编写OS(操作系统)或实时代码——其细节事 关重大。大多数情况下,Windows将执行和调度你的应用程序,无需你参与。   深入接触Windows,我们可以看到,它不仅是多任务的,而且还是多线程的。这意味着 程序由许多理简单的多个执行线程构成。这些线程(像更重要的进程)如程序一样被调度。 实际上,在你的计算机上可同时运行30~50个线程,执行不同的任务。所以事实上你可能 只运行一个程序,但这个程序由一个或多个执行线程构成。   Windows实际的多线程示意图如图2.2所示,从图中可以看到,每一个程序实际上都是 由一个主线程和几个工作线程构成。 ———————————————— ———————————————— 获取线程的信息   下面让我们来看一下你的计算机现在正在运行多少个线程。在Windows机器上,同时按 Ctrl+ALt+Delete键,弹出显示正在运行的任务(过程)的当前程序任务管理器。这和我们所 希望的不同,但也很接近。我们希望的是一个显示正在执行的实际线程数的工具或程序。 许多共享软件和商用软件工具都能做到这一点,但是Windows内嵌了这几个工具。   在安装Windows的目录(一般是Windows)下,可以发现一个名字为SYSMON.EXE(Windows 95/98)或PREFMON.EXE程序。图中除了正在运行的线程外还有大量的信息,如:内存使用 和处理器装载等。实际上在进行程序开发时,我喜欢使SYSMON.EXE运行,由此可以了解正 在进行什么以及系统如何加载程序。   你可能想知道能否对线程的创建进行控制,答案是能够!!!实际上这是Windows游戏 编程最令人激动的事情之一——就像我们所希望的那样除了游戏主进程外,还能够执行其 他的任务,我们也能够创建像其他任务一样多的线程。   注意:在Windows98/NT环境下,实际上还有一种叫fiber的新型执行对象,它比线程还 简单(明白吗?线程是由fiber构成的)。   这和DOS游戏程序的编写有很大不同。DOS是单线程操作系统,也就是说一旦你的程序 开始运行,就只能运行该程序(不时出现的中断管理除外)。因此,如果想使用任何一种多 任务或多线程,就必须自己来模拟(参阅《Sams Teach Yourself Game Programming in 2 1 Days》中关于一个完整的基于DOS多任务核心部分)。这也正是游戏程序员在这么多年中 所作的事。的确,模拟多任务和多线程远远不能和拥有一个完整的支持多任务和多线程的 操作系统相提并论。但是对于单个游戏来讲,它足可以良好地工作。   在我们接触到真正的Windows编程和那些工作代码之前,我想提及一个细节。你可能在 想,Windows真是一个神奇的操作系统,因为它允许多个任务和程序立即执行。请记住,实 际上并不是这样的。如果不有一个处理器的话。那么一次也只能执行一个执行流,线程、 程序或你所调用的任何对象。Windows相互之间的切换太快了,以至于看上去就像几个程在 同时运行一样。另一方面,如果有几个处理器的话,可以同时运行多个程序。例如,我有 一个双CPU的PentiumⅡ处理器在运行WindowsNT5.0。使用这种配置,可以同时执行两个指 令流。   我希望在不远的将来,个人计算机的新型微处理器结构能够允许多个线程或fiber同时 执行,将这样一个目标作为处理器设计的一部分。例如,Pentium具有两个执行单元——U 管和V管。因此它能够立即执行两个指令。但是,这两个指令都是来自同一个线程,类似的 是PentiumⅡ能够立即执行5个简单的指令,但也是来自同一个线程。 事件模型   Windows是个多任务/多线程的操作系统,并且还是一个事件驱动的操作系统。和DOS程 序不同的是,Windows程序都是等着用户去使用,由此而触发一个事件,然后Windows对该 事件发生响应,进行动作。请看图2.4所示的示意图,图中描述了大量的应用程序窗口,每 个程序都向Windows发送待处理的事件和消息。Windows对其中的一些进行处理,大部分的 消息和事件被传递给应用程序来处理。   这样做的好处是你不必去关心其他的正在运行的应用程序,Windows会为你处理它们。 你所要关心的就是你自己的应用程序和窗口中信息的处理。这在Windows3.0/3.1中是根本 不可能的。Windows的那些版本并不是真正的多任务操作系统,每一个应用程序都要产生下 一个程序,也就是说,在这些版本的Windows下运行的应用程序感觉相当粗糙、缓慢。如果 有其他应用程序干扰系统的话,这个正在“温顺地”运行的程序将停止工作。但这种情况 在Windows9X/NT下就不会出现。操作系统将会适当的时间终止你的应用程序——当然,运 行速度非常快,你根本就不会注意到。   到目前为止,读者已了解了所有有关操作系统的概念。幸运的是,有了Windows这个目 前最好的编写游戏操作系统,读者根本就不必担心程序调度——你所要考的就是游戏代码 和如何最大限度地发挥计算机的性能。   在本章后面内容中,我们要接触一些实际的编程工作,便于读者了解Windows编程有多 容易。但是(永远都有但是)在进行实际编程之前我们应当了解一些Microsoft程序员喜欢使 用的约定。这样你就不会被那些古怪的函数和变量名弄得不知所措。 按照Microsoft方式编程:匈牙利符号表示法     如果你正在动作一个像Microsoft一样的公司,有几千个程序员都在干不同的项目 ,在某一点上就应当提出一个编写代码的标准方式。否则,结果将是一片混乱。因此一个 名字叫Charles Simonyi的人被委托创立了一套编写Microsoft代码的规范。这个规范已经 用作编写代码的基本指导说明书。所有Microsoft的API、界面、技术文件等等都采用这些 规范。   这个规范通常被称为匈牙利符号表示法,可能是因为创立这个规范工作很长时间,弄 得他饥肠辘辘的原因吧(英文中饥饿和匈牙利谐音),或者可能他是匈牙利人。对我们根本 不知道,关键是你必须了解这个规范,以便于你能够阅读Microsoft代码。匈牙利符号表示 法包括许多与下列命名有关的约定:   ·变量   ·函数   ·类型和常量   ·类   ·参数   表2.1给出了匈牙利符号表示法使用的前缀代码。这些代码在大多数情况下一半用于前 缀变量名,其他约定根据名称确定。其他解释可以参考本表。      表2.1 匈牙利符号表示法的前缀代码指导说明书   ━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━   前缀    │数据类型(基本类型)   ──────┼────────────────────────   c     │字符   by    │字节(无符号字符)   n     │短整数和整数(表示一个数)   i     │整数   x,y   │短整数(通常用于x坐标和y坐标)   cx,cy │短整数(通常用于表示x和y的长度:c表示计数)   b     │布尔型(整数)   w     │UINT(无符号整数)和WORD(无符号字)   l     │LONG(长整数)   dw    │DWORD(无符号长整数)   fn    │函数指针   s     │串   sz,str│以0字节终止的字符串   lp    │32位的长整数指针   h     │编号(常用于表示Windows对象)   msg   │消息   ──────┴─────────────────────── 变量的命名   应用匈牙利符号表示法,变量可用表2.1中的前缀代码来表示。另外,当一个变量是由 一个或几个子名构成时,每一个子名都要以大写字母开头。下面是几个例子:   char *szFileName;//a nulla terminated string   int *lpiDate;//a 32-bit pointer to an int   BOOL bSemaphore;//a boolean value   WORD dwMaxCount;//a 32-bit unsigned WORD   尽管我了解一个函数的局部变量没有说明,但是也有个别表示全局变量:   int g_iXPos;//a global x-position   int g_iTimer;//a global y-position   char *g_szString;//a global NULL terminated string   总的来说,变量以g_开头,或者有时就只用g。 函数的命名   函数和变量命名方式相同,但是没有前缀。换句话说,子名的第一个字母要大写。下 面是几个例子:   int PlotPixel(int ix,int iy,int ic);   void *MemScan(char *szString);   而且,下划线是非法的,例如,下面的函数名表示是无效的匈牙利符号表示法:   int Get_Pixel(int ix,int iy); 类型和常量的命名   所有的类型和常量都是大写字母,但名字中可以允许使用下划线。例如:   const LONG NUM_SECTORS = 100;//a C++ style constant   #define MAX_CELLS 64;//a C style constant   #define POWERUNIT 100;//a C style constant   typedef unsigned char UCHAR;//a user defined type   这儿并没有什么不同的地方——非常标准的定义。尽管大多数Microsoft程序员不使用 下划线,但我还是喜欢用,因为这样能使名字更具有可读性。   C++ 在C++中,关键字const不止一个意思。在前面的代码行中,它用来创建一个常 数变量。这和#define相似,但是它增加了类型信息这个特性,const不仅仅像#define一样 是一个简单的预处理文本替换,而且更像是一个变量,它允许编译器进行类型检查和替换 。 类的命名   类命名的约定可能要麻烦一点。但我也看到有很多人在使用这个约定,并独立地进行 补充。不管怎样说,所有C++的类必须以大写C为前缀,类名字的每一个子名的第一个字母 都必须大写。下面是几个例子:   class CVector   {    public    CVector();{ix=iy=yz=imagnitude = 0;}    CVector(int x,int y,int z){ix=x;iy=y;iz=z;}    .    .    private:    int ix,iy,iz;//the position of the vector    int imagnitude;//the magnitude of the vector   }; 参数的命名   函数的参数命名和标准变量命名的约定相同,但也不总是如此。例如下面例子给出了 一个函数定义:   UCHAR GetPixel(int x,int y);   这种情况下,更准确的匈牙利函数原型是:   UCHAR GetPixel(int ix,int iy);   但我认为这并没有什么两样。   最后,你甚至可能都看不到这些变量名,而仅仅看到类型,如下所示:   UCHAR GetPixel(int, int);   当然,这仅仅是原型使用的,真正的函数声明必须带有可赋值的变量名,这一点你已 经掌握了。   注意:仅仅会读匈牙利符号表示并不代表你能使用它。实际上,我进行编程工作已经 有20多年了,我也不准备为谁改变我的编程风格。因此,本书中的代码使用类匈牙利符号 表示法的编码风格,这是Win32 API造成的,在其他位置将使用我自己的风格,必须注意的 是,我使用的变量名的第一个字母没有大写,并且我还使用下划线。 世界上最简单的Windows程序   现在读者已经对Windows操作系统及其性能和基本设计问题有了一般了解,那就让我们 第一个Windows程序开始真正的Windows编程吧。   以每一种新语言或所学的操作系统来编写一个“世界你好”的程序是一个惯例,让我 们来试试,清单2.1是标准的基于DOS的“世界你好”程序。   程序清单2.1 基于DOS的“世界你好”程序   //DEMO2_1.cpp - standard version   #include <stdio.h>   //main entry point for all standard DOS/console programs   void main(void)   {    printf("/nTHERE CAN RE ONLY ONE!!!/n");   }//end main   现在让我们看一看用Windows如何编写它。   技巧 顺便说一句,如果读者想编译DEMO2_1.CPP的话,就应当用VC++或Borland编译 器实际创建一个调用内容的控制应用程序,这是一个类DOS的应用程序,只是它是32位的, 它仅以文本模式运行,但对于检验一个想法和算法是很有用的。 总是从WinMain()开始   如前面所述,所有的Windows程序都以WinMain()开始,这和简单直观的DOS程序都以M ain()开始一样。WinMain()中的内容取决于你。如果你愿意的话,可以创建一个窗口、开 始处理事件并在屏幕上画一些东西。另一方面,你可以调用几百个(或者是几千个)Win32 API函数中的一个。这正是我们将要做的。   我只想在屏幕上的一个信息框中打印一点东西。这正是一个Win32 API函数 MessageB ox()的功能。清单2.2是一个完整的、可编译的Windows程序,该程序创建和显示了一个能 够到处移动和关闭的信息框。 程序清单2.2 第一个Windows程序 //DEMO2_2.CPP - a simple message box #define WIN32_LEAN_AND_MEAN #include <windows.h>//the main windows headers #include <windowsx.h>//a lot of cool macros //main entry point for all windows programs int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hprevinstance, LPSTRlpcmdline, int ncmdshow) { //call message box api with NULL for parent window handle MessageBox(NULL,"THERE CAN BE ONLY ONE!!!", "MY FIRST WINDOWS PROGRAM", MB_OK|MB_ICONEXCLAMATION); //exit program return(0); }//end WinMain   要编译该程序,按照下面步骤:   1.创建新的Win32.EXE项目并包含CD-ROM上T3DCHAP02/下的DEMO02_2.CPP。   2.编译和联接程序。   3.运行!   你可能会以为一个基本的Windows程序有几百行代码。当你编译和运行程序时,可能会 看到如图2.5所示的内容。 程序剖析   现在已经有了下完整的Windows程序,让我们一行一行地分析程序的内容。首先第一行 程序是 #define Win32_LEAN_AND_MEAN   这个应稍微解释一下。创建Windows程序有两种方式——使用Microsoft基础类(Micro soft Foundation Classes,MFC),或者使用软件开发工具包(Software Development Kit ,SDK)。MFC完全基于C++类,要比以前的游戏编程所需的工具复杂得多,功能和难度也要强 大和复杂10倍。而SDK是一个可管理程序包,可以在一到两周内学会(至少初步学会),并且 使用了简单明了的C语言。因此,我在本书所使用的工具是SDK。   Win32_LEAN_AND_MEAN指示编译器(实际上是逻辑头文件)不包含无关的MFC操作,现在 我们又离题了,回来继续看程序。   之后,下面的头文件是:   #include "windows.h"   #include "windowsx.h"   第一个引用“windows.h”实际上包括所有的Windows头文件。Windows有许多这样的头 文件,这就有点像包含宏,可以节省许多手工包含显式头文件的时间。   第二个引用“windowsx.h”是一个含有许多重要的宏和常量的头文件,该文件可以简 化Windows编程。   下面就到最重要的部分——所有Windows应用程序的主要入口位置WinMain():   int WINAPI WinMain(HINSTANCE hinstance,             HINSTANCE hprevinstance,             LPSTR lpcmdline,             int ncmdshow);   首先,应当注意到奇怪的WINAPI声明符,这相当于PASCAL函数声明符,它强制参数从 左边向右边传递,而不是像默认的CDECL声明符那样参数从右到左转移。但是,PASCAL调用 约定声明已经过时了,WINAPI代替了该函数。必须使用WinMain()的WINAPI声明符;否则, 将向函数返回一个不正确的参数并终止开始程序。 测试参数   下面我们详细看一下每个参数   ·hinstance——该参数是一个Windows为你的应用程序生成的实例句柄。实例是一个 用来跟踪资源的指针或数。本例中,hinstance就像一个名字或地址一样,用来跟踪你的应 用程序。   ·hprevinstance——该参数已经不再使用了,但是在Windows的旧版本中,它跟踪应 用程序以前的实例(换句话说,就是产生当前实例的应用程序实例)。难怪Microsoft要去除 它,它就像一次长途跋涉——主我们为之头疼。   ·lpcmdline——这是一个空终止字符串,和标准C/C++ main(int argc,char **argv )函数中的命令行参数相似。不同的是,它不是一个单独的像argc那样指出命令行的参数。 例如,如果你创建一个名字为TEST.EXE的Windows应用程序,并且使用下面的参数运行:   TEST.EXE one   lpcmdline将含有下面数据:   lpcmdline = "one two three"   注意,.EXE的名字本身并不是命令行的一部分。   ·ncmdshow——最后一整型参数在运行过程中被传递给应用程序,带有如何打开主应 用程序窗口的信息。这样,用户便会拥有一点控制应用程序如何启动的能力。当然,作为 一个程序员,如果想忽略它也可以,而想使用它也行。(你将参数传递给ShowWindow(),我 们又超前了!)表2.2列出了ncmdshow最常用的参数值。 表2.2 cmdshow的Windows代码 值 功  能 SW_SHOWNORMAL   激活并显示一个窗口。如果该窗口最小化或最大化的话,Windows将它 恢复到原始尺寸和位置。当第一次显示该窗口时,应用程序将指定该标志。 SW_SHOW   激活一个窗口,并按当前尺寸和位置显示 SW_HIDE   隐藏一个窗口,并激活另外一个窗口 SW_MAXIMIZE   将指定的窗口最大化 SW_MINIMIZE   将指定的窗口最小化 SW_RESTORE   激活并显示一个窗口,如果该窗口最小化或最大化的话,Windows将它恢 复到原始尺寸和位置。当恢复为最小化窗口时,应用程序必须指定该标志。 SW_SHOWMAXIMIZED   激活一个窗口,并以最大化窗口显示 SW_SHOWMINIMIZED   激活一个窗口,并以最小化窗口显示 SW_SHOWMINNOACTIVE   以最小化窗口方式显示一个窗口,激活的窗口依然保持激活的状 态 SW_SHOWNA   以当前状态显示一个窗口,激活的窗口依然保持激活的状态   SW_SHOWONACTIVATE   以上一次窗口尺寸和位置来显示窗口,激活的窗口依然保持激活 的状态   如表2.2所示,ncmdshow有许多设置(目前许多值都没有意义)。实际上,这些设置大部 分都不在ncmdshow中传递。可以应用另一个函数 ShowWindow()来使用它们,该函数在一个 窗口创建时就开始显示。对此我们在本章后面将进行详细的讨论。   我想说的一点是,Windows带有大量的你从未使用过的选项和标志等等,就像VCR编程 选项一样——越多越好,任你使用。Windows就是按照这种方式设计的。这将使每个人都感 到满意,这也意味着它包含了许多选项。实际上,我们在99%时间内将会使用SW_SHOW、SW _SHOWNORMAL和SW_HIDE,但是你还要了解在1%的时间内会用到的其他选项。 选择一个信息框   最后让我们讨论一下WinMain()中调用MessageBox()的实际机制。MessageBox()是一个 Win32 API函数,它替我们做某些事,使我们不需自己去做。该函数经常以不同的图标和一 个或两个按钮来显示信息。你看,简单的信息显示在Windows应用程序中非常普通,有了这 样一个函数就节省了程序员半个多小时的时间,而不必每次使用都要编写它。   MessageBox()并没有什么多少功能,但是能够在屏幕上显示一个窗口,提出一个问题 ,并且等候用户的输入。下面是MessageBox()的原型:   int MessageBox(HWND hwn,//handle of owner window           LPCTSTR lptext,//address of text in message box           LPCTSTR lpcaption,//address of title of message box           UINT utype);//style of message box   参数定义如下:   hwnd——这是信息框连续窗口的句柄。目前我们还不能谈及窗口句柄,因此只能认为 它是信息框的父窗口。在DEMO2_2.CPP,我们将它设置为空值NULL,因此使用Windows桌面 作为父窗口。   lptext——这是一个包含显示文本的空值终止字符串。   lpcaption——这是一个包含显示文本框标题的空值终止字符串。   utype——这大概是该簇参数中唯一令人激动的参数了,控制信息显示框的各类。   表2.3列出了几种MessageBox()选项(有些删减)。   表2.3 MessageBox()选项   ─────────────────────────────────            标志  描述   ─────────────────────────────────         下列设置控制信息框的一般类型   ───────────┬─────────────────────   MB_OK        │信息框含有一个按钮:OK,这是默认值   MB_OKCANCEL     │信息框含有两个按钮:OK和Cancel   MB_RETRYCANCEL   │信息框含有两个按钮:Retry和Cancel   MB_YESNO      │信息框含有两个按钮:Yes和No   MB_YESNOCANCEL   │信息框含有三个按钮:Yes、No和Cancel   MB_ABORTRETRYIGNORE│信息框含有三个按钮:Yes、No和Cancel   ───────────┴─────────────────────        这一组控制在图标上添加一点“穷人的多媒体”   ───────────┬─────────────────────   MB_ICONEXCLAMATION │信息框显示一个惊叹号图标   MB_ICONINFORMATION │信息框显示一个由圆圈中的小写字母I构成的图标   MB_ICONQUESTION   │信息框显示一个问号图标   MB_ICONSTOP     │信息框显示一个终止符图标   ───────────┴─────────────────────          该标志组控制默认时高亮的按钮   ───────────┬─────────────────────   MB_DEFBUTTONn    │其中n是一个指示默认按钮的数字(1-4),从左              │到右计数   ───────────┴─────────────────────   注意:还有其他的高级OS级标志,我们没有讨论。如果希望了解更多细节的话,可以 通过编译器Win32 SDK的在线帮助来查阅。   可以同时使用表2.3中的值进行逻辑或运算,来创建一个信息框。一般情况下,只能从 每一组中仅使用一个标志来进行或运算。   当然,和所有Win2 API函数一样,MessageBox()函数返回一个值业通知编程者所发生 的事件。但在这个例子中谁关心这个呢?通常情况下,如果信息框是yes/no提问之类的情 况的话,就希望知道这个返回值。表2.4列出了可能的返回值。       表2.4 MessageBox()的返回值   ─────┬─────────────────────     值  │ 按钮选择   ─────┼─────────────────────   IDABORT │Abort   IDCANCEL │Cancel   IDIGNORE │Ignore   IDNO   │No   IDOK   │OK   IDRETRY │Retry   IDYES  │Yes   ─────┴─────────────────────   最后,这个表已经毫无遗漏地列出了所有的返回值,正在已经完成了对我们第一个Wi ndows程序——单击的逐行分析。   技巧:现在希望你能轻松地对这个程序进行修改,并以不同的方式进行编译,使用不 同的编译器选项,例如优化,然后尝试通过调试程序来运行该程序,看看你是否已经领会 ,做完后,请回到此处。   BOOL MessageBeep(UNIT utype);//运行声音   可以从表2.5所示常数中得到不同的声音。         表2.5 MessageBeep()函数的声音标识符   ───────────┬───────────────         值    │ 声音   ───────────┼───────────────   MB_ICONASTERISK   │系统星号   MB_ICONEXCLAMATION │系统惊叹号   MB_ICONHAND     │系统指针   MB_ICONQUESTION   │系统问号   MB_OK        │系统默认值   0xFFFFFFFF     │使用计算机扬声器的标准嘟嘟声   ───────────┴───────────────   注意:如果已经安装了MS_Plus主题曲的话,你应能得到有意思的结果。   看Win32 API多酷啊!可以有上百个函数使用。它们虽然不是世界是最快的函数,但是 对于一般的内部管理I/O和GUN来讲,它们已经很棒了。   让我们稍微花点时间总结一下我们目前所知的有关Windwos编程方面的知识。首先,W indows支持多任务/多线程,因此可以同时运行多个应用程序。我们不必费心就可以做到这 一点,我们最关系的是Windows支持事件触发。这就意味着我们必须处理事件(在这一点上 目前我们还不知如何做)并且做出反应。好,听上去不错。最后所有Windows程序都以函数 WinMain()开始,WinMain()函数中的参数要比标准DOS Main()多得多,但这些参数都属于 逻辑和推理的领域。   掌握了上述的内容,就到了编写一个真正的Windows应用程序的时候了。 真实的Windows应用程序   尽管本书的目标是编写在Windows环境下运行的3D游戏,但是你并不需要了解更多的W indows编程。实际上,你所需要的就是一个基本的Windows程序,可以打开一个窗口、处理 信息、调用主游戏循环等等。了解了这些,本章中的目标是首先向你展示如何创建简单的 Windows应用程序,同时为编写类似32位DOS环境的游戏外壳程序奠定基础。   一个Windows程序的关键就是打开一个窗口。一个窗口就是一个显示文本和图形信息的 工作区。要创建一个完全实用的Windows程序,只要进行下列工作:   1.创建一个Windows类。   2.创建一个事件句柄或WinProc。   3.用Windows注册Windows类。   4.用前面创建的Windows类创建一个窗口。   5.创建一个能够从事件句柄获得或向事件句柄传递Windows信息的主事件循环。   让我们详细了解一下每一步的工作。 Windows类   Windows实际上是一个面向对象的的操作系统,因此Windows中大量的概念和程序都出 自C++。其中一个概念就是Windows类。Windows中的每一个窗口、控件、列表框、对话框和 小部件等等实际上都是一个窗口。区别它们的就是定义它们的类。一个Windows类就是Win dows能够操作的一个窗口类型的描述。   有许多预定义的Windows类,如按钮、列表框、文件选择器等等。你也可以自己任意创 建你的Windows类。实际上,你可以为自己编写的每一个应用程序创建至少一个Windows类 。否则你的程序将非常麻烦。因此你应当在画一个窗口时,考虑一个Windows类来作为Win dows的一个模板,以便于在其中处理信息。   控制Windows类信息的数据结构有两个:WNDCLASS和WNDCLASSEX。WNDCLASS是比较古老 的一个,可以不久将废弃,因此我们应当使用新的扩展版WNDLCASSEX。二都结构非常相似 ,如果有兴趣的话,可以在Win32帮助中查阅WNDCLASS。让我们看一下在Windows头文件中 定义的WNDCLASSEX。   typedef struct _WNDCLASSEX   {   UINT cbSize;//size of this structure   UINT style;//style flags   WNDPROC lpfnWndProc;//function pointer to handler   int cbClsExtra;//extra class info   int cbWndExtra;//extra window info   HANDLE hInstance;//the instance of the application   HICON hIcon;//the main icon   HCURSOR hCursor;//the cursor for the window   HBRUSH hbrBackground;//the background brush to paint the window   LPCTSTR lpszMenuName;//the name of the menu to attach   LPCTSTR lpszClassName;//the name of the class itself   HICON hIconSm;//the handle of the small icon   } WNDCLASSEX   因此你所要做的就是创建一个这样的结构,然后填写所有的字段:   WNDCLASSEX winclass;//a blank windows class   第一个字段cbSize非常重要(但Petzold在《Programming Windows 95》忘记了这个内 容),它是WNDCLASSEX结构本身的大小。你可能要问,为什么应当知道该结构的大小?这个 问题问得好,原因是如果这个结构作为一个指针被传递的话,接收器首先构件第一个字段 ,以确定该数据导体最低限度有多大。这有点像提示和帮助信息,以便于其他函数在运行 时不必计算该类的大小。因此,我们应当这样做:   winclass.cbSize = sizeof(WNDCLASSEX);   第二个字段包含描述该窗口一般属性的结构信息标志。有许多这样的标志,因此我们 不能全部列它们。只要能够使用它们创建任何类型的窗口就行了。表2.6列出了常用的标志 。读者可以任意对这些值进行逻辑“或”运算,来派生所希望的窗口类型。          表2.6 Windows在的类型标志   ──────┬──────────────────────────    标志   │ 说明   ──────┼──────────────────────────   CS_HREDRAW │若移动或改变了窗口宽度,则刷新整个窗口   CS_VREDRAW │若移动或改变了窗口高度,则刷新整个窗口   CS_OWNDC  │为该类中每个窗口分配一个单值的设备描述表(在本章后         │面详细描述)   CS_DBLCLKS │当用户双击鼠标时向窗口程序发送一个双击的信息,同时,         │光标位于属于该类的窗口中   CS_PARENTDC│在母窗口中设定一个子窗口的剪切区,以便于子窗口能够         │画在母窗口中   CS_SAVEBITS│在一个窗口中保存用户图像,以便于在该窗口被遮住、移         │动时不必每次刷新屏幕。但是,这样会占用更多的内存,         │并且比人工同样操作要慢得多   CS_NOCLOSE │禁止系统菜单上的关闭命令   ──────┴──────────────────────────   注意:用黑体显示的部分为最常用的标志   表2.6包含了大量的标志,即使读者对此尚有疑问,也无关紧要。现在,设定类型标识 符,描述如果窗口移动或改变尺寸就进行屏幕刷新,并可以获得一个静态的设备描述表以 及处理双击事件的能力。   我们将在第三章“高级Windows编程”中详细讨论设备描述表,但基本说来,它被用作 窗口中图像着色的数据结构。因此如果你要处理一个图像,就应为感兴趣的特定窗口申请 一个设备描述表。如果设定了一个Windows灰,它就通过CS_OMNDC得到了一个设备描述表, 如果你不想每次处理图像时都申请设备描述表,可以将它保存一段时间。上面说的对你有 帮助还是使你更糊涂?Windows就是这样——你知道得越多,问题就越多。旭了!下面说一 下如何设定类型字段:   winclass.style = CS_VERDRAW | CS_HREDRAW | CS_OWNDC | CS_DBLCLICKS;   WNDCLASSEX结构的下一个字段lpfnWndProc是一个指向事件句柄的函数指针。基本上这 里所设定的都是该类的回调函数。回调函数在Windows编程中经常使用,工作原理如下:当 有事件发生时,Windows通过调用一个你已经提供的回调函数来通知你,这省去你盲目查询 的麻烦。随后在回调函数中,再进行所需的操作。   这个过程就是基本的Windows事件循环和事件句柄的操作过程。向Windows类申请一个 回调函数(当然需要使用特定的原型)。当一个事件发生时,Windows按如图2.6所示的那样 替你调用它。关于该项内容我们将在下面部分进行更详细的介绍。但是现在,读者只要将 其设定到你将编写的事件函数中去:   winclass.lpfnWndProc = WinProc;//this is our function   -----------(图2.6 windows事件句柄回调函数的操作流程 略)   C++ 函数指针有点像C++中的虚函数。如果你对它们不熟悉的话,在这里讲一上,假设 有两个函数用以操作两个数:   int Add(int op1,int op2) {return(op1+op2);}   int Sub(int op1,int op2) {return(op1-op2);}   要想用同一调用来调用两个函数,可以用一个函数指针来实现,如下:   //define a function pointer that takes two int and returns an int   int (Math*)(int,int);   然后可以如下配置函数指针:   Math = Add;   int result = Math(1,2);//this reslly calls Add(1,2)   //result will be 3   Math = Sub;   int result = Math(1,2);//this really calls Sub(1,2)   //result will be -2   看,不错吧。   下面两个字段,cbClsExtra和cbWndExtra原是为指示Windows将附加的运行时间信息保 存到Windows类某些单元中而设计的。但是绝大多数人使用这些字段并简单地将其值设为0 ,如下所示:   winclass.cbClsExtra = 0;//extra class info space   winclass.cbWndExtra = 0;//extra window info space   下一个是hInstance字段。这是一个简单的、在启动时传递给WinMain()函数的句柄实 例,因此只需简单地从WinMain()中复制即可:   winclass.hInstance = hinstance;//assign the application instance   剩下的字段和Windows类的图像方面有关,在讨论它们之前,先花一点时间回顾一下句 柄。   在Windows程序和类型中将一再看到句柄:位图句柄、光标句柄、任意事情的句柄。请 记住,句柄只是一个基于内部Windows类型的标识符。其实它们都是整数。但是Microsoft 可能改变这一点,因此安全使用Microsoft类型是个好主意。总之,你将会看到越来越多的 “[...]句柄”,请记住,有前缀h的任何类型通常都是一个句柄。好,回到原来的地方继 续吧。   下一个字段是设定表示应用程序的图标的类型。你完全可以装载一下你自己定制的图 标,但现在你使用系统图标,需要为它设置一个句柄。要为一个常用的系统图标检索一个 句柄,可以使用LoadIcon()函数:   winclass.hIcon = loadIcon(NULL,IDI_APPLICATION);   这行代码装载一个标准的应用程序图标——虽然烦人,但是简单。如果对LoadIcon() 函数有兴趣的话,请看下面的它的原型,表2.7给出了几个图标选项: HICON LoadIcon(HINSTANCE hInstance,//handle of application instance LPCTSTR lpIconName;//icon-name string or icon resource identifier   hInstance是一个从应用程序装载图标资源的实例(后面将详细讨论)。现在将它设置为 NULL来装载一个标准的图标。LpIconName是包含被装载图标资源名称的NULL终止字符串。 当hInstance为NULL时,IpIconName的值如表2.7所示。            LoadIcon()的图标标识符   ─────────┬───────────────────────    值       │ 说明   ─────────┼───────────────────────   IDI_APPLICATION │默认应用程序图标   IDI_ASTERISK   │星号   IDI_EXCLAMATION │惊叹号   IDI_HAND     │手形图标   IDI_QUESTION   │问号   IDI_WINLOGO   │Windows徽标   ─────────┴───────────────────────   好,现在我们已经介绍了一半的字段了。做个深呼吸休息一会,让我们进行下一个字 段hCursor的介绍。和hIcon相似,它也是一个图像对象句柄。不同的是,hCursor是一个指 针进入到窗口的用户区才显示的光标句柄。使用LoadCursor()函数可以得到资源或预定义 的系统光标。我们将在后面讨论资源,简单而言资源就是像位图、光标、图标、声音等一 样的数据段,它被编译到应用程序中并可以在运行时访问。Windows类的光标设定如下所示 :   winclass.hCursor = LoadCursor(NULL,IDC_ARROW);   下面是LoadCursor()函数的原型(表2.8列出了不同的系统光标标识符):   HCURSOR lpCursor(HINSTANCE hInstance,//handle of application instance   LPCTSTR lpCursorName);//icon_name string or icon resource identifier   hInstance是你的.EXE的应用程序实例。该.EXE应用程序包含订制光标名称来源的资源 。但现在读者还不能使用该功能,仅将默认的系统光标值设定为NULL。   lpCursorName标识了资源名字符串或资源句柄(我们一般不使用),或者是一个常数, 以标识如表2.8中所示的系统默认值。            表2.8 LoadCursor()的值   ────────┬────────────────────────    值      │ 说明   ────────┼────────────────────────   IDC_ARROW   │标准箭头   IDC_APPSTARTING│标准箭头和小沙漏标   IDC_CROSS   │横标线   IDC_IBEAM   │文本I型标   IDC_NO     │带正斜线的圆圈   IDC_SIZEALL  │四向箭头   IDC_SIZENESW │指向东北-西南方向的双向箭头   IDC_SIZENS  │指向南北方向的双向箭头   IDC_SIZENWSE │指向东南-西北方向的双向箭头   IDC_SIZEWE  │指向东西方向的双向箭头   IDC_UPARROW  │垂直方向的箭头   IDC_WAIT    │沙漏   ────────┴────────────────────────   现在我们就要解放了,因为我们几乎已经全部介绍完了——剩下的字段更有意义。让 我们看一看hbrBackground。   无论在什么时候绘制或刷新一个窗口,Windows都至少将以用户预定义的颜色或Windo ws内部设置的画笔颜色填充该窗口的背景。因此,hbrBrackground是一个用于窗口刷新的 画笔句柄。画笔、笔、色彩和图形都时GDI(图形设备接口)的一部分,我们将在下一章中详 细讨论。现在,介绍一下如何申请一个基本的系统画笔来填充窗口。该项功能由GetStock Object()来实现,如下面程序所示:   winclass.hbrBackground = GetStockObject(WHITE_BRUSH);   GetStockObject()是一个通用函数,用于获得Windows系统画笔、笔、调色板或字体的 一个句柄。GetStockObject()只有一个参数,用来指示装载哪一项资源。表2.9仅列出了画 笔和笔的可能库存对象。   表2.9 GetStockObject()的库存对象标识符   ────────┬────────────────────────    值      │ 说明   ────────┼────────────────────────   BLACK_BRUSH  │黑色画笔   WHITE_BRUSH  │白色画笔   GRAY_BRUSH   │灰色画笔   LTGRAY_BRUSH  │淡灰色画笔   DKGRAY_BRUSH  │深灰色画笔   HOLLOW_BRUSH │空心画笔   NULL_BRUSH  │无效(NULL)画笔   BLACK_PEN   │黑色笔   WHITE_PEN   │白色笔   NULL_PEN    │无效(NULL)笔   ────────┴────────────────────────   WNDCLASS 结构中的下一个字段是lpszMenuName。它是菜单资源名称的空终止ASCII字 符串,用于加载和选用窗口。其工作原理将在第三章“高级Windows编程”中讨论。现在我 们只需要将值设为NULL。   winclass.lpszmenuNume = NULL://the name of the menu to attach   如我刚提及的那样,每个Windows类代表你的应用程序所创建的不同窗口类型。在某种 程序上,类与模板相似,Windows需要一些途径来跟踪和识别它们。因此,下一个字段lps zClassName,就用于该目的。该字段被赋以包含相关类的文本标示符的空终止字符串。我 个人喜欢用诸如“WINCLASS1”、“WINCLASS2”等标示符。读者以自己喜好而定,以简单 明了为原则,如下所示:   Winclass.lpszClassName="WINCLASS1"//the name of the class itself   这样赋值以后,你可以使用它的名字来引用这个新的Windows类——很酷,是吗?   最后就是小应用程序图表。这是Windows类WINCLASSEX中新增加的功能,在老版本WND CLASS中没有。首先,它是指向你的窗口标题栏和Windows桌面任务样的句柄。你经常需要 装载一个自定义资源,但是现在只要通过LoadIcon()使用一个标准的Windows图标即可实现 :   winclass.hIconSm = LoadIcon(NULL,IDI_APPLICATION);//小图标句柄   下面让我们迅速回顾一下整个类的定义:   WNDCLASSEX winclass;//this will hole the class we create   //first fill in the window class structure   winclass.cbSize = sizeof(WNDCLASSEX);   winclass.style = CS_DBLCLKS | CS_OWNDC | CS_HREDRAW | CS_VREDRAW;   winclass.lpfnWndProc = WindowProc;   winclass.cbClsExtra = 0;   winclass.cbWndExtra = 0;   winclass.hInstance = hinstance;   winclass.hIcon = LoadIcon(NULL,IDI_APPLICATION);   winclass.hCursor = LoadCursor(NULL,IDC_ARROW);   winclass.hbrBacground = GetStockObject(BLACK_BRUSH);   winclass.lpszMenuName = NULL;   winclass.lpszClassName = "WINCLASS";   winclass.hIconSm = LoadIcon(NULL,IDI_APPLICATION);   当然,如果想节省一些打字时间的话,可以像下面这样简单地初始化该结构:   WNDCLASSEX winclass = {     winclass.cbSize = sizeof(WNDCLASSEX);     CS_DBLCLKS | CS_OWNDC | CS_HREDRAW | CS_VREDRAW,     WindowProc,     0,     0,     hinstance,     LoadIcon(NULL,IDI_APPLICATION),     LoadCursor(NULL,IDC_ARROW),     GetStockObject(BLACK_BRUSH),     NULL,     "WINCLASS1",     LoadIcon(NULL,IDI_APPLICATION)};   这样就省去了许多输入! 注册Windows类   现在Windows类已经定义并且存放在winclass中,必须将新的类通知Windows。该功能 通过RegisterClassEx()函数,使用一个指向新类定义的指针来完成,如下所示:   RegisterClassEx(&winclass);   警告:注意我并没有使用我们例子中的“WINCLASS1”的类名,对于RegisterClassEx ()来讲,必须使用保存该类的实际结构,因为在该类调用RegisterClassEx()函数之前,W indows并不知道该类的存在,明白了吧?   此外还有一个旧版本的RegisterClass()函数,用于注册基于旧结构WINCLASS基础上的 类。该类一旦注册,我们就可以任意创建它的窗口。请看下面如何进行这个工作,然后再 详细看一下事件句柄和主事件循环,了解使一个Windows应用程序运行还要做哪些工作。 创建窗口   要创建一个窗口(劳动者一个类窗口的对象),使用CreateWindow()或CreateWindowEx ()函数。后者是更新一点的版本,支持附加类型参数,我们就使用它。该函数是创建Wind ows类的函数,我们要多花一点时间来逐行分析。在创建一个窗口时。必须为这个Windows 类提供一个正文名——我们现在就使用“WINCLASS1”命名。这是识别该Windows类并区别 于其他类以及内嵌的诸如按钮、文本框等类型的标示。   下面是CreateWindowEx()函数的原型:   HWND CreateWindowEx(    DWORD dwExStyle,//extended window style    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,//widow height    HWND hWndParent,//handle to parent or owner window    HMENU hMenu,//handle to menu,or child-window identifier    HINSTANCE hInstance,//handle to application instance    LPVOID lpParam);//pointer to window-creation data   如果该函数执行正确的话,将返回五个指向新建窗口的句柄;否则就返回空值NULL。 上述大多数参数是不需要加以说明的,现在让我们浏览一下:   ·dwExStyle——该扩展类型标志具有高级特征,大多数情况下,可以设为NULL。如果 读者对其取值感兴趣的话,可以查阅Win32 SDK帮助,上面有详细的有关该标识符取值的说 明。WS_EX_TOPMOST是我唯一使用过的一个值,该功能使窗口一直保持在上部。   ·lpClassName——这是你所创建的窗口的基础类名——例如“WINCLASS1”。   ·lpWindowName——这是包含窗口标题的空终止文本字符串——例如“我的第一个窗 口”。   ·dwStyle——这是一个说明窗口外观和行为的通用窗口标志——非常重要!表2.10列 出了一些最常用的值。当然,可以任意组合使用这些值来得到希望的各种特征。   ·x,y——这是该窗口左上角位置的像素坐标。如果你无所谓,可使用CW_USEDEFALT ,这将由Windows来决定。   ·nWidth,nHeight——这是以像素表示的窗口宽度和高度。如果你无所谓,可使用C W_USEDEFAULT,这将由Window来决定。   ·hWndParent——假如父窗口存在,这是指向父窗口的句柄。如果没有父窗口,桌面 就是父窗口。   ·hMenu——这是指向附属于该窗口菜单的句柄。下一章中将详细介绍,现在将其赋值 NULL。   ·hInstance——这是应用程序实例,这里从WinMain()中使用实例。   ·lpParam——高级特征,设置为NULL。   表2.10列出了各种窗口标志设置。            表2.10 dwStyle的通用类型值   ──────────┬──────────────────────    类型       │ 所创建的内容   ──────────┼──────────────────────   WS_POPUP      │弹出式窗口   WS_OVERLAPPED   │带有标题栏和边界的重叠式窗口,类似WS_TILED类             │型   WS_OVERLAPPEDWINDOW│具有WS_OVERLAPPED、WS_CAPTION、WS_SYSMENU、             │WS_THICKFRAME、WS_MAXIMIZEBOX和             │WS_MINIMIZEBOX样式的重叠式窗口   WS_VISIBLE    │开始就可见的窗口   WS_SYSMENU    │标题栏上有窗口菜单的窗口   WS_BORDER     │有细线边界的窗口   WS_CAPTION    │有标题栏的窗口(包括WS_BORDER样式)   WS_ICONIC     │开始就最小化的窗口,类似WS_MINIMIZE样式   WS_MAXIMIZE    │开始就最大化的窗口   WS_MAXIMIZEBOX│具有最大化按钮的窗口。不能和WS_EX_CONGTEXTHELP          │样式合并。WS_SYSMENU也必须指定   MS_MINIMIZE  │开始就最小化的窗口,类似WS_ICONIC样式   WS_MINIMIZEOBX│具有最小化按钮的窗口。不能和WS_EX_CONGTEXITHELP          │样式合并。WS_SYSMENU也必须指定   WS_POPUPWINDOW  │带有WS_BORDER、WS_POPUP和WS_SYSMENU类型的             │弹出式窗口   WS_SIZEBOX   │一个窗口边界可以变化,和WS_THICKFRAME类型相同   WS_HSCROLL    │带有水平滚动条的窗口   WS_VSCROLL    │带有垂直滚动条的窗口   ──────────┴──────────────────────   注意:用黑体显示的是经常使用的值。   下面是使用标准控件在(0,0)位置创建一个大小为400×400像素的、简单的重叠式窗口 。   HWND hwnd;//window handle   //create the window,bail if problem   if(!(hwnd = CreateWindowEx(NULL,//extended style    " WINCLASS";//class    " Your Basic Window",//title    WS_OVERLAPPEDWINDOW|WS_VISIBLE;    0,0 //initial x,y    400,400 /initital width,height    NULL,//handle to parent    NULL,//handle to menu    hinstance,//instance of this application    NULL)))//extra creation parms   return(0);   一旦创建了该窗口,它可能是可见或不可见的。但是,在这个例子中,我们增加了自 动显示的类型标识符WS_VISIBLE。如果没有添加该标识符,则调用下面的函数来人工显示 该窗口:   //this shows the window   ShowWindow(hwnd,ncmdshow);   记住WinMain()中的ncmdshow参数吗?这就是使用它的方便之处。尽管我们使用WS_VI SIBLE覆盖了ncmdshow参数,但还是应将其作为一个参数传递给ShowWindow()。下面让Win dows更新窗口的内容,并且产生了一个WM_PAINT信息,这通过调用函数UpdateWindow()来 完成:   //this sends a WM_PAINT message to window and makes   //sure the contents are refreshed   UpdateWindow();  事件处理程序   我并不了解你的情况,但注意我现在正使你掌握Windows的核心。它有如一本神秘小说 。请记住,我所说的事件处理程序就是当事件发生时Windows从主事件循环调用的回调函数 。回顾一下图2.6,刷新一下你对通用数据流的印象。   事件处理器由读者自己编写,它能够处理你所关心 的所有事件,蓁的工作就交给wi ndows处理。当然,请记住,你的应用程序所能处理的事件和消息越多,它的功能越强。   在编写程序之前,让我们讨论一下事件处理器的一些细节,即事件能做什么,工作机 理如何。首先,对于创建的一个Windows类,都有一个独立的事件处理器,我指的是Windo ws' procedure,从现在开始简称WinProc。当收到用户或Windows发送的消息并放在主事件 序列中时,WindProc就接收到主事件循环发送的消息。这简单是一个智力绕口令,我换个 方式来说明……   当用户和Windows运行任务时,你的窗口和/或其他应用程序窗口产生事件和消息。所 有消息都进入一个队列,而你窗口的消息发送到你的窗口专用队列中。然后主事件循环检 索这些消息,并且将它们发送到你的窗口的WinProc中来处理。   这几乎有上而个可能的消息和变量,因此,我们就不全部分析了。值得调研员幸的是 ,你只需处理很少的消息和变量,就可以启动并运行Windows应用程序。   简单地说,主事件循环将消息和事件反馈到WinProc,WinProc对它们进行处理。因此 不仅你要关注WinProc,主事件循环同样也要关心WinPorc。现在我们简单地了解一下WinP orc,现WinProc只接收消息。   正在来看一下WinProc的工作机掉,让我们看一下它的原型:   LRESULT CALLBACKWindowProc{     WHND hwnd,//window handle of sender     UINT msg,//the message id     WPARAM wparam,//further defines message     LPARAM lparam);//further defines message   当然,这仅仅是回调函数的原型。只要将函数地址作为一个函数指针传递给winclass .lpfnWndProc,就可以调用该函数的任何信息,如下如示:   winclass.lpfnWndProc = WindowProc;   参数的含义是不言自明的:      ·hwnd——这是一个Windows句柄,只有当你使用同一个Windows打开多个窗口时它才 用到。这种情况下,hwnd是表明消息来自哪个窗口唯一途径。图2.7表示了这种情况。   ·msg——这是一个实际的WinProc处理的消息标识符。这个标识符可以是众多主要消 息中的一个。   ·Wparam和lparam——进一步限定或细发送到msg参数中的信息。   最后,我们感兴趣的是返回类型LRESULT和声明说明符CALLBAK。这些关键字都是必需 的,不能忽略它们。   因此大多数人所要做的就是使用switch()来处理msg所表示的消息,然后为每一种情况 编写代码。在msg基础上,你可以知道是否需要进一步求wparam和/或lparam的值。很酷吗 ?因此让我们看一下由WinProc传递过来的所有可能的消息,然后看一下WinProc的工作机 理。表2.11简单列出了一些基本的消息说明符。   ────────┬────────────────────────    类型     │ 所创建的内容   ────────┼────────────────────────   WN_ACTIVATE  │当窗口被激活或者成为一个焦点时传递   WM_CLOSE    │当窗口关闭时传递   WM_CREATE   │当窗口第一次创建时传递   WM_DESTROY  │当窗口可能要被破坏时传递   WM_MOVE    │当窗口移动时传递   WM_MOUSEMOVE │当移动鼠标时传递   WM_KEYUP   │当松开一个键时传递   WM_KEYDOWN  │当按钮一下键时传递   WM_TIMER   │当发生定时程序事件时传递   WM_USER   │允许传递消息   WM_PAINT   │当一个窗口需重画时传递   WM_QUIT   │当Windows应用程序最后结束时传递   WM_SIZE   │当一个窗口改变大小时传递   ───────┴─────────────────────────   要认真看表2.11,了解所有消息的功能。在应用程序运行时将有一个或多个上述消息 传递到WinProc。消息说明符本身在msg中,而其他停止都存储在wparam和lparam中。因此 参考在线Win32SDK帮助来了解某个消息的参数所代表的意思是个不错的方法。   幸好我们现在只对下面三个消息感兴趣:   ·WM_CREATE——当窗口第一次创建时传递该消息,以便你进行启动、初始化或资源配 置工作。   ·WM_PAINT——当一个窗口内容需要重画时传递该消息。这可能有许多原因:用户移 动窗口或改变其尺寸、弹出其他应用程序而遮挡了你的窗口等。   ·WM_DESTROY——当窗口可能要被破坏时该消息传递到你的窗口。通常 这是由于用 户单击该窗口的关闭按钮,或者是从该窗口的系统菜单中关闭该窗口造成的。无论上述哪 一种方式,都应用释放所有的资源,并且通过发送一个WM_QUIT消息来通知Windows完全终 止应用程序。后面还将详细介绍。   OK!让我们看一看WinPorc处理这些消息的整个过程。   LRESULT CALLBACK WindowProc(HWND hwnd,                  UINT msg,                  WPARAM wparam,                  LPARAM lparam);   {//this if the main message haneler of the system    PAINTSTRUCT ps;//Used in WM_PAINT    HDC hdc;//handle to a device context    //what is the message    switch(msg)    {     case WM_CREATE;     {      //do initialization stuff here      //return success      return(0)     }break;     case WM_PAINT;     {      //simply validate the window      hdc = BeginPaint(hwnd,&ps);      //you woudle do all your painting here      EndPaint(hwnd,&ps);      //return success      return(0)     }break;     case WM_DESTROY;     {      //Kill the application,this sends a WM_QUIT message      PostQuitMessage(0);      //return success      return(0)     }break;     default:break;    }//end switch    //process any message that we didn't take care of    return (DefWindowProc(hwnd,msg,wparam,lparam));   }//end WinProc   由上面可以看到,函数的大部分是由空白区构成——这真是件好事件。让我们就以WM _CREATE处理程序开始吧。该函数所作的一切就是return(0)。这就是通知Windows由编程人 员自己处理该函数,因此就无需更多的操作。当然,也可以在WM_CREATE消息中进行全部的 初始化工作,但那是你的事了。   下一个消息WM_PAINT非常重要。该消息在窗口需要重画时被发送。一般来说这表示你 应当进行重画工作。对于DirectX游戏来说,这并不是件什么大事,因为你将以30到60帧/ 秒的频率来重画屏幕。但是对于标准Windows应用程序来说,它就是件大事了。我将在后面 章节中更详细地介绍WM_PAINT,目前的功能就是通知Windows,你要重画该窗口了,因此就 停止发送WM_PAINT消息。   要完成该功能,你必须激活该窗口的窗户区。有许多方法可以做到,但调用函数Begi nPaint()和EndPaint()最简单。这一对调用将激活窗口,并使用原先存储在Windows类中的 变量hbrbackground的背景刷来填充背景。下面是程序代码。   //begin painting   hdc = BdginPaint(hwnd,&ps);   //you would do all your painting here   EndPaint(hwnd,&ps);   下面要提醒几件事情。第一,请注意,每次调用的第一个参数是窗口句柄hwnd。这是 一个非常必要的参数,因为BeginPaint——EndPaint函数能够在任何应用程序窗口中绘制 ,因此该窗口句柄指示了要重画哪个窗口。第二个参数是包含必须重画矩形区域的PAINTS TRUCT结构的地址。下面是PAINTSTRUCT结构:   typedef struct tagPAINTSTRUCT   {   HDC hdc;   BOOL fErase;   RECT rcPaint;   BOOL fRestore;   BOOL fIncUpdate;   BYTE rgbReserved[32];   } PAINTSTRUCT;   实际上不需要关心这个函数,当我们讨论图形设备接口时会再讨论这个函数。其中最 重要的字段就是rcPaint。图2.8表示了这个字段的内容。注意Windows一直尽可能地谋略作 最少的工作,因此当一个窗口内容破坏之后,Windows至少会告诉你要恢复该内容并能够重 画的最小的矩形。如果你对矩形结构感兴趣的话,会发现只有矩形的四个角是最重要的, 如下所示:   typedef struct tagRECT   {   LONG left;//left x-edge of rect   LONG top;//top yoedge of rect   LONG right;//right x-edge of rect   LONG bottom;//bottom y-edge of rect   }//RECT;   调用BeginPaint()函数应注意的最后一件事情是,它返回一个指向图形环境或hdc的句 柄:   HDC hdc;//handle to graphics context   hdc = BeginPaint(hwnd,&ps);   图2.8 仅重新绘制无效区   图形环境就是描述视频系统和正在绘制表面的数据结构。奇妙的是,如果你需要绘制 图形的话,只要获得一个指向图形环境的句柄即可。这便是关于WM_PAINT消息内容。   WM_DESTROY消息实际上非常有意思。WM_DESTROY在用户关闭窗口时被发送。当然仅仅 是关闭窗口,而不是关闭应用程序。应用程序继续运行,但是没有窗口。对此要进行一些 处理。大多数情况下,当用户关闭主要窗口时,也就意味着要关闭该应用程序。因此,你 必须通过发送一个消息来通知系统。该消息就是WM_QUIT。因为该消息经常使用,所以有一 个函数PostQuitMessage()来替你完成发送工作。   在WM_DESTROY处理程序中你所要做的就是清除一切,然后调用PostQuitMessage(0)通 知Windows终止应用程序。接着将WM_QUIT置于消息队列,这样在某一个时候终止主事件循 环。   在我们所分析的WinProc句柄中还有细节应当了解。首先,你肯定注意到了每个处理程 序体后面的return(0)。它有两个目的:退出WinPorc以及通知Windows你已处理的信息。第 二个重要的细节是默认消息处理程序DefaultWindowProc()。该函数是一个传递Windows默 认处理消息的传递函数。因此,如果不处理该消息的话,可通过如下所示的调用来结束你 的所有事件处理函数:   //porcess any message that we didn't take card of   return (DefWindowProc(hwnd,msg,wparam,lparam));   我认为代码本身过多并且过于麻烦。然而,一旦你有了一个基本Windows应用程序架构 的话,你只要将它复制并在其中添加你自己的代码就行了。正如我所说的那样,我的主要 目标是帮助你创建一个可以使用的类DOS32的游戏操作台,并且几乎忘记了任何正在运行的 Windows工作。让我们转到下一部分——主事件循环。 主事件循环   最难的一部分终于结束了。我正要脱口而出:主事件循环太简单了。下面讨论一下:   //enter main event loop   while(GetMessage(&msg,NULL,0,0))   {   //translate any accelerator keys   TranslateMessage(&msg);   //send the message to the window proc   DispatchMessage(&msg);   }//end while   这是什么?OK!让我们来研讨一下。只要GetMessage()返回一个非零值,主程序whil e()就开始执行。GetMessage()是主事件循环的关键代码,其唯一的用途就是从事件队列中 获得消息,并进行处理。你会注意到GetMessage()有四个参数。第一个参数对我们非常重 要,而其余的参数都可以设置为NULL或0。下面列出其原型,以供参考:   BOOL GetMessage{    LPMSG lpMsg,//address of structure with message    HWND hWnd,//handle of window    UINT wMsgFileterMin,//first message    UINT wMsgFileterMax);//last message   msg参数是Windows放置下一个消息的存储器。但是和WinProc()的msg参数不同的是, 该msg是一修复杂的数据结构,而不仅仅是一个整数。当一个消息传递到WinProc时,它就 被处理并分解为各个组元。MSG的结构如下所示:   typedef struct tagMSG   {   HWND hwnd,//window where message occurred   UINT message;//message id itself   WAPARAM wParam;//sub qualifies message   LPARAM lParam;//sub qualifies message   DWORD time;//time if message event   POINT pt;//position of mouse   }MSG;   看出点眉目来了,是吗?注意所有向WinPorc()传递的参数都包含在该结构中,还包括 其他参数,如事件发生时的时间和鼠标的位置。   GetMessage()从时间序列中获得下一个消息,然后下一个被调用的函数就是Translat eMessage()。TranslateMessage()是一个虚拟加速键转换器——换句话说就是输入工具。 现在只是调用它,不必管其功能。最后一个函数DispatchMessage()指出所有操作发生的位 置。当消息被GetMessage获得以后,由函数TranslateMessae()稍加处理和转换,通过函数 DispatchMessage()调用WinProc进行进一步的处理。   DispatchMessage()调用WinProc,并从最初的MSG结构中传递适当的参数。图2.9表示 了整个处理过程的最后部分。   这样,你就成了Windows专家了!如果你已经理解了上面刚刚讨论过的概念以及事件循 环、事件处理程序等等的重要性,那你至少已经掌握了90%的内容了。剩下的就是一些细节 问题了。   程序清单2.3是一个完整的Windows程序,内容是创建一个窗口。并等候关闭。   程序清单2.3 一个基本的Windows程序 -------------------- // DEMO2_3.CPP - A complete windows program // INCLUDES /// #define WIN32_LEAN_AND_MEAN // just say no to MFC #include <windows.h> // include all the windows headers #include <windowsx.h> // include useful macros #include <stdio.h> #include <math.h> // DEFINES // defines for windows #define WINDOW_CLASS_NAME "WINCLASS1" // GLOBALS // FUNCTIONS // LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { // this is the main message handler of the system PAINTSTRUCT ps; // used in WM_PAINT HDC hdc; // handle to a device context // what is the message switch(msg) { case WM_CREATE: { // do initialization stuff here // return success return(0); } break; case WM_PAINT: { // simply validate the window hdc = BeginPaint(hwnd,&ps); // you would do all your painting here EndPaint(hwnd,&ps); // return success return(0); } break; case WM_DESTROY: { // kill the application, this sends a WM_QUIT message PostQuitMessage(0); // return success return(0); } break; default:break; } // end switch // process any messages that we didn't take care of return (DefWindowProc(hwnd, msg, wparam, lparam)); } // end WinProc // WINMAIN int WINAPI WinMain( HINSTANCE hinstance, HINSTANCE hprevinstance, LPSTR lpcmdline, int ncmdshow) { WNDCLASSEX winclass; // this will hold the class we create HWND hwnd; // generic window handle MSG msg; // generic message // first fill in the window class stucture winclass.cbSize = sizeof(WNDCLASSEX); winclass.style = CS_DBLCLKS | CS_OWNDC | CS_HREDRAW | CS_VREDRAW; winclass.lpfnWndProc = WindowProc; winclass.cbClsExtra = 0; winclass.cbWndExtra = 0; winclass.hInstance = hinstance; winclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); winclass.hCursor = LoadCursor(NULL, IDC_ARROW); winclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); winclass.lpszMenuName = NULL; winclass.lpszClassName = WINDOW_CLASS_NAME; winclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION); // register the window class if (!RegisterClassEx(&winclass)) return(0); // create the window if (!(hwnd = CreateWindowEx(NULL, // extended style WINDOW_CLASS_NAME, // class "Your Basic Window", // title WS_OVERLAPPEDWINDOW | WS_VISIBLE, 0,0, // initial x,y 400,400, // initial width, height NULL, // handle to parent NULL, // handle to menu hinstance,// instance of this application NULL))) // extra creation parms return(0); // enter main event loop while(GetMessage(&msg,NULL,0,0)) { // translate any accelerator keys TranslateMessage(&msg); // send the message to the window proc DispatchMessage(&msg); } // end while // return to Windows like this return(msg.wParam); } // end WinMain /// --------------------   要编译DEMO2.3.CPP,只需创建一个Win32环境下的.EXE应用程序,并且将DEMO2_3.CP P添加到项目中即可。假如你喜欢的话,可以直接从CD-ROM上运行预先编译好的程序DEMO2 _3.EXE。图2.10显示了运行中的该程序。   图2.10运行中的DEMO2_3.EXE   在进行下一部分内容之前,我还有事情要说。首先,如果你认真阅读了事件循环的话 ,会发现它看上去并不是个实时程序。也就是说,当程序在等待通过GetMessage()传递的 消息的同进,主事件循环基本是锁定 的。这的确是真的:你必须以各种方式来避免这种现 象,因为你需要连续地运行你的游戏处理过程,并且在Windows事件出现时处理这些事件。 产生一个实时事件循环   这种实时的无等侯的事件循环很容易实现.你所需要的就是一种测试在消息序列中是否 有消息的方法。如果不,你就处理它;否则,继续处理其他的游戏逻辑并重复进行。运行 的测试函数是PeekMessage()。其原形几乎和GetMessage()相同,如下所示:   BOOL PeekMessage{    LPMSG lpMag,//pointer to structeure for message    HWND hWnd,//handle to window    UINT wMsgFilterMin,//first message    UINT wMsgFilterMax,//last message    UINT wRemoveMsg);//removal flags   如果有可用消息的话返回值非零。   区别在于最后一个参数,它控制如何从消息序列中检索消息。对于wRemoveMsg,有效的 标志有:   ·PM_NOREMOVE——PeekMessage()处理之后,消息没有从序列中去除。   ·PM_REMOVE——PeekMessage()处理之后,消息已经从序列中去除。   如果将这两种情况考虑进去的话,你可以做出两个选择:如果有消息的话,就使用Pe ekMessage()和PM_NOREMOVE,调用GetMessate();另一种选择是:使用PM_REMOVE。如果有 消息则使用PeekMessage()函数本身来检索消息。一般使用后一种情况。下面是核心逻辑的 代码,我们在主事件循环中消作改动以体现这一新技术:   while(TRUE)   {    //test if there is a message in queue,if so get it    if(PeekMessage(&msg,NULL,0,0,PM_REMOVE)    {     //test if this a quit     if(msg.message == WM_QUIT)      break;     //translate any accelerator keys     TranslateMessage(&msg);     //send the message to the window proc     DispatchMessage(&msg);    }//end if    //main game processing goes here    Game_Main();   }//end while   我已经将程序中的重要部分用黑体显示。黑体的第一部分内容是:   if(msg.message == WM_QUIT) break;   下面是如何测试从无限循环体while(true)中退出。请记住,当在WinProc中处理WM_D ESTROY消息时,你的工作就是通过调用PostQuitMessage()函数来传递WM_QUIT消息。WM_Q UIT就在事件序列中慢慢地移动,你可以检测到它,所以可以跳出主循环。   用黑体显示的程序最后一部分指出调用主游戏程序代码循环的位置。但是请不要忘记 ,在运行一幅动画或游戏逻辑之后,调用Game_Main()或者调用任意程序必须返回。否则, Windows主事件循环将不处理消息。   这种新型的实时结构的例子非常适合于游戏逻辑处理程序,请看源程序DEMO2_4.CPP以 及CD-ROM上相关的DEMO2_4.EXE。这种结构实际上是本书剩下部分的模型。 打开多个窗口   在完成本章内容之前,我想讨论一个你可能非常关心的更重要的话题——如果打开多 个窗口。实际上,这是小事一桩,其实你已经知道如何打开多个窗口。你所需要做的就是 多次调用函数CreateWindowEx()来创建这些窗口,事实也的确如此。但是,对此还有一些 需要注意的问题。   首先,请记住当你创建一个窗口时,是建立在Windows类的基础之上的。在该类中定义 了WinProc或者整个类的事件处理程序。这是非常重要的细节,因此应当注意。你可以使用 同一个类来创建多个窗口,这些窗口的所有消息都要传递到同一个WinProc中,正如由WIN CLASSEX结构中定义lpfnWndProc字段指向的事件处理程序一样。图2.11表示了这种情况下 的消息流。 ------图 ------   这样可能达到你的愿望,也可能达不到。如果想应用不同的WinProc打开各个窗口的话 ,你必须创建多个Windows类,并且使用每一个类创建各个窗口。这亲每一个类的窗口就指 向各自的WinProc并同其传递消息。图2.12表示了这种情况。   图2.12 多个窗口的多个Windows类   了解了这些内容后,下面是基于同一个类来创建多个窗口的程序代码:   //create the first window   if(!(hwnd = CreateWindowEx(NULL,//extended style     WINDOW_CLASS_NAME,//class     "Window 1 Based on WINCLASS1",//title     WS_OVERLAPPEDWINDOW|WS_VISIBLE,     0,0,//initial x,y     400,400//initial width,height     NULL,//handle to parent     NULL,//handle to menu     hinstance,//instance of this application     NULL)))//extra creation parms   return (0);   //create the second window   if(!(hwnd = CreateWindowEx(NULL,//extended style     WINDOW_CLASS_NAME,//class     "Window 2 Also Based on WINCLASS1",//title     WS_OVERLAPPEDWINDOW|WS_VISIBLE,     100,100,//initial x,y     400,400//initial width,height     NULL,//handle to parent     NULL,//handle to menu     hinstance,//instance of this application     NULL)))//extra creation parms   return (0);   当然,你可能希望用不同的变量而不是同一个变量来跟踪每一个窗口,就像hwnd中的 例子那样,其实你已掌握了其要义,如一次打开两个窗口的例子。请看DEMO2_5.CPP以及C D-ROM上相关的可执行文件DEMO2_5.EXE。当你运行.EXE执行文件时,应当可以看到如图2. 13所示的情况。注意当你关闭任何一个窗口时,两个窗口将同时关闭,并且应用程序也终 止。看一看是否能够找到每次只关闭一个窗口的方法。(提示:创建两个Windows类,直到 两个窗口都关闭之后再传递WM_QUIT消息。) 总结   尽管我并了解你,但是我还是很为你激动!到目前为止,你已具备了Windows编程的基 本知识并需要开始掌握更复杂的Windows编程知识。你已了解了Windows和多任务的结构, 并且也知道了如何创建一个Windows类、注册类、创建窗口、编写事件循环和句柄等等内容 。因此你已经打下了坚实的Windows编程基础。你完成了一项最杰出的任务。   下一章中,我们将了解更多的和Windows相关的内容,如:使用资源、创建菜单、使用 对话框以及获取信息等。

    最新回复(0)