第2章 Windows编程模型
"Lilu Dallas Multipass!"
—Lilu, The 5th Element(电影《第五元素》)
Windows编程就像去见牙科医生:虽然明明知道对自己是有益处的,可还是没人喜欢总是找牙医。对不对?在本章中,我将要使用“禅”的方法——或者换句话说,就是深入浅出地向你介绍基本的Windows编程。虽然我不能保证在阅读本章之后你会变得更加喜欢去见牙医,但是我敢保证你会比以往更喜欢Windows编程。下面是本章的内容:
• Windows的历史
• Windows的基本风格
• Windows的类(Class)
• 创建窗口
• Windows事件句柄(Event Handler)
• 事件驱动编程和事件循环
• 打开多个窗口
Windows的起源
别因为我要解放你的思想而感到害怕(特别是钟情于DOS的顽固分子)。让我们迅速浏览一下Windows这些年的形成和发展,以及它与游戏开发界的关系,好吗?
早期版本的Windows
Windows的发展始于Windows 1.0版本。这是Microsoft公司商业化视窗操作系统的第一次尝试,当然它是一个相当失败的产品。Windows 1.0完全建立在DOS基础上(这就是一个错误),不能执行多任务,运行速度很慢,看上去也差劲。它的外观可能是其失败的最重要原因。除了讽刺以外,问题还在于Windows 1.0与那个时代的80286计算机(或更差的8086)所能提供的相比需要更高的硬件、图像和声音性能。
然而,Microsoft稳步前进,很快就推出了Windows 2.0。我记得获得Windows 2.0的Beta测试版时我正在Software Publishing Corporation工作。在会议室中,挤满了公司的各级主管,也包括公司总裁(像往常一样,他正端着一杯鸡尾酒)。我们运行Windows 2.0 Beta演示版,装载了多个应用程序,看上去似乎还说得过去。但是,那时IBM已经推出了PM。PM看上去要好得多,而且是建立在比Windows 2.0先进得多的操作系统OS/2的基础上的。而Windows 2.0依然是基于DOS的视窗管理器。那天董事的结论是:“不错,但还不是一个可继续开发的操作系统。让我们继续开发DOS程序好了,给我再来一杯鸡尾酒怎么样?”
Windows 3.x
1990年,各星系的行星终于结盟了,因为Windows 3.0问世了,而且表现酷毙!尽管它仍然赶不上 Mac OS的标准,但是谁还在意呢?(真正的程序员都讨厌Mac)。软件开发人员终于可以在PC机上创建迷人的应用程序了,而商用应用程序也逐渐脱离DOS。这成了PC机的转折点,终于将Mac完全排除在商用应用程序之外了,而后也将其挤出桌面出版业(那时,Apple公司每5分钟就推出一种新硬件)。
尽管Windows 3.0工作良好,却还是存在许多的问题、软件漏洞,但从技术上说它已是Windows 2.0之后的巨大突破,有问题也是在所难免。为了解决这些问题,Microsoft推出了Windows 3.1,开始公关部和市场部打算称之为Windows 4.0,但是,Microsoft决定只简单地称之为Windows 3.1,因为它还不足以称之为升级的换代版本。它还没有做到市场部广告宣传的那样棒。
Windows 3.1非常可靠。它带有多媒体扩展以提供音频和视频支持,而且它还是一个出色的、全面的操作系统,用户能够以统一的方式来工作。另外,还存在一些其他的版本,如可以支持网络的Windows 3.11(适用于工作组的Windows)。惟一的问题是Windows 3.1仍然是一个DOS应用程序,运行于DOS扩展器上。
Windows 95
另一方面,从事游戏编程的人们还在唱着“坚守DOS岗位直到炼狱冻结!”的赞歌,而我甚至都焚烧了一个Windows 3.1的包装盒!但是,1995年炼狱真的开始冷却了——Windows 95终于推出。它是一个真正32位的、多任务、多线程的操作系统。诚然,其中还保留了一些16位代码,但在极大程度上,Windows 95是PC机的终极开发和发布平台。
(当然,Windows NT 3.0也同时推出,但是NT对于大多数用户来讲还是不可用的,因此这里也就不再赘述。)
当Windows 95推出后,我才真正开始喜欢Windows编程。我一直憎恨使用Windows 1.0、2.0、3.0和3.1来编程,尽管随着每一种版本的推出,这种憎恨都越来越少。当Windows 95出现时,它彻底改变了我的思想,如同其他被征服的人的感觉一样——它看上去非常酷!那正是我所需要的。
提 示
游戏编程中最重要的事情是游戏包装盒设计得如何,发给杂志公布的游戏画面如何。发送免费的东西给审阅人也是个好主意。
因此几乎是一夜间,Windows 95就改变了整个计算机行业。的确,目前还有一些公司仍然在使用Windows 3.1(你能相信吗?),但是Windows 95使得基于Intel的PC成为除游戏之外的所有应用程序的选择。不错,尽管游戏程序员知道DOS退出游戏编程行业只是一个时间的问题了,但是DOS还是他们的核心。
1996年,Microsoft公司发布了Game SDK(游戏软件开发工具包),这基本上就是DirectX的第一个版本。这种技术仅能在Windows 95环境下工作,但是它实在是太慢了,甚至竞争不过DOS游戏(如DOOM和Duke Nukem等)。所以游戏开发者继续为DOS32开发游戏,但是他们知道,假以时日,DirectX必定能具有足够快的速度,从而能使游戏流畅地运行在PC机上。
到了3.0版,DirectX的速度在同样计算机上已经和DOS32一样快了。到了5.0版,DirectX已经相当完善,实现了该技术最初的承诺。对此我们将在第5章“DirectX基础和令人生畏的COM”涉及DirectX时再作详细介绍。现在要意识到:Win32和DirectX的组合是PC机上开发游戏的惟一选择。现在回头看历史。
Windows 98
1998年中期,Windows 98推出了。这至多是技术革命中的一步,而不像Windows 95那样是一个完全革命性的产品,但毫无疑问它也占有很重要的地位。Windows 98就像一辆由旧车改装而成的跑车——外观时髦,速度飞快,好得无以伦比。它是全32位的,能够支持你想到的任何事情,并具有无限扩充的能力。它很好地集成了DirectX、3D图形、网络以及Internet。
Windows 98和Windows 95相比也非常稳定。诚然,Windows 98仍然会死机,但是请相信我,比起Windows 95来已经少了许多。而且,Windows 98支持即插即用,并且支持得很好——是时候了!
Windows ME
在1999年下半年到2000年上半年间,Windows ME(或千年版,Millennium)发布了。很难解释ME,基本上还是98的内核,但更紧密地集成了多媒体和网络支持。ME是定位在消费者市场,而不是技术或商用市场。就功能性而言,它和Windows 98没什么两样。而且,由于ME较高的集成度,某些应用程序在上面运行会遇到麻烦。一些旧的硬件完全不被支持,ME只对配置较新的电脑来说是好的选择。比如你可以用一台1995年装配的电脑来很好地运行98,但是用它来跑ME肯定不行。话说回来,对于配置较新的电脑来说,用来游戏,这还是一个较可靠、稳定的操作系统。
Windows XP
在本书写作的时候,微软刚刚发布了新的操作系统Windows XP。我真的很喜欢用它。Windows XP可以说是我见过的最嗲的操作系统。它具有98或ME的外观,同时又有Windows 2000或NT的稳定和可靠性。XP是面向消费者的操作系统迈出的一大步。但是,也带来了不好的地方。XP是完全的32位Windows兼容操作系统。这些年来微软不停地在说服硬件软件开发商不可以自行其是,一定要遵守规范。这下好了,付出代价的时候到了,XP不支持很多违反了游戏规则的软件。从另一方面这也是好事,从长远来讲,所有的软件公司将会重新编译他们的程序,清理那些不良和硬件相关的代码。这些不良代码就是造成98和98都那么不稳定的罪魁祸首。因而,XP就像是一次涅磐——我们有最酷最时髦的操作系统,想用的话,一定要守规矩。
无论怎样,为了不至于成为兼容性讨论的灾难范本,XP为用户提供了两个帮助他们运行他们想要的软件的工具。第一,XP操作系统不断地通过微软更新来更新自己,有许多公司不停地在解决各种讨厌的软件问题。第二,XP有一个“兼容性”模式,通过在这个模式里运行,你可以运行原本与XP并不兼容的软件;简言之,只需屏蔽对错误的检测,软件就能运行。 除了这些问题,我建议读者不要犹豫,尽快升级到Windows XP。
Windows NT/2000
现在我们来讨论一下Windows NT。在本书编写期间,Windows NT正在推出5.0版本,而且已经正式地被命名为Windows 2000。以我个人估计,它最终将取代Windows 9X成为每个人的操作系统选择。2000要比Windows 9X严谨得多;而且绝大多数游戏程序员都在NT上开发将在Windows 9X/ME/XP上运行的游戏。Windows 2000最酷的是它完全支持即插即用和Win32/DirectX,因此使用DirectX为Windows 9X编写的应用程序可以在Windows 2000上运行。这可是个好消息,因为从历史上看,是编写PC游戏的开发人员占有最大的市场份额。
那么最低标准是什么呢?如果你使用DirectX(或其他工具)编写了一个Win32应用程序,它完全可以在Windows 95、98、ME、XP和2000或更高版本上运行。这可是件好事情。因此你在本书中所学到的任何东西可以轻松应用到多种操作系统上。对了,甚至还包括Windows CE 3.0/Pocket PC 2002系统,因为它们也支持DirectX和Win32的一个子集。
Windows基本构架: Win9X/NT
和DOS不同,Windows是一个多任务的操作系统,允许许多应用程序和/或小程序同时运行,可以最大限度地发挥硬件的性能。这表明Windows是一个共享的环境——一个应用程序不能独占整个系统。尽管Windows 95、98、ME、XP和2000/NT它们都很相似,但仍然存在许多技术上的差异。但是就我们所关心的,我们可以进行一般性的讨论。这里所参照的Windows机器一般是指Win9X/NT或Windows环境。让我们开始吧!
多任务(Multitasking)和多线程(Multithreading)
如我所说,Windows允许不同的应用程序以轮询(Round-robin)的方式同时执行,每一个应用程序都占用一段很短的时间片来运行,而后就轮到下一个应用程序运行。如图2-1所示,CPU由几个不同的应用程序以循环的方式共享。而负责判断出下一个运行的应用程序、和给每个应用程序分配运行时间量是调度程序(Scheduler)的工作。
图2-1:在单处理器上进行多处理
调度程序可以非常简单——每个应用程序分配固定的运行时间,也可以非常复杂——将应用程序设定为不同的优先级和抢先性或低优先级的事件。就Win9X/NT而言,调度程序采用基于优先级的抢先占用方式。这就意味着一些应用程序要比其他的应用程序占用处理器更多的时间,但是如果一个应用程序需要CPU处理的话,在另一任务运行的同时,当前的任务可以被阻止或抢先占用。
但是不要太担心这些,除非你正在编写OS(操作系统)或实时代码——那样的话调度的细节事关重大。大多数情况下,Windows将执行和调度你的应用程序,无需你参与。
深入接触Windows,我们可以看到,它不仅是多任务的,而且还是多线程的。这意味着程序由许多较为简单的多个执行线程(Threads of Execution)构成。这些线程被视为具有较重的权值的进程——如程序一样,从而被调度。实际上,在同一时刻,你的计算机上有30~50个线程正同时运行,并执行着不同的任务。所以事实上你可能运行一个程序,但这个程序由一个或多个执行线程构成。
Windows实际的多线程示意图如图2-2所示,从图中可以看到,每一个程序实际上都是由一个主线程和几个工作线程构成。
图2-2:A more realistic multithreaded view of Windows.
获取线程的信息
下面让我们来看一下你的计算机现在正在运行多少个线程。在Windows机器上,同时按Ctrl+Alt+Delete键,弹出显示正在运行的任务(过程)的活动程序任务管理器。这和我们所希望的不同,但也很接近。我们希望的是一个显示正在执行的实际线程数的工具或程序。许多共享软件和商用软件工具都能做到这一点,但是Windows内嵌了这几个工具。
在安装Windows的目录(一般是Windows\)下,可以发现一个名字为SYSMON.EXE(Windows 95/98)或PREFMON.EXE(Windows NT)的可执行程序。图2-3描述了在我的Windows 98机器上运行的SYSMON.EXE程序。图中除了正在运行的线程外还有大量的信息,如内存使用和处理器装载等。实际上在进行程序开发时,我喜欢使SYSMON.EXE运行,由此可以了解正在进行什么以及系统如何加载程序。
图2-3:运行中的SYSM
你可能想知道能否对线程的创建进行控制,答案是能够!!!实际上这是Windows游戏编程最令人激动的事情之一——就像我们所希望的那样除了游戏主进程外,还能够执行其他的任务,我们也能够创建像其他任务一样多的线程。
注 意
在Windows 98/NT环境下,实际上还有一种叫纤程(Fiber)的新型执行对象,它比线程还简单(明白吗?线程是由纤程构成的)
这和DOS游戏程序的编写有很大不同。DOS是单线程操作系统,也就是说一旦你的程序开始运行,就只能运行该程序(不时出现的中断处理程序除外)。因此,如果想使用任何一种多任务或多线程,就必须自己来模拟(参阅《Sams Teach Yourself Game Programming in 21 Days》中关于一个完整的基于DOS的多任务内核的介绍)。这也正是游戏程序员在这么多年中所作的事。的确,模拟多任务和多线程远远不能和拥有一个完整的支持多任务和多线程的操作系统相提并论,但是对于单个游戏来讲,它足可以良好地工作。
在我们接触到真正的Windows编程和那些工作代码之前,我想提及一个细节。你可能在想,Windows真是一个神奇的操作系统,因为它允许多个任务和程序立即执行。请记住,实际上并不是这样的。如果只有一个处理器的话,那么一次也只能执行一个执行流、线程、程序或你所调用的任何对象。Windows相互之间的切换太快了,以至于看上去就像几个程序在同时运行一样。另一方面,如果有几个处理器的话,可以同时运行多个程序。例如,我有一个双CPU的Pentium II计算机,有两个400MHz的Pentium II处理器在运行Windows NT 5.0。使用这种配置,可以同时执行两个指令流。
我希望在不远的将来,个人计算机的新型微处理器结构能够允许多个线程或纤程同时执行,将这样一个目标作为处理器设计的一部分。例如,Pentium具有两个执行单元——U管和V管。因此它能够同时执行两个指令。但是,这两个指令都是来自同一个线程。类似的是Pentium II、II、IV能够同时执行多个简单的指令,但它们也需来自同一个线程。
事件模型
Windows是个多任务/多线程的操作系统,并且还是一个事件驱动的操作系统。和DOS程序不同的是,Windows程序都是等着用户去使用,并由此而触发一个事件,然后Windows对该事件发生响应,进行动作。请看图2-4所示的示意图,图中描述了大量的应用程序窗口,每个程序都向Windows发送待处理的事件和消息。Windows对其中的一些进行处理,大部分的消息和事件被传递给应用程序来处理。
图2-4:Windows event handling.
这样做的好处是你不必去关心其他的正在运行的应用程序,Windows会为你处理它们。你所要关心的就是你自己的应用程序和窗口中信息的处理。这在Windows 3.0/3.1中是根本不可能的。Windows的那些版本并不是真正的多任务操作系统,每一个应用程序都要产生下一个程序。也就是说,在这些版本的Windows下运行的应用程序感觉相当粗糙、缓慢。如果有其他应用程序干扰系统的话,这个正在“温顺地”运行的程序将停止工作。但这种情况在Windows 9X/NT下就不会出现。操作系统将会在适当的时间终止你的应用程序——当然,运行速度非常快,你根本就不会注意到。
到现在为止,读者已了解了所有有关操作系统的概念。幸运的是,有了Windows这个目前最好的编写游戏的操作系统,你根本就不必担心程序调度——你所要考虑的就是游戏代码和如何最大限度地发挥计算机的性能。
在本章后面内容中,我们要接触一些实际的编程工作,便于读者了解Windows编程有多容易。但是(永远都有但是)在进行实际编程之前,我们应当了解一些Microsoft程序员喜欢使用的约定。这样你就不会被那些古怪的函数和变量名弄得不知所措。
按照微软风格编程:匈牙利符号表示法
如果你正在运作一个像Microsoft一样的公司,有几千个程序员在干不同的项目,在某种程度上应当提出一个编写代码的标准方式。否则,结果将是一片混乱。因此一个名字叫Charles Simonyi的人负责创建了一套编写Microsoft代码的规范。这个规范一直以来都被用作编写代码的基本指导说明书。所有Microsoft的API、界面、技术文件等等都采用这些规范。
这个规范通常被称为匈牙利符号表示法,可能是因为为了创立这个规范他经常加班——弄得饥肠辘辘的原因吧(英文中饥饿(Hungry)和匈牙利(Hungary)谐音),也可能是因为他是匈牙利人。虽然不知道名字的由来,关键在于你还是必须了解这个规范,以便于你能够阅读Microsoft代码。
匈牙利符号表示法包括许多与下列命名有关的约定:
• 变量
• 函数
• 类型和常量
• 类
• 参数
表2-1给出了匈牙利符号表示法使用的前缀代码。这些代码在大多数情况下一半用于前缀变量名,其他约定根据名称确定。其他解释可以参考本表。
表2-1:匈牙利符号表示法使用的前缀代码
前缀 数据类型(基础类型)
c Char字符
by BYTE字节(无符号字符)
n Short短整数和整数(表示一个数)
i int整数
x, y Short短整数(通常用于x坐标和y坐标)
cx, cy short短整数(通常用于表示x和y的长度;c表示计数)
b BOOL(整数)
w UINT(无符号整数)和WORD(无符号字)
l LONG(长整数)
dw DWORD(无符号长整数)
fn 函数指针
s 字符串
sz, str 以一个字节的0(空值)终止的字符串
lp 32位指针
h 编号(常用于表示Windows对象)
msg 消息
变量的命名
变量的命名应用匈牙利符号表示法,变量可用表2-1中的前缀代码来表示。另外,当一个变量是由一个或几个子名构成时,每一个子名都要以大写字母开头。下面是几个例子:
char *szFileName; // a null terminated string
int *lpiData; // 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 timer
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++中,关键字const不止一个意思。在前面的代码行中,它用来创建一个常数变量。这和#define相似,但是它增加了类型信息这个特性。const不仅仅像#define一样是一个简单的预处理文本替换,而且更像是一个变量。它允许编译器进行类型检查和转型。
类的命名
类命名的约定可能要麻烦一点。但我也看到有很多人在使用这个约定,并独立地进行补充。不管怎样说,所有C++的类必须以大写C为前缀,类名字的每一个子名的第一个字母都必须大写。下面是几个例子:
class CVector
{
public:
CVector() {ix=iy=iz=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编程吧。
以每一种新语言或所学的操作系统来编写一个输出“Hello World”文字的程序是一个习惯做法,让我们也来试试。程序清单2-1是标准的基于DOS的“Hello World”程序。
程序清单2-1:基于DOS的“Hello World”程序
// DEMO2_1.CPP - standard version
#include <stdio.h>
// main entry point for all standard DOS/console programs
void main(void)
{
printf("\nTHERE CAN BE ONLY ONE!!!\n");
} // end main
现在让我们看一看如何在Windows下完成同样功能。
提 示
顺便说一句,如果想编译DEMO2_1.CPP,应当用VC++或Borland编译器实际创建一个控制台应用程序(CONSOLE APPLICATION)。这是类DOS的应用程序,只是它是32位的,它仅以文本模式运行,但对于检验一下想法和算法是很有用的。注意在编译器中应当将目标.EXE设为控制台应用程序,而非Win32 .EXE!
要编译本程序,请执行以下步骤:
1. 创建新的控制台应用程序.EXE工程,并包含光盘上T3DCHAP02\目录中的DEMO2_1.CPP
2. 编译并连接程序。
3. 运行它!(也可以运行预先编译好的光盘上的DEMO2_1.EXE。)
总是从WinMain()开始
如前面所述,所有的Windows程序都以WinMain()开始,这和正统的DOS程序都以Main()开始一样。WinMain()中的内容取决于你。如果你愿意的话,可以创建一个窗口、开始处理事件并在屏幕上画一些东西。另一方面,你可以调用成百个(或者是上千个)Win32 API函数中的一个。这正是我们将要做的。
我只想在屏幕上的一个信息框中显示一些东西。这恰好是一个Win32 API函数MessageBox()的功能。程序清单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,
LPSTR lpcmdline,
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\下的DEMO2_2.CPP。
2. 编译和连接程序。
3. 运行!(或在CD-ROM上直接运行预编译版本DEMO2_2.EXE。)
你一定一直都以为一个基本的Windows程序至少有几百行代码。当你编译并运行这个程序的时候时,会看到如图2-5所示的内容。
图2-5:Running DEMO2_2.EXE
程序剖析
现在已经有了一个完整的Windows程序,让我们一行一行地分析程序的内容。首先第一行程序是
#define WIN32_LEAN_AND_MEAN
这个应稍微解释一下。创建Windows程序有两种方式——使用Microsoft基础类库(Microsoft Foundation Classes,MFC),或者使用软件开发工具包(Software Development Kit,SDK)。MFC完全基于C++和类,要比以前的游戏编程所需的工具复杂得多,功能足够强大和复杂,足以应付游戏的需要。而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— 这是一个空值终止字符串(null-terminated string),和标准C/C++的main(int argc, char **argv)函数中的命令行参数相似。不同的是,它没有一个单独的参数来像argc那样指出命令行参数个数。例如,如果你创建一个名字为TEST.EXE的Windows应用程序,并且使用下面的参数运行:
TEST.EXE one two three
lpcmdline将含有如下数据:
lpcmdline = "one two three"
注意,.EXE的文件名并不是命令行的一部分。
• ncmdshow— 最后的这个参数是个整数。它在启动过程中被传递给应用程序,带有如何打开主应用程序窗口的信息。这样,用户便会拥有一点控制应用程序如何启动的能力。当然,作为一个程序员,如果想忽略它也可以,而想使用它也行。(你将参数传递给ShowWindow(),我们又超前了!)表2-2列出了ncmdshow最常用的参数值。
表2-2:Windows Codes for ncmdshow
值 功能
SW_SHOWNORMAL 激活并显示一个窗口。如果该窗口最小化或最大化的话,Windows将它恢复到原始尺寸和位置。当第一次显示该窗口时,应用程序将指定该标志
SW_SHOW 激活一个窗口,并按当前尺寸和位置显示
SW_HIDE 隐藏一个窗口,并激活另外一个窗口
SW_MAXIMIZE 将指定的窗口最大化
SW_MINIMIZE 将指定的窗口最小化,并激活Z顺序下的下一个窗口
SW_RESTORE 激活并显示一个窗口。如果该窗口最小化或最大化的话,Windows将它恢复到原始尺寸和位置。当恢复为最小化窗口时,应用程序必须指定该标志
SW_SHOWMAXIMIZED 激活一个窗口,并以最大化窗口显示
SW_SHOWMINIMIZED 激活一个窗口,并以最小化窗口显示
SW_SHOWMINNOACTIVE 以最小化窗口方式显示一个窗口,激活的窗口依然保持激活的状态
SW_SHOWNA 以当前状态显示一个窗口,激活的窗口依然保持激活的状态
SW_SHOWNOACTIVATE 以上一次的窗口尺寸和位置来显示窗口,激活的窗口依然保持激活的状态
如表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 hwnd, // 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 信息框含有三个按钮:Abort、Retry和Ignore
这一组控制在图标上添加一点“穷人的多媒体”
MB_ICONEXCLAMATION 信息框显示一个惊叹号图标
MB_ICONINFORMATION 信息框显示一个由圆圈中的小写字母I构成的图标
MB_ICONQUESTION 信息框显示一个问号图标
MB_ICONSTOP 信息框显示一个终止符图标
该标志组控制默认时高亮的按钮
MB_DEFBUTTONn 其中n是一个指示默认按钮的数字(1~4),从左到右计数
注意:还有其他的高级OS级标志,我们没有讨论。如果希望了解更多细节的话,可以查阅编译器Win32 SDK的在线帮助。
可以同时使用表2-3中的值进行逻辑或运算,来创建一个信息框。一般情况下,只能从每一组中仅使用一个标志来进行或运算。
当然,和所有Win32 API函数一样,MessageBox()函数返回一个值来通知编程者所发生的事件。但在这个例子中谁关心这个呢?通常情况下,如果信息框是yes/no提问之类的情况的话,就希望知道这个返回值。表2-4列出了可能的返回值。
表2-4:MessageBox()的返回值
值 选择的按钮
IDABORT Abort
IDCANCEL Cancel
IDIGNORE Ignore
IDNO No
IDOK OK
IDRETRY Retry
IDYES Yes
最后,这个表已经毫无遗漏地列出了所有的返回值。现在已经完成了对我们第一个Windows程序——单击的逐行分析。
提 示
现在希望你能轻松地对这个程序进行修改,并以不同的方式进行编译。使用不同的编译器选项,例如优化。然后尝试通过调试程序来运行该程序,看看你是否已经领会。做完后,请回到此处。
如果希望听到声音的话,一个简单的技巧就是使用MessageBeep()函数,可以在Win32 SDK中查阅,它和MessageBox()函数一样简单好用。下面就是该函数原型:
BOOL MessageBeep(UINT utype); // the sound to play
可以从表2-5所示常数中得到不同的声音。
表2-5:Sound Identifiers for MessageBeep()
值 声音
MB_ICONASTERISK 系统星号
MB_ICONEXCLAMATION 系统惊叹号
MB_ICONHAND 系统手形指针
MB_ICONQUESTION 系统问号
MB_OK 系统默认值
0xFFFFFFFF 使用计算机扬声器的标准嘟嘟声,令人讨厌
注意:如果已经安装了MS-Plus主题曲的话,你应能得到有意思的结果。
看Win32 API多酷啊!可以有上百个函数使用。它们虽然不是世界上最快的函数,但是对于一般的日常工作、输入输出和图形用户界面来讲,它们已经很棒了。
让我们稍微花点时间总结一下我们目前所知的有关Windows编程方面的知识。首先,Windows支持多任务/多线程,因此可以同时运行多个应用程序。我们不必费心就可以做到这一点。我们最关心的是Windows是事件驱动的。这就意味着我们必须处理事件(在这一点上目前我们还不知如何做)并且做出反应。好,听上去不错。最后所有Windows程序都以函数WinMain()开始,WinMain()函数中的参数要比标准DOS的Main()多,但这些参数都属于逻辑和推理的领域。
掌握了上述的内容,就到了编写一个真正的Windows应用程序的时候了。
现实中的Windows应用程序
尽管本书的目标是编写在Windows环境下运行的3D游戏,但是你并不需要了解更多的Windows编程。实际上,你所需要的就是一个基本的Windows程序,可以打开一个窗口、处理信息、调用主游戏循环等等。了解了这些,本章中的目标是首先向你展示如何创建简单的Windows应用程序,同时为编写类似32位DOS环境的游戏外壳(Shell)程序奠定基础。
一个Windows程序的关键就是打开一个窗口。一个窗口就是一个显示文本和图形信息的工作区。要创建一个完全实用的Windows程序,只要进行下列工作:
1. 创建一个Windows类。
2. 创建一个事件句柄或WinProc。
3. 用Windows注册Windows类。
4. 用前面创建的Windows类创建一个窗口。
5. 创建一个能够从事件句柄获得或向事件句柄传递Windows信息的主事件循环。
让我们详细了解一下每一步的工作。
Windows类
Windows实际上是一个面向对象的操作系统,因此Windows中大量的概念和程序都源于C++。其中一个概念就是Windows类。Windows中的每一个窗口、控件、列表框、对话框和小部件等等实际上都是一个窗口。区别它们的就是定义它们的类。一个Windows类就是Windows能够操作的一个窗口类型的描述。
有许多预定义的Windows类,如按钮、列表框、文件选择器等等。你也可以自己任意创建你的Windows类。实际上,你可以为自己编写的每一个应用程序创建至少一个Windows类。否则你的程序将非常麻烦。因此你应当在画一个窗口时,考虑一个Windows类来作为Windows的一个模板,以便于在其中处理信息。
控制Windows类信息的数据结构有两个:WNDCLASS和WNDCLASSEX。WNDCLASS是比较古老的一个,可能不久将废弃,因此我们应当使用新的扩展版WNDCLASSEX。二者结构非常相似,如果有兴趣的话,可以在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);
第二个字段包含描述该窗口一般属性的样式(Style)信息标志。有许多这样的标志,因此我没有将它们全部列出。只要能够使用它们创建任何类型的窗口就行了。表2-6列出了常用的标志。读者可以任意对这些值进行逻辑“或”运算,来派生所希望的窗口类型。
表2-6:Style Flags for Window Classes
标识 描述
CS_HREDRAW 若移动或改变了窗口宽度,则刷新整个窗口
CS_VREDRAW 若移动或改变了窗口高度,则刷新整个窗口
CS_OWNDC 为该类中每个窗口分配一个单值的设备描述表(在本章后面详细描述)
CS_DBLCLKS 当用户双击鼠标时向窗口程序发送一个双击的信息,同时,光标位于属于该类的窗口中
CS_PARENTDC 在母窗口中设定一个子窗口的剪切区,以便于子窗口能够画在母窗口中
CS_SAVEBITS 在一个窗口中保存用户图像,以便于在该窗口被遮住、移动时不必每次刷新屏幕。但是,这样会占用更多的内存,并且比人工同样操作要慢得多
CS_NOCLOSE 禁止系统菜单上的关闭命令
注意:用加粗体显示的部分为最常用的标志。
表2-6包含了大量的标志,如果你觉得迷惑,我并不会责怪你。现在,设定样式标识符,描述如果窗口移动或改变尺寸就进行屏幕刷新,并可以获得一个静态的设备描述表(Device Context)以及处理双击事件的能力。
我们将在第3章“高级Windows编程”中详细讨论设备描述表,但基本说来,它被用作窗口中图像着色的数据结构。因此,如果你要处理一个图像,就应为感兴趣的特定窗口申请一个设备描述表。如果设定了一个Windows类,它就通过CS_OWNDC得到了一个设备描述表,如果你不想每次处理图像时都申请设备描述表,可以将它保存一段时间。上面说的对你有帮助还是使你更糊涂?Windows就是这样——你知道得越多,问题就越多。好了!下面说一下如何设定style字段:
winclass.style = CS_VREDRAW | CS_HREDRAW | CS_OWNDC | CS_DBLCLICKS;
WNDCLASSEX结构的下一个字段lpfnWndProc是一个指向事件句柄的函数指针。基本上这里所设定的都是该类的回调函数。回调函数在Windows编程中经常使用,工作原理如下:当有事件发生时,Windows通过调用一个你已经提供的回调函数来通知你,这省去你盲目查询的麻烦。随后在回调函数中,再进行所需的操作。
这个过程就是基本的Windows事件循环和事件句柄的操作过程。向Windows类申请一个回调函数(当然需要使用特定的原型)。当一个事件发生时,Windows按如图2-6所示的那样替你调用它。关于该项内容我们将在下面部分进行更详细的介绍。但是现在,读者只要将其设定到你将编写的事件函数中去:
winclass.lpfnWndProc = WinProc; // this is our function
图2-6:The Windows event handler callback in action.
提 示
函数指针有点像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 really calls Add(1,2)
// result will be 3
Math = Sub;
int result = Math(1,2); // this really calls Sub(1,2)
// result will be –1
看,不错吧。
下面两个字段,cbClsExtra和cbWndExtra原是为指示Windows将附加的运行时间信息保存到Windows类某些单元中而设计的。但是绝大多数人使用这些字段并简单地将其值设为0,如下所示:
winclass.cbClsExtra = 0; // extra class info space
winclass.cbWndExtra = 0; // extra window info space
下一个是hInstance字段。它就是在启动时传递给WinMain()函数的hInstance,因此只需简单地从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是包含被装载图标资源名称的空值终止字符串。当hInstance为NULL时,lpIconName的值如表2-7所示。
表2-7:Icon Identifiers for 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 LoadCursor(HINSTANCE hInstance,// handle of application instance
LPCTSTR lpCursorName); // name string or cursor resource identifier
hInstance是你的.EXE的应用程序实例。该.EXE应用程序包含资源数据,可以按名字来解出自定义的光标。但现在我们不打算使用该功能,所以让我们将hInstance设为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都至少将以用户预定义的颜色或者按照Windows的说法——画刷(Brush)填充该窗口的背景。因此,hbrBackground是一个用于窗口刷新的画笔句柄。画刷、画笔、色彩和图形都是GDI(图形设备接口)的一部分,我们将在下一章中详细讨论。现在,介绍一下如何申请一个基本的系统画笔来填充窗口。该项功能由GetStockObject()函数实现,如下面程序所示(注意转型到(HBRUSH)):
winclass.hbrBackground = (HBRUSH)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字符串,用于加载和选用窗口。其工作原理将在第3章“高级Windows编程”中讨论。现在我们只需将值设为NULL:
winclass.lpszMenuName = NULL; // the name of the menu to attach
如我刚提及的那样,每个Windows类代表你的应用程序所创建的不同窗口类型。在某种程度上,类与模板相似,Windows需要一些途径来跟踪和识别它们。因此,下一个字段lpszClassName,就用于该目的。该字段被赋以包含相关类的文本标示符的空值终止字符串。我个人喜欢用诸如“WINCLASS1”、“WINCLASS2”等标识符。你可以自己喜好而定,但应以简单明了为原则,如下所示:
winclass.lpszClassName = "WINCLASS1"; // the name of the class itself
这样赋值以后,你可以使用它的名字来引用这个新的Windows类了,“WINCLASS1”——很酷,是吗?
最后就是小应用程序图标。这是Windows类WNDCLASSEX中新增加的功能,在老版本WNDCLASS中没有。首先,它是指向你的窗口标题栏和Windows桌面任务栏的句柄。你经常会需要装载一个自定义资源,但是现在只要通过LoadIcon()使用一个标准的Windows图标即可实现:
winclass.hIconSm =
LoadIcon(NULL, IDI_APPLICATION); // the handle of the small icon
下面让我们整体回顾一下整个类的定义:
WNDCLASSEX winclass; // this will hold 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.hbrBackground = GetStockObject(BLACK_BRUSH);
winclass.lpszMenuName = NULL;
winclass.lpszClassName = "WINCLASS1";
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()函数之前,Windows并不知道该类的存在。明白了吧?
此外,为完全起见,还有一个旧版本的RegisterClass()函数,用于注册基于旧结构WNDCLASS的类。
类一旦注册,我们就可以任意创建它的窗口。请看下面如何进行这个工作,然后再详细看一下事件句柄和主事件循环,了解使一个Windows应用程序运行还要做哪些工作。
创建窗口
要创建一个窗口(或者一个类窗口的对象),使用CreateWindow()或CreateWindowEx()函数。后者是更新一点的版本,支持附加类型参数,我们就使用它。该函数是创建Windows类的函数,我们要多花一点时间来逐行分析。在创建一个窗口时,必须为这个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, // window 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— 这是包含窗口标题的空值终止字符串——例如“My First Window”。
• dwStyle— 这是一个说明窗口外观和行为的通用窗口标志——非常重要!表2-10列出了一些最常用的值。当然,可以用逻辑“或”运算来任意组合使用这些值来得到希望的各种特征。
• x,y— 这是该窗口左上角位置的像素坐标。如果你无所谓,可使用CW_USEDEFAULT,这将由Windows来决定。
• nWidth, nHeight— 这是以像素表示的窗口宽度和高度。如果你无所谓,可使用CW_USEDEFAULT,这将由Windows来决定窗口尺寸。
• hWndParent— 假如存在父窗口,这是指向父窗口的句柄。如果没有父窗口,取NULL,桌面就是父窗口。
• hMenu— 这是指向附属于该窗口菜单的句柄。下一章中将详细介绍,现在将其赋值NULL。
• hInstance— 这是应用程序的实例。这里从WinMain()中使用hinstance。
• lpParam— 高级特征,设置为NULL。
表2-10列出了各种窗口标志设置。
表2-10:General Style Values for dwStyle
类型 所创建的内容
WS_POPUP 弹出式窗口
WS_OVERLAPPED 带有标题栏和边界的重叠式窗口,类似WS_TILED类型
WS_OVERLAPPEDWINDOW 具有WS_OVERLAPPED、WS_CAPTION、WS_SYSMENU、WS_THICKFRAME、WS_MINIMIZEBOX和WS_MAXIMIZEBOX样式的重叠式窗口
WS_VISIBLE 开始就可见的窗口
WS_SYSMENU 标题栏上有窗口菜单的窗口,WS_CAPTION必须也被指定
WS_BORDER 有细线边界的窗口
WS_CAPTION 有标题栏的窗口(包括WS_BORDER样式)
WS_ICONIC 开始就最小化的窗口,类似WS_MINIMIZE样式
WS_MAXIMIZE 开始就最大化的窗口
WS_MAXIMIZEBOX 具有最大化按钮的窗口。不能和WS_EX_CONTEXTHELP样式合用。WS_SYSMENU也必须指定
WS_MINIMIZE 开始就最小化的窗口,类似WS_ICONIC样式
WS_MINIMIZEBOX 具有最小化按钮的窗口。不能和WS_EX_CONTEXTHELP 样式合并。WS_SYSMENU也必须指定
WS_POPUPWINDOW 带有WS_BORDER、WS_POPUP和WS_SYSMENU类型的弹出式窗口;WS_CAPTION和WS_POPUPWINDOW s也必须指定以使窗口菜单可见
WS_SIZEBOX 一个窗口边界可以变化,和WS_THICKFRAME类型相同
WS_HSCROLL 带有水平滚动条的窗口
WS_VSCROLL 带有垂直滚动条的窗口
注意:用加粗体显示的是经常使用的值。
下面是使用标准控件在(0,0)位置创建一个大小为400X400像素的、简单的重叠式窗口。
HWND hwnd; // window handle
// create the window, bail if problem
if (!(hwnd = CreateWindowEx(NULL, // extended style
"WINCLASS1", // 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);
一旦创建了该窗口,它可能是可见或不可见的。但是,在这个例子中,我们增加了自动显示的类型标识符WS_VISIBLE。如果没有添加该标识符,则调用下面的函数来手工显示该窗口:
// this shows the window
ShowWindow(hwnd, ncmdshow);
记住WinMain()中的ncmdshow参数了吗?这就是使用它的方便之处。尽管我们使用WS_VISIBLE覆盖了ncmdshow参数,但还是应将其作为一个参数传递给ShowWindow()。下面让Windows更新窗口的内容,并且产生一个WM_PAINT信息,这通过调用函数UpdateWindow()来完成:
// this sends a WM_PAINT message to window and makes
// sure the contents are refreshed
UpdateWindow();
事件句柄
我并不了解你的情况,但注意我现在正使你掌握Windows的核心。它有如一本神秘小说。请记住,我所说的事件句柄(event handler)就是当事件发生时Windows从主事件循环调用的回调函数。回顾一下图2-6,巩固一下你对一般数据流的印象。
事件句柄由你自己编写,它能够处理你所关心的所有事件。其余的工作就交给Windows处理。当然,请记住,你的应用程序所能处理的事件和消息越多,它的功能也就越多。
在编写程序之前,让我们讨论一下事件句柄的一些细节,即事件句柄能做什么,工作机理如何。首先,对于创建的任何一个Windows类,都有一个独立的事件句柄,从现在开始我将用Windows’ Procedure称呼它,简称WinProc。当收到用户或Windows发送的消息并放在主事件序列中时,WinProc就接收到主事件循环发送的消息。这简直是一个发疯的绕口令,让我换个方式来说……
当用户和Windows运行任务时,你的窗口和/或其他应用程序窗口产生事件和消息。所有消息都进入一个队列,而你的窗口的消息发送到你的窗口专用队列中。然后主事件循环检索这些消息,并且将它们发送到你的窗口的WinProc中来处理。
这几乎有上百个可能的消息和变量,因此,我们就不全部分析了。值得庆幸的是,你只需处理很少的消息和变量,就可以启动并运行Windows应用程序。
简单地说,主事件循环将消息和事件反馈到WinProc,WinProc对它们进行处理。因此不仅你要关注WinProc,主事件循环同样也要关心WinProc。现在我们简要地了解一下WinProc,现假定WinProc只接收消息。
现在来看一下WinProc的工作机制,让我们看一下它的原型:
LRESULT CALLBACK WindowProc(
HWND 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句柄,只有当你使用同一个窗口类建立多个窗口时它才用到。这种情况下,hwnd是表明消息来自哪个窗口的惟一途径。图2-7表示了这种情况。
图2-7:Multiple windows based on the same class.
• msg— 这是一个实际的WinProc处理的消息标识符。这个标识符可以是众多主要消息中的一个。
• wparam and lparam— 进一步匹配或分类发送到msg参数中的信息。
最后,我们感兴趣的是返回类型LRESULT和声明说明符CALLBACK。这些关键字都是必需的,不能忘记它们!
因此大多数人所要做的就是使用switch()来处理msg所表示的消息,然后为每一种情况编写代码。在msg的基础上,你可以知道是否需要进一步求wparam和/或lparam的值。很酷吗?因此让我们看一下由WinProc传递过来的所有可能的消息,然后再看一下WinProc的工作机理。表2-11简要列出了一些基本的消息说明符。
表2-11:A Short List of Message IDs
值 说明
WM_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中。因此,参考在线Win32 SDK帮助查找某个消息的参数所代表的意义是个好主意。
幸好我们现在只对下面三个消息感兴趣:
• WM_CREATE— 当窗口第一次创建时传递该消息,以便你进行启动、初始化或资源配置工作。
• WM_PAINT— 当一个窗口内容需重画时传递该消息。这可能有许多原因:用户移动窗口或改变其尺寸、弹出其他应用程序而遮挡了你的窗口等。
• WM_DESTROY— 当你的窗口将要被销毁时该消息会被传递到窗口。通常这是由于用户单击该窗口的关闭按钮,或者是从该窗口的系统菜单中关闭该窗口造成的。无论上述哪一种方式,都应当释放所有的资源,并且通过发送一个WM_QUIT消息来通知Windows完全终止应用程序。后面还将详细介绍。
不要慌,让我们看一个完整的WinProc处理所有这些消息。
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
由上面可以看到,函数的大部分是由空行构成——这真是件好事情啊。让我们就以处理WM_CREATE开始讲解吧。该函数所作的就只是return(0)。这就是通知Windows你已经处理了它,因此无需更多的操作。当然,也可以在WM_CREATE消息中进行全部的初始化工作,但那由你决定。
下一个消息WM_PAINT非常重要。该消息在窗口需要重画时被发送。一般来说这表示应当由你进行重画工作。对于DirectX游戏来说,这并不是件什么大事,因为你本来就将以30~60fps(帧/秒)的速度重画屏幕。但是对于标准Windows应用程序来说,它就是件大事了。我将在后面章节中更详细地介绍WM_PAINT,目前的功能就是通知Windows,你已经重画好窗口了,因此就停止发送WM_PAINT消息。
要完成该功能,你必须激活该窗口的客户区。有许多方法可以做到,但调用函数BeginPaint()和EndPaint()最简单。这一对调用将激活窗口,并使用原先存储在Windows类中的变量hbrBackground的背景画刷来填充背景。下面是相关程序代码,供你验证:
// begin painting
hdc = BeginPaint(hwnd,&ps);
// you would do all your painting here
EndPaint(hwnd,&ps);
需要提醒几件事情。第一,请注意,每次调用的第一个参数是窗口句柄hwnd。这是一个非常必要的参数,因为BeginPaint()—EndPaint()函数能够在任何应用程序窗口中绘制,因此该窗口句柄指示了要重画哪个窗口。第二个参数是包含必须重画的矩形区域的PAINTSTRUCT结构的地址。下面是PAINTSTRUCT结构:
typedef struct tagPAINTSTRUCT
{
HDC hdc;
BOOL fErase;
RECT rcPaint;
BOOL fRestore;
BOOL fIncUpdate;
BYTE rgbReserved[32];
} PAINTSTRUCT;
实际上现在还不需考虑这些,当我们讨论图形设备接口(GDI)时会再讨论这个函数。其中最重要的字段就是rcPaint,它是一个表示最小需重画区域的矩形结构(RECT)。图2-8表示了这个字段的内容。注意Windows一直尽可能地试图作最少的工作,因此当一个窗口内容被破坏之后,Windows至少会告诉你要恢复该内容需要重画的最小的矩形。如果你对感兴趣的话,会发现只有矩形的四个角是最重要的,如下所示:
typedef struct tagRECT
{
LONG left; // left x-edge of rect
LONG top; // top y-edge of rect
LONG right; // right x-edge of rect
LONG bottom; // bottom y-edge of rect
} RECT;
图2-8:Repainting the invalid region only.
调用BeginPaint()函数应注意的最后一件事情是,它返回一个指向图形环境(Graphics Context)或hdc的句柄:
HDC hdc; // handle to graphics context
hdc = BeginPaint(hwnd,&ps);
图形环境就是描述视频系统和正在绘制表面的数据结构。奇妙的是,如果你需要绘制图形的话,只要获得一个指向图形环境的句柄即可。这便是关于WM_PAINT消息的内容。
WM_DESTROY消息实际上非常有意思。WM_DESTROY在用户关闭窗口时被发送。当然仅仅是关闭窗口,而不是关闭应用程序。应用程序继续运行,但是没有窗口。对此要进行一些处理。大多数情况下,当用户关闭主要窗口时,也就意味着要关闭该应用程序。因此,你必须通过发送一个消息来通知系统。该消息就是WM_QUIT。因为该消息经常使用,所以有一个函数PostQuitMessage()来替你完成发送工作。
在WM_DESTROY处理程序中你所要做的就是清除一切,然后调用PostQuitMessage(0)通知Windows终止应用程序。接着将WM_QUIT置于消息队列,这样在某一个时候终止主事件循环。
在我们所分析的WinProc句柄中还有许多细节应当了解。首先,你肯定注意到了每个处理程序体后面的return(0)。它有两个目的:退出WinProc以及通知Windows你已处理了信息。第二个重要的细节是默认消息处理程序DefaultWindowProc()。该函数是一个传递Windows默认处理消息的传递函数。因此,如果不处理该消息的话,可通过如下所示的调用来结束你的所有事件处理函数:
// process any messages that we didn't take care 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
就这么简单?是的就是这么简单!让我们来看一下。只要GetMessage()返回一个非零值,主程序while()就开始执行。GetMessage()是主事件循环的关键代码,其惟一的用途就是从事件队列中获得消息,并进行处理。你会注意到GetMessage()有四个参数。第一个参数对我们非常重要,而其余的参数都可以设置为NULL或0。下面列出其原型,以供参考:
BOOL GetMessage(
LPMSG lpMsg, // address of structure with message
HWND hWnd, // handle of window
UINT wMsgFilterMin, // first message
UINT wMsgFilterMax); // last message
你或许已经猜到了,msg参数是Windows放置下一个消息的存储单元。但是和WinProc()的msg参数不同的是,该msg是一个复杂的数据结构,而不仅仅是一个整数。当一个消息传递到WinProc时,它就被处理并分解为各个组元。MSG结构的定义如下所示:
typedef struct tagMSG
{
HWND hwnd; // window where message occurred
UINT message; // message id itself
WPARAM wParam; // sub qualifies message
LPARAM lParam; // sub qualifies message
DWORD time; // time of message event
POINT pt; // position of mouse
} MSG;
看出点眉目来了,是吗?注意所有向WinProc()传递的参数都包含在该结构中,还包括其他参数,如事件发生时的时间和鼠标的位置。
GetMessage()从时间序列中获得下一个消息,然后下一个被调用的函数就是TranslateMessage()。TranslateMessage()是一个虚拟加速键翻译器(Virtual Accelerator Key Translator)——换句话说就是输入工具。现在只是调用它,不必管其功能。最后一个函数DispatchMessage()指出所有操作发生的位置。当消息被GetMessage()获得以后,由函数TranslateMessage()稍加处理和转换,通过函数DispatchMessage()调用WinProc进行进一步的处理。
DispatchMessage()调用WinProc,并从最初的MSG结构中发送适当的参数。图2-9表示了整个处理过程。
图2-9:The mechanics of event loop message processing.
没错,你已经成为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 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.hbrBackground = 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.CPP添加到该工程中即可。假如你喜欢的话,可以直接从CD-ROM上运行预先编译好的程序DEMO2_3.EXE。图2-10显示了该程序运行中的样子。
图2-10:运行中的DEMO2_3.EXE
在进行下一部分内容之前,我还有事情要说。首先,如果你认真阅读了事件循环的话,会发现它看上去并不是个实时程序。也就是说,当程序在等待通过GetMessage()传递的消息的同时,主事件循环基本上是锁定的。这的确是真的;你必须以各种方式来避免这种现象,因为你需要连续地执行你的游戏处理过程,并且在Windows事件出现时处理它们。
产生一个实时事件循环
有一种实时的无等候的事件循环很容易实现。你所需要的就是一种测试在消息序列中是否有消息的方法。如果有,你就处理它;否则,继续处理其他的游戏逻辑并重复进行。运行的测试函数是PeekMessage()。其原型几乎和GetMessage()相同,如下所示:
BOOL PeekMessage(
LPMSG lpMsg, // pointer to structure 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()处理之后,消息已经从序列中去除。
如果将这两种情况考虑进去的话,你可以做出两个选择:如果有消息的话,就使用PeekMessage()和PM_NOREMOVE,调用GetMessage();另一种选择是:使用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 is 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_DESTROY消息时,你的工作就是通过调用PostQuitMessage()函数来传递WM_QUIT消息。WM_QUIT就在事件序列中慢慢地移动,你可以检测到它,所以可以跳出主循环。
用加粗体显示的程序最后一部分指出调用主游戏程序代码循环的位置。但是请不要忘记,在运行一幅动画或游戏逻辑之后,调用Game_Main()或者调用任意程序必须返回。否则,Windows主事件循环将不处理消息。
这种新型的实时结构的例子非常适合于游戏逻辑处理程序,请看源程序DEMO2_4.CPP以及CD-ROM上相关的DEMO2_4.EXE。这种结构实际上是本书剩下部分的原型。
打开多个窗口
在完成本章内容之前,我想讨论一个你可能非常关心的更重要的话题——如何打开多个窗口。实际上,这是小事一桩,其实你已经知道如何打开多个窗口。你所需要做的就是多次调用函数CreateWindowEx()来创建这些窗口,事实也的确如此。但是,对此还有一些需要注意的问题。
首先,记住当你创建窗口的时候,它必定是基于某个窗口类的。在所有东西里,是这个窗口类定义了WinProc或者说事件句柄。这点细节至关重要,应次要注意。你可以使用同一个类创建任意数量的窗口,但是这些窗口的所有的消息都会按照WINCLASSEX结构里的lpfnWndProc字段指向的事件句柄所定义的那样,被发往同一个WinProc。图2-11详细示意了这种情况下的消息流程。
图2-11:The message flow for multiple windows with the same Windows class.
这可能是,也可能不是你所想要的。如果你希望每个窗口有自己的WinProc,你必须创建多于一个的窗口类,并用不同的类来创建各自的窗口。于是,对于每一个窗口类,有不同的WinProc发送消息。图2-12体现了这一过程。
图2-12:Multiple Windows classes with multiple 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和对应的可执行文件DEMO2_5.EXE。运行.EXE的时候,你会看见类似图2-13的画面。请注意,当你关闭了两个窗口中的任意一个,另一个也随之关闭,应用程序就此结束运行。试试看你是否能想出办法使得可以一次仅关掉一个窗口。(提示:创建两个窗口类,并且仅当两个窗口都关闭以后才发送WM_QUIT消息。)
图2-13:The multiple-window program DEMO2_5.EXE.
总结
虽然我并不认识你,但我很激动,因为在此时此刻,你已经具有了可以更加深入地理解Windows编程的基础。你已经了解了Windows的架构、多任务,你也知道如何创建窗口类、注册类、创建窗口、编写事件循环和句柄,及很多其他知识点!轻轻拍拍自己的后背,对自己诚实地说:你干得真不错!
在下一章里,我们将读到更深入的Windows相关内容,例如使用资源、创建菜单、操作对话框及获取信息等。