【3D技术宅公社】XR数字艺术论坛  XR技术讨论 XR互动电影 定格动画

 找回密码
 立即注册

QQ登录

只需一步,快速开始

调查问卷
论坛即将给大家带来全新的技术服务,面向三围图形学、游戏、动画的全新服务论坛升级为UTF8版本后,中文用户名和用户密码中有中文的都无法登陆,请发邮件到324007255(at)QQ.com联系手动修改密码

3D技术论坛将以计算机图形学为核心,面向教育 推出国内的三维教育引擎该项目在持续研发当中,感谢大家的关注。

查看: 9552|回复: 23

[推荐]游戏编程起源

[复制链接]
发表于 2006-4-19 15:54:32 | 显示全部楼层 |阅读模式
这是一部非常棒的初学者入门游戏编程教材。虽然目前3D游戏大行其道,但是对于想急切入门的初学者来说2D游戏的编程是最好的开始,因为它屏蔽了繁杂的数学理论,同时又给于了初学者一些游戏编程的概念。当然如果你选择3D游戏开发为起步,那也不是一件坏事。就像初学C语言和C++语言时的选择一样。开始学习这游戏编程教材前,首先得学会C++/C的一些必要语句用法,对编程语言的熟练程度直接影响到你写出的代码的质量。

你还在用TC么?那么请你马上仍掉它吧,现在开始下面的教材里都是在VC中编写和编译的。如果你对VC很陌生,没关系它很友善,而且你也只需知道一点小小的操作即可以开始你的游戏编程之旅。在此祝你旅途愉快。

原著作者的邮箱:ironblayde@aeon-software.com 要用英文给作者发信哦!
原著作者的ICQ:53210499
【译:傻马乱踢:howsee@163.com
【水平有限,诚心一片◆傻马乱踢】

第一章 Windows编程基础

☆ 简介
本章目的是介绍WINDOWS编程基础。在本章结束时,你应该能够很好的工作了,虽燃可能是简单的WINDOWS程序。你需要有C语言的基础知识,我很少将C++的代码扩充到程序中。当然,由于WINDOWS本身就是面向对象的,一点类的知识是不会对你有什么损害的。如果你不熟悉C++,没有关系,我想你还是能从我这里学到大部分的东西。所有的程序代码都通过了MICROSOFT VISUAL C++6.0的编译,如果你还没有合适的编译器,弄一个同我一样的好了,它还是很棒的。开动吧!
☆ 开始
多数的Windows程序都需要Windows.h和Windowsx.h这两个头文件,要确保使用它们。当然,你还需要其它的标准的C的头文件,象stdio.h,conio.h等。除了这些,你还会经常看到在程序的开始有这样一行代码:
#define WIN32_LEANAND_MEAN
它表示Windows的头文件中将拒绝接受MFC的东西,这将加速你的build时间。如果你从没有打算应用MFC在你的游戏编程中,那就使用它吧。如果你以前从没有看过这种声明类型——在#define后,直接加上一个“单词”,那么它的作用就是有条件编译。看看下面的例子:

#ifdef DEBUG_MODE
printf("Debug mode is active!");
#endif

意思是:如果程序的开始包含#define DEBUG_MODE,那么就printf(),否则退出。这个对于你跟踪程序的逻辑错误是很有帮助的。


☆ WinMain()函数

DOS下的C语言从main()开始,Windows下的C语言从WinMain()开始,一个空的WinMain()函数是这样的:

int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
return(0);
}

一个函数即使什么也没做,也应该返回一个值。是的,有好多东西我们不熟悉。首先的首先,WINAPI是个什么声明?WINAPI是在windows.h头文件中定义的一个宏,它把函数调用翻译成正确的调用约定。当我们在程序中需要用到汇编语言的时候,我们在来深究它好了,记住,如果要用WinMain(),就必须要有WINAPI。

下一步让我们来看看括号里的四个参数:
HINSTANCE hinstance:HINSTANCE是一个句柄类型的标识符。变量hinstance是一个整数,用于标识程序实例。Windows设置这个参数的值,并把它传递给你的程序代码。很多Windows函数都要用到它。
HINSTANCE hPreInstance:你不用担心这个参数,它已经被废掉了。它只是为古老的Windows版本服务的。你将还会看到类似的情况。
LPSTR lpCmdLine:是一个指向字符串的指针,它仅在程序名是从DOS命令行输入或是从Run对话框中输入时才起作用。因此,很少被程序代码所用。
int nCmdShow:决定了窗口在初始显示时的状态。Windows通常给这个参数分配一个值。通常是SW_打头的一个常量。例如SW_SHOWNORMAL表示默认状态,SW_MAXINIZE或SW_MINIMIZE分别表示最大和最小模式等等。

以上大体上是WinMain()的参数的介绍。下面对变量、参数、常量、类等的命名方法介绍一下。

☆ 匈牙利命名法
【不翻译这一段了,相关的资料很多】

☆ 消息
当你在DOS下编程的时候,你不必担心其它程序的运行,因为DOS是独占模式。但你在Windows平台上编程时,你不得不考虑其它正在运行的程序。鉴于此,Windows通过“消息”来连接操作申请和具体操作。简单的说,就是我们指示程序或程序本身向Windows发出诸如移动窗口、放大窗口、关闭窗口等地申请,Windows再根据申请,考察实地情况,拒绝或发出指令,让程序(计算机)作出相应的动作。再例如,鼠标随时向Windows发出消息,汇报光标位置,左键或右键是否按下等,Windows再根据消息作出相应的反应。总之,无论何时,Windows都要随时掌控所有的消息,而且,Windows是一直不断地接收到各种消息。
这种功能是通过一种被命名为CALLBACK函数类型实现的。不用害怕,消息的传递来,传递去都是由Windows自己完成的,你只要声明一个CALLBACK函数就可以了,就像WINAPI用在WinMain()前一样。如果还没有明白,不要紧,往下看你就明白了。现在,我要离开这个话题一会,因为你只有先建立窗口(Windows),传递消息才有可能实现。

☆ 窗口类
现在谈论一点C++的知识,因为要想建立一个窗口,你就得先建立一个窗口类。窗口类包含所有的有关窗口的信息,如用什么样的鼠标符号,菜单样式等等。开发任何一个窗口程序,都离不开窗口类的建立。为了达到此目的,你必须填写WNDCLASSEX结构。EX的意思是“扩充”的意思,因为有一个老的结构叫作WNDCLASS,这里,我们将使用WNDCLASSEX结构,它的样子如下:

typedef struct _WNDCLASSEX {
UINT cbSize;
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
HICON hIconSm;
} WNDCLASSEX;

这个结构有不少成员,讨厌的是,你必须为窗口类设置每一个成员。莫发愁,纸老虎一个。让我们来个速成。
UINT cbSize:指定了以字节为单位的结构的大小。这个成员是通过sizeof(WNDCLASSEX)实现的。你将会经常看到它,尤其是你使用了DirectX。
UINT style:指定了窗口的风格。它经常被以CS_打头的符号常量定义。两种或两种以上的风格可以通过C语言中的“或”(|)运算符加以组合。大多数情况我们只应用四种风格,出于对文章长度的考虑,我们只列出这四种。若你还需要其它的,到MSDN里找一下好了。别告诉我你用的不是Visual C++啊!
◎ CS_HREDRAW:一旦移动或尺寸调整使客户区的宽度发生变化,就重新绘制窗口。
◎ CS_VREDRAW:一旦移动或尺寸调整使客户区的高度发生变化,就重新绘制窗口。
◎ CS_OWNDC:为该类中的每一个窗口分配一个唯一的设备上下文。
◎ CS_DBLCLKS:当用户双击鼠标时向窗口过程发送双击消息。
WNDPROC lpfnWndProc:是指向窗口过程的指针。一般都指向CALLBACK函数。如果你没有用过函数指针,简单理解为函数的地址就是函数的名字,名字后面别带括号。
int cbClsExtra:它是为类保留的额外信息 。大多数程序员不用它,你在在写游戏程序时也不太可能用它,所以,设为0好了。
int cbWndExtra:同上一个差不多,设为0好了。
HANDLE hInstance:是指向窗口过程实例的句柄。同时也是WinMain()函数的参数之一。应该设置为hinstance。
HICON hIcon:指向窗口图标的句柄,它通常被LoadIcon()函数设置。在你学会如何在你的程序中使用资源前,你先设置成如下样子:LoadIcon(NULL,IDI_WINLOGO)。当然,还有一些其它的IDI_打头的符号常量,你自己去帮助文件里找吧。

HCURSOR hCursor:指向窗口光标的句柄,它通常被LoadCursor()函数设置,在你学会如何在你的程序中使用资源前,你先用Windows默认的吧,LoadCursor(NULL,IDC_ARROW)。
HBRUSH hbrBackground:当你的窗口过程得到消息,要求刷新(或重画)窗口时,至少要用一种纯色或“brush”(画刷)重画窗口区域,画刷是由参数确定的。你可以使用GetStockObject()函数调用几种常备的画刷,如BLACK_BRUSH, WHITE_BRUSH, GRAY_BRUSH等。现在,你就用GetStockObject(BLACK_BRUSH)吧。对不起,你可能觉得我说的太简单了,但我不想把开始弄得太复杂。我在以后的篇幅里会详细讲的,我保证。
LPCTSTR lpszMenuName:如果你想建立一个有下拉菜单的窗口,你得给这个参数赋一个菜单名称(这涉及到资源),由于你还不知道怎么创建菜单,你就先用NULL设置成一个没有菜单的窗口吧。
LPCSTR lpszClassName:很显然,你需要给类起个名字,随你便,如“TMD”。要用双引号引上啊!
HICON hIconSm:指向小图标的句柄。小图标用来显示在窗口的标题栏里。要用到LoadIcon()函数,现在,你就用Windows默认的吧,LoadIcon(NULL,IDI_WINLOGO)。
好了,你关于WNDCLASSEX结构知道的差不多了,你可以自己设置它了。下面是一个例子:

WNDCLASSEX sampleClass; // declare structure variable

sampleClass.cbSize = sizeof(WNDCLASSEX); // always use this!
sampleClass.style = CS_DBLCLKS | CS_OWNDC | CS_HREDRAW | CS_VREDRAW; // standard settings
sampleClass.lpfnWndProc = MsgHandler; // we need to write this!
sampleClass.cbClsExtra = 0; // extra class info, not used
sampleClass.cbWndExtra = 0; // extra window info, not used
sampleClass.hInstance = hinstance; // parameter passed to WinMain()
sampleClass.hIcon = LoadIcon(NULL, IDI_WINLOGO); // Windows logo
sampleClass.hCursor = LoadCursor(NULL, IDC_ARROW); // standard cursor
sampleClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); // a simple black brush
sampleClass.lpszMenuName = NULL; // no menu
sampleClass.lpszClassName = "Sample Class" // class name
sampleClass.hIconSm = LoadIcon(NULL, IDI_WINLOGO); // Windows logo again

【“//”后面的我就不翻译了】

我想,你已经有点儿不太崇拜Windows程序员了。言归正传,有一点我得提醒你,注意函数GetStockObject()前的(HBRUSH)类型配置,这是因为GetStockObject()可以调用其它的对象,不仅仅是“brush”,所以你需要一个HBRUSH类型配置。在Visual C++旧版本里不用配置,但新的6.0版本需要它,否则会编译出错。
下一件事是注册这个窗口类,只有这样,你才能创建新的窗口。十分简单,你只需要调用一个RegisterClassEX()函数,它只有一个参数,就是你的窗口类的地址(名字),根据我上面给的例子,这里应该这样:
RegisterClassEx(&sampleClass);
嗨,我们的窗口类创建完了,我们可以用它创建一个窗口了。只是时间问题喽!

☆ 创建窗口
待续。。。。

 楼主| 发表于 2006-4-19 16:05:02 | 显示全部楼层
★ 第五章 DirectDraw的调色板和象素

☆ 简介

今天我们将分别使用调色板和RGB模式来熟悉DirectDraw的基本图形。它们有什么不同呢?如果你曾经在DOS下编程,你可能使用过调色板映射模式。调色板是个颜色查询表,为了绘制象素,你将一个单独的字节写入视频内存,通过这个字节你可以索引到一个拥有各种颜色的链表,这个颜色的链表,或查询表就叫作调色板。而RGB模式是不同的,因为它不需要颜色查询表。在RGB模式下绘制一个象素,你可以直接把红色、绿色和蓝色的值写入视频内存。任何色彩深度高于8位的调色板都可以用RGB模式取代。
编写本章时,我假设你已经读过了前面几章,知道了怎样设置DirectDraw和创建表面。我们将使用DirectX7,它包含了最新的DirectDraw接口。实际上,DirectX 7 中的DirectDraw接口可能是最后的升级版本了!不用担心,未来的版本一定会兼容它的,但是未来可能是一个DirectDraw和Direct3D综合的产品,管它那,我们学的不会没有用的。^_^
在开始前我还有最后一件事要提醒你:在我的后续文章中关于调色板的部分可能再也用不到了,所以,如果你对于调色板模式不是很感兴趣,你可以跳过文章的前一部分,从象素格式开始看起。调色板的开发和使用是PC中使用的原始视频系统的内存限制带来的直接后果。现在由于充足的显存使调色板模式几乎废弃不用了。值得保留调色板模式的一个原因是,执行调色板操作可以实现一些有趣的动画效果。不罗嗦了,让我们开始吧!

☆ 创建DirectDraw的调色板

当你在色彩深度为8位或低于8位的模式下显示图形时,你必须创建调色板,也就是颜色查询表。更明确的讲,对于DirectX,调色板就是PALETTEENTRY结构。要建立一个调色板,我们要做如下三步:
1、 创建颜色查询链表。
2、 得到指向IDirectDrawPalette接口的指针。
3、 把调色板链接到DirectDraw表面。

我假设我们使用的是8位色彩深度。如果你要用16位或更高位的色彩深度编写游戏,你就不用继续看以下这段疯狂的Windows素材了。总之,8位色彩深度,我们可以有一个256个条目的调色板。所以,创建颜色查询链表,有256个条目在其中:

typedef struct tagPALETTEENTRY { // pe
BYTE peRed;
BYTE peGreen;
BYTE peBlue;
BYTE peFlags;
} PALETTEENTRY;

头三个参数很明显,分别是红色、绿色和蓝色的强度。每一个取值范围0-255,BYTE是无符号数据类型。最后一个参数是控制标志,应该设置为PC_NOCOLLAPSE。原因我就不说了。
现在,我们需要把256个条目有秩序的排列好,也就是为了一下能找到,我们为链表设置一个数组,象这样:
PALETTEENTRY palette[256];
Ok,我们有了数组了,你可以装载颜色了。当我工作在调色板模式下时,通常把颜色存储在一个外部文件里,然后用一些如下的东东装载颜色:

FILE* file_ptr;
int x;

if ((file_ptr = fopen("palette.dat", "rb")) != NULL)
{
fread(palette, sizeof(PALETTEENTRY), 256, file_ptr);
fclose(file_ptr);
}

All right,第一步完成了。现在我们需要得到调色板的接口。交给IDirectDraw7::CreatePalette()函数就好了:

HRESULT CreatePalette(
DWORD dwFlags,
LPPALETTEENTRY lpColorTable,
LPDIRECTDRAWPALETTE FAR *lplpDDPalette,
IUnknown FAR *pUnkOuter
);

返回类型是HRESULT,你知道它的,所以可以用FAILED()和SUCCEEDED()这两个宏检测函数是否调用成功。参数的说明如下:
DWORD dwFlags:描述调色板对象的标志常量。当然,你可以用“|”组合它们:
◎ DDPCAPS_1BIT:1位色彩,对应2色调色板。
◎ DDPCAPS_2BIT:2位色彩,对应4色调色板。
◎ DDPCAPS_4BIT:4位色彩,对应16色调色板。
◎ DDPCAPS_8BIT:8为色彩,对应256色调色板。
◎ DDPCAPS_8BITENTRIES:指出引用一个8位色彩索引。就是说,每个颜色条目是它本身的到目的表面8位调色板的索引。这叫作被变址的调色板。它必须同DDPCAPS_1BIT、DDPCAPS_2BIT,或者DDPCAPS_4BIT合用。乱套吧!^_^
◎ DDPCAPS_ALPHA:每一个PALETTEENTRY的peFlags成员都应该被认为是阿尔发值。用这些标志创建的调色板可以被粘贴在Dierct3D纹理表面,因为DirectDraw本身并不支持阿尔发混合。
◎ DDPCAPS_ALLOW256:允许8位调色板的全部256个条目被使用。通常,0指向黑色,255指向白色。
◎ DDPCAPS_INITIALIZE:指出应该用PALETTEENTRY的数组初始化调色板。
◎ DDPCAPS_PRIMARYSURFACE:调色板将链接到主表面,好快速更改显示颜色。
◎ DDPCAPS_VSYNC:一般画圆时用到它。
大多数情况,你将使用DDPCAPS_8BIT | DDPCAPS_INITIALIZE,如果你刚好想建立一个空的调色板,稍后再设置它,你可以去掉后者,就是DDPCAPS_INITIALIZE。当然,你还可以使用DDPCAPS_ALLOW256,如果你真的想改变这两个常用的条目。
LPPALETTEENTRY lpColorTable:这个指针指向我们创建的查询表,把数组的名称传递给它就好了。
LPDIRECTDRAWPALETTE FAR *lplpDDPalette:这是指向IDirectDrawPalette接口指针的地址。如果函数调用成功,它将被初始化。
IUnkown FAR *pUnkOuter:同以前一样,这总是为COM高级应用准备的。设置为NULL好了。

不是太糟糕吧!现在我们可以建立我们的调色板对象了。最后一步是把调色板链接到一个表面,这只需要一个函数就好了——IDirectDrawSurface7::Setpalette()。它的原形如下:

HRESULT SetPalette(LPDIRECTDRAWPALETTE lpDDPalette);

很简单,是不是?你只要把上一步得到的接口指针传递给它就可以了。那好,让我们把学到的综合到一起,下面我给你一个程序框架,我假设我们已经利用调色板的数组建立了一个索引链表,就像我们上一步做的。该框架是建立DirectDraw调色板来控制颜色,并且把它链接到主表面(当然,主表面是我们事先做好的):

LPDIRECTDRAWPALETTE lpddpal;

// create the palette object
if (FAILED(lpdd7->CreatePalette(DDPCAPS_8BIT | DDPCAPS_INITIALIZE, palette, &lpddpal, NULL)))
{
// error-handling code here
}

// attach to primary surface
if (FAILED(lpddsPrimary->SetPalette(lpddpal)))
{
// error-handling code here
}

就是这么简单。一旦你的调色板建立完成,绘制象素部分同RGB模式就没有什么不同了。从此时开始,我将同时介绍RGB模式和调色板模式,在我们真正的显示图象前,我需要告诉你什么是RGB象素格式。

☆ 象素格式

象我前面说过的,当你把一个调色板模式的象素写入内存时,你同时分配了一个字节,每个字节表示一个到色彩查询表的索引。在RGB模式下,你只需要把颜色描述值写入内存,但每个颜色需要的字节数都要多于一个字节。字节的多少同色彩的深度相关。对于16-bit色彩,你要为每个象素准备两个字节(16位),以此类推,你可以猜到32-bit色彩是怎么回事了,这些都是很容易理解的。32-bit色彩对于一个象素来说,每一位的字符如下:

AAAA AAAA RRRR RRRR GGGG GGGG BBBB BBBB

“A”是代表“alpha”(阿尔发),表示一个透明的值,这是为Direct3D准备的。我以前说过,DirectDraw不支持α混合,所以当你为DirectDraw创建32-bit色彩时,把高位都设置为0好了。下一个8位表示红色强度的值,再下一个8位表示绿色,最后8位表示蓝色。
一个32-bit色彩的象素需要32位,所以我们一般用UINT类型来定义相对应的变量类型,这是一个无符号实数类型。通常我用一个宏来把RGB数据转换成正确的象素格式。让我给你看看它的样子,希望这能更好的帮助你理解象素格式:

#define RGB_32BIT(r, g, b) ((r << 16) | (g << 8) | (b))

就象你看到的,这个宏通过位移在相应的位置写入了相应的红、绿、蓝的强度值,并且完全符合正确的象素格式。是不是开始感觉有点儿入门了?要建立一个32-bit的象素,你就可以调用这个宏。红、绿、蓝每一个颜色的强度值都是8位,它们的取值范围都是从0——255。例如建立一个白色的象素,你可以这样:

UINT white_pixel = RGB_32BIT(255, 255, 255);

24-bit色彩基本相同,道理实际上是一样的,只是24-bit没有关于α的描述,也就是少了α那8位。象素格式如下:

RRRR RRRR GGGG GGGG BBBB BBBB

所以红色、绿色、蓝色仍然都分别是8位,这就意味着24-bit色彩和32-bit色彩实际上是有相同颜色深度的,只是32-bit多了个α混合。现在,你一定会想,24-bit比32-bit要好,真的是这样吗?否,因为使用24-bit有一些麻烦,事实上没有24-bit的数据类型,在你建立象素时,你不得不分三步写入红、绿、蓝的强度值,而不是象32-bit一次就完成。尽管32-bit色彩需要更多的内存,但在大多数的机器上,它要更快一些。实际上,很多显示卡不支持24-bit色彩模式,因为每一个象素占用3个字节是很不方便的。
现在,轮到16-bit色彩了,它有一点儿小麻烦,因为对于16-bit色彩,不是每一种显示卡都使用相同的象素格式!有两种格式。其中一种,也是比较流行的,红色占据5位,绿色占据6位,蓝色占据剩下的5位。另一种格式是分别都占据5位,剩下的一位,也就是高位不使用,一些老的显示卡都使用这种格式。所以这两种格式看起来是这样的:

565 format: RRRR RGGG GGGB BBBB
555 format: 0RRR RRGG GGGB BBBB

当你工作在16-bit色彩深度下,你首先需要检测显示卡是支持565格式还是555格式,然后使用适当的方式。这是很讨厌的,但你坚持用16-bit色彩,这是没有办法避免的。由于存在两种格式,你就需要两种宏:

#define RGB_16BIT565(r, g, b) ((r << 11) | (g << 5) | (b))
#define RGB_16BIT555(r, g, b) ((r << 10) | (g << 5) | (b))

对于565格式,红色和蓝色的取值范围是0——31,绿色是0——63;对于555格式,取值范围都是0——31,所以当要创建一个白色象素时,就会有所不同:

USHORT white_pixel_565 = RGB_16BIT565(31, 63, 31);
USHORT white_pixel_555 = RGB_15BIT555(31, 31, 31);

这个USHORT是无符号短实数类型,对应的变量只有16位。存在两种格式把事情搞得有些复杂,但在实际的游戏编程过程中,你将会感觉到这并没有你想象的那么讨厌。顺便说一下,有些时候555格式被称为15-bit色彩深度,所以在以后如果我这样谈到了它,你一定要心领神会哦!^_^
现在或许是告诉你在16-bit色彩深度模式下,怎样检测显示卡到底支持哪种格式的时机了,是555还是565呢?最简单的办法就是调用IDirectDrawSurface7接口下的GetPixelFormat()函数,它的原形如下:

HRESULT GetPixelFormat(LPDDPIXELFORMAT lpDDPixelFormat);

参数是指向DDPIXELFORMAT结构的指针。你只要声明它,初始化它,然后传递它的地址就一切OK了。这个结构的本身是巨大的,所以我就不列举它了,但我得告诉你它的三个成员,都是DWORD类型的,它们是dwRBitMask、dwGBitMask、和dwBBitMask。你可以从dwRBitMask、dwGBitMask和dwBBitMask中获得掩码值(新东东,先不用太明白)。你也可以用它们检测显示卡支持的格式。如果显示卡支持565,dwGBitMask将为0x07E0。如果是555格式,dwGbitMask为0x03E0。
现在,我们已经学习了所有我们可能用到的象素格式,可以进入在DirectX下显示图象的实际阶段了。你已经等待了很久了,不是吗?在把象素放到表面上前,我们需要锁定表面,至少是锁定表面的一部分。锁定表面返回一个指向表面在内存里位置的指针,然后,我们就可以为所欲为了。
 楼主| 发表于 2006-4-19 16:05:58 | 显示全部楼层
☆ 锁定表面

没什么令人意外的东东,我们将使用的函数是IDirectDrawSurface7:ock()。让我们仔细看看它:

HRESULT Lock(
LPRECT lpDestRect,
LPDDSURFACEDESC lpDDSurfaceDesc,
DWORD dwFlags,
HANDLE hEvent
);

一定要检测函数的调用是否成功,否则可能会有大麻烦的:如果锁定失败,而返回的指针指向了一个不正确的内存区域,你若操控该区域,很有可能导致系统的混乱。函数的参数有以下这些组成:
LPRECT lpDestRect:是一个指向RECT结构的指针,它描述了将要被直接访问的表面上的矩形区。该参数被设置为NULL,以锁定整个表面。
LPDDSURFACEDESC2 lpDDSurfaceDesc:是DDSURFACEDESC2类型的结构变量的地址,它由直接访问表面内存所必需的全部信息进行填充。在该结构中返回的信息表面的基地址、间距和象素格式。
DWORD dwFlags:好像没有几个DirectX函数没有这个东东的。下面列出几个最有用的标志常量:
◎ DDLOCK_READONLY:被锁定的表面为只读。(当然就不能写入了)
◎ DDLOCK_SURFACEMEMORYPTR:表面返回一个指向锁定矩形左上角坐标的有效指针;如果没有指定矩形,那么返回表面左上角的坐标。此项为默认且无需显式的输入。
◎ DDLOCK_WAIT:如果其它线程或进程正在进行位转换操作,不能锁定表面内存,则一直等到该表面可以锁定为止或返回错误信息。
◎ DDLOCK_WRITEONLY:被锁定表面为可写。(当然就不能读取了)
由于我们使用锁定去操控象素,你将总会用到DDLOCK_SURFACEMEMORYPTR。即使我们目前还没有学习位块操作,但使用DDLOCK_WAIT总是一个好主意。
HANDLE hEvent:没用的东东,设置为NULL好了。

一旦我们锁定了表面,我们需要查看一下DDSURFACEDESC2结构来获取一些表面信息。我们以前介绍过这个结构,但在这里,针对现在的课题,我们只需要它的两个成员。由于它们都很重要,我就再重复一遍:
LONG lPitch:这个lPitch成员表示每个显示行的字节数,也就是行间距。例如,对于640×480×16模式,每一行有640个象素,每一个象素需要两个字节存放颜色信息,所以行间距应该为1280个字节,对不对?Well,对于一些显示卡,它的长度大于1280,每行上多于的内存不存放任何的图象数据,但你必须让它存在,因为这种显示卡在某种显示模式下不能创建线性内存模式。的确,这种显示卡的比例很小,但你需要考虑到它。
LPVOID lpSurface:这是指向内存中表面的指针。不管你使用何种显示模式,DirectDraw都创建一个线性地址模式,使你能够操控表面上的象素。

这个lpSurface指针是很容易理解的,而行间距是一个需要记住的重要值,因为你将必须使用它去计算特殊象素的偏移量。
我们过一会儿在细说,因为有一件事我们现在必须知道,当对锁定的表面操作完成后,你需要释放这个锁定表面,这个函数IDirectDrawSurface7::Unlock()的原形为:

HRESULT Unlock(LPRECT lpRect);

参数同你传递给Lock()函数的要保持一致。都准备好了,让我们画一些象素吧!

☆ 绘制象素

首先是确定从Lock()函数得到的指针类型。逻辑上,我们希望指针的大小同象素的大小要保持一致。所以我们为8-bit色彩深度分配了UCHAR*类型,USHORT*是16-bit的,UINT*是32-bit的。但是24-bit怎么办呢?因为没有与之相对应的数据类型,我们还是使用UCHAR*类型,但具体操作有一些不同。
我们也应该把lPitch成员转换成与指针相同的单位。记得吗,当我们第一次从DDSURFACEDESC2结构得到lPitch时,它是以字节为单位。对于16-bit模式,我们应该把它除以2以适应USHORT,对于32-bit我们应该把它除以4以适应UINT。
在我们进行第二步前先看看实例代码。假设我们在32-bit模式锁定主表面来绘制象素。以下是代码:

// declare and initialize structure
DDSURFACEDESC2 ddsd;
INIT_DXSTRUCT(ddsd);

// lock the surface
lpddsPrimary->Lock(NULL, &ddsd, DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR, NULL);

// now convert the pointer and the pitch
UINT* buffer = (UINT*)ddsd.lpSurface;
UINT nPitch = ddsd.lPitch >> 2;

现在让我先一步告诉你象素绘制函数,然后我再解释:

inline void PlotPixel32(int x, int y, UINT color, UINT *buffer, int nPitch)
{
buffer[y*nPitch + x] = color;
}

All right,让我分别解说。首先,你可能已经注意到了我把它声明为一个inline函数,目的是消除传递所有参数时的辅助操作,例如每次我们想要做些简单的事情(如绘制一个象素)。在函数里,仅用了一行就定位了我们要绘制的点和设置了该点的颜色。注意,颜色仅仅是一个值,不是由红、绿、蓝分别组成的,所以我们需要使用宏RGB_32BIT()来设置这个颜色值。
公式用来定位要绘制象素的具体位置——y*nPitch + x 。nPitch表示行间距,被y乘后就得到了正确的行数,再加上x,就得到了正确的位置。这就是你需要知道的,很简单吧!让我再告诉你在8-bit和16-bit模式下绘制象素的函数,它们都十分相象:

inline void PlotPixel8(int x, int y, UCHAR color, UCHAR* buffer, int byPitch)
{
buffer[y*byPitch + x] = color;
}

inline void PlotPixel16(int x, int y, USHORT color, USHORT* buffer, int nPitch)
{
buffer[y*nPitch + x] = color;
}

几个函数间唯一不同的就是参数数据类型的不同。应该还记得对于8-bit色彩深度,间距是以字节表示,对于16-bit,间距是以USHORT类型表示。现在,只剩下一个模式没有说了,就是24-bit模式。由于没有相应的数据类型,我们需要分别传递红、绿、蓝三个值,函数看起来应该如下:

inline void PlotPixel24(int x, int y, UCHAR r, UCHAR g, UCHAR b, UCHAR* buffer, int byPitch)
{
int index = y*byPitch + x*3;

buffer[index] = r;
buffer[index+1] = g;
buffer[index+2] = b;
}

如你所看到的,它将工作慢一些,因为它多了一次乘法运算,并且有三次内存写操作。你可以用其它方法替换x*3加快一些速度,如(x+x+x)或者(x<<1)+x,但是不会有太大效果的。当然,她还没有到应该放弃的地步。现在你就明白了为什么说24-bit色彩深度有点儿讨厌了吧!

☆ 关注速度

你应该采取一些行动使程序尽可能会的运行。首先,锁定一个表面并不是最快的,所以你要试图锁定表面上你要操作的最小矩形区域。对于很多操作,包括很简单的绘制象素演示程序,你都应该锁定最小的矩形区域。
第二,让我们就640×480×16模式来说,间距总是1280个字节,你应该试图考虑有没有更好的办法表述它。当然,1280个字节你是不能改变的,但我们可以使公式最优化,用位移来替代乘法是一贯的加速方法。我们先前的公式是这样的:
buffer[y*nPitch + x] = color;
如果我们知道nPitch将会是640(由于nPitch是USHORT类型,不是字节),我们就可以加速它(我们本来就知道它是640)。640不是一个理想的位移数字,但512是2的9次幂,128是2的7次幂,你猜到了吧,512+128=640。^_^ 很棒吧?我们就可以用下面这个更快的公式取代先前的公式:
buffer[(y<<9) + (y<<7) + x] = color;
多数的解决办法都是分解成2的几次幂,有的需要动一点儿脑筋,如800×600(512+256+32=800),小菜一碟哦!位移是我们应用的最快的运算符。
最后,如果你要使用两个函数—— 一个做乘法运算,一个做位移运算,要将比较判断放到循环的外部,不能象下面这样:

for (x=0; x<1000; x++)
{
if (nPitch == 640)
PlotPixelFast16();
else
PlotPixel16();
}

判断部分使你的优势殆尽,你应该这样做:

if (nPitch == 640)
{
for (x=0; x<1000; x++)
PlotPixelFast16( parameters );
}
else
{
for (x=0; x<1000; x++)
PlotPixel16( parameters );
}

有意义吧?无论何时用大的循环,都应该尽量把判断放到循环的外部,没有必要进行上千次同样的比较判断。同理,如果你要绘制象素,形成有规律的图案,如水平线或垂直线,甚至是斜线,你都没有必要每一次都重复确定象素的位置。看看下面的例子,画一条任意颜色的直线:

for (x=0; x<640; x++)
PlotPixel16(x, 50, color, buffer, pitch);

函数每次都重复计算正确的行,你可以一次就把行指定好。下面是快一点儿的做法:

// get the address of the line
USHORT* temp = &buffer[50*pitch];

// plot the pixels
for (x=0; x<640; x++)
{
*temp = color;
temp++;
}

你可能认为节省这么一点点时间意义不大,但当你进行千万次的循环时,意义就很大了。游戏程序员总是想办法提高游戏的速度的。
看看以前的文章,我们已经进行了好长时间的铺垫了。现在,我们知道了怎样绘制象素了,让我们看看能用现在学到的做些什么。

☆ 淡出操作

在游戏中最常用到的屏幕操作就是淡出成黑色,或者从黑色淡入。两种方式是同样的机理:你简单画出你的图象,然后申请一些屏幕转换来改变图象的亮度。对于淡出,你减少亮度从100%——0%;对于淡入,你增加亮度从0%——100%。如果你工作在调色板模式,这很容易做到,你只要改变你的调色板的颜色就可以了。如果你工作在RGB模式下,你得考虑一些其它方法。
现在,我将说一说屏幕淡入、淡出相对好一些的方法。你可以使用Direct3D,它支持α混合,先设定每一帧的纹理,然后设置透明层;或者,更容易的方法,你可以使用DirectDraw的color/gamma控制。但是,如果你仅仅希望屏幕的一部分进行淡入或淡出的操作,或者淡入或淡出一种非黑色的颜色,而且你又不是一个Direct3D的高手——我本人就不是!——那么具体做法的手册就在你眼前。现在,你所需要做的最基本的就是读取每一个你需要控制的象素,然后把它分解成红色、绿色和蓝色,然后你把三个值分别乘以要淡出或淡入的级别,再合成RGB值,把新的颜色值写回缓冲区。听起来很复杂?别害怕,没有想象的那么坏。看看下面这段演示代码,它演示了屏幕左上角200×200区域的淡出效果,是16-bit色彩深度和565格式:

void ApplyFade16_565(float pct, USHORT* buffer, int pitch)
{
int x, y;
UCHAR r, g, b;
USHORT color;

for (y=0; y<200; y++)
{
for (x=0; x<200; x++)
{
// first, get the pixel
color = buffer[y*pitch + x];

// now extract red, green, and blue
r = (color & 0xF800) >> 11;
g = (color & 0x0730) >> 5;
b = (color & 0x001F);

// apply the fade
r = (UCHAR)((float)r * pct);
g = (UCHAR)((float)g * pct);
b = (UCHAR)((float)b * pct);

// write the new color back to the buffer
buffer[y*pitch + x] = RGB_16BIT565(r, g, b);
}
}
}

现在,这个函数有很多不稳妥的地方。首先,计算象素的位置公式不但包含在循环中,而且还出现了两次!你可以在整个程序中只计算它一次,但现在的代码计算了它80000次!^_^ 下面是你应该做的:在函数的开始部分,你应该声明一个USHORT*的变量,让它等于buffer(如USHORT* temp = buffer;)。在内部循环里,增加一个指针使其能得到下一个象素;在外部循环,增加一行(temp+=jump;),使其能转入下一行。下面是修改后的代码:

void ApplyFade16_565(float pct, USHORT* buffer, int pitch)
{
int x, y;
UCHAR r, g, b;
USHORT color;
USHORT* temp = buffer;
int jump = pitch - 200;

for (y=0; y<200; y++)
{
for (x=0; x<200; x++, temp++) // move pointer to next pixel each time
{
// first, get the pixel
color = *temp;

// now extract red, green, and blue
r = (color & 0xF800) >> 11;
g = (color & 0x0730) >> 5;
b = (color & 0x001F);

// apply the fade
r = (UCHAR)((float)r * pct);
g = (UCHAR)((float)g * pct);
b = (UCHAR)((float)b * pct);

// write the new color back to the buffer
*temp = RGB_16BIT565(r, g, b);
}

// move pointer to beginning of next line
temp+=jump;
}
}
 楼主| 发表于 2006-4-19 16:06:25 | 显示全部楼层
这就好一些了吧!jump值是USHORT类型,是表示从200个象素宽的末尾(200个象素没有占满一行)到下一行开始的值。尽管如此,对于浮点运算和提取/还原颜色计算并没有提高速度。应该有办法的,看看这个:

USHORT clut[65536][20];

如果你要求一个DOS程序员把这么大的数组放入他的程序中,他可能痛苦的会哭出声来,甚至当场昏死过去,起码也要加速自然死亡。但在Windows中,如果你需要这样做,不会遇到什么麻烦的。因为你拥有整个系统的可利用内存。如果把整个的内循环替换成下面这一行,是不是很美妙的一件事呢?

*temp = clut[*temp][index];

这样做,又快了一些!^_^ 你可以传递一个0——100间的整数来替代浮点数传递给函数。如果为100,就不需要淡出的操作了,所以就返回“什么事儿也不用做”;如果为0,就用ZeroMemory()函数处理所有的工作好了。另外,把传递的数除以5,作为数组的第二个下标。
如果你对于我知道查询表的尺寸感到好奇,我就告诉你好了,65536是2的16次幂,所以在16-bit模式下,就有65536种颜色。既然我们的颜色值是无符号的值,它们的范围从0——65535,那么我们就用20作为淡出的增量值好了,反正考虑到相关的内存,我觉得挺合适的。
对于24-bit和32-bit模式,你显然不能直接使用颜色查询表,因为数组太巨大了,所以你只有使用三个小一点的数组:

UCHAR red[256];
UCHAR green[256];
UCHAR blue[256];

然后,每当你读取一个象素,就把它分解出的颜色值放入相应的数组,使其形成自己的查询表,经过变化,再组合到一起,得到RGB色彩值。有很多办法可以实现程序的优化,最好的办法是根据你的目的不断地测试哪一种是最适合你的程序的,然后总结经验,记住它。我下面将简单的介绍一下你可能用得着的其它的转换。

☆ 透明操作

把一个透明的图象覆盖在非透明的图象上,你就不能使用颜色查询表了,因为它总共需要有65536个查询表,一台普通的电脑就需要8.6GB的内存来处理这个庞然大物。^_^ 所以你不得不计算每一个象素。我将给你一个基本的思路。假设你要用图象A覆盖图象B,图象A的透明百分比为pct,这是一个0——1之间的浮点数,当为0时是完全不可见的,当为1时是完全可见的。那么,让我们把图象A的象素称作pixelA,相对应,图象B的象素称作pixelB。你将应用下面这个公式:

color = (pixelA * pct) + (pixelB * (1-pct));

基本上,这是一个两个象素颜色的平均值。所以,你实际上看到每个象素有6个浮点乘法运算。你可以用一些小型的查询表降低你的工作量。你真的应该试一试!
你或许想做的另一件事情是建立一个部分透明的纯色窗口。如果你看过了我做的一个RPG游戏的DEMO——地球人(http://www.aeon-software.com/framesok/tn_demo1.html )你或许明白我的意思。那种效果用一个颜色查询表完全可以达到。因为对于“地球人”,我只需要为屏幕上可能出现的颜色提供蓝色。实际上,我就是用查询表完成的。我将告诉你我实际的意思:

void Init_CLUT(void)
{
int x, y, bright;
UCHAR r, g, b;

// calculate textbox transparency CLUT
for (x=0; x<65536; x++)
{
// transform RGB data
if (color_depth == 15)
{
r = (UCHAR)((x & 0x7C00) >> 10);
g = (UCHAR)((x & 0x03E0) >> 5);
b = (UCHAR)(x & 0x001F);
}
else // color_depth must be 16
{
r = (UCHAR)((x & 0xF800) >> 11);
g = (UCHAR)((x & 0x07E0) >> 6); // shifting 6 bits instead of 5 to put green
b = (UCHAR)(x & 0x001F); // on a 0-31 scale instead of 0-63
}

// find brightness as a weighted average
y = (int)r + (int)g + (int)b;
bright = (int)((float)r * ((float)r/(float)y) + (float)g * ((float)g/(float)y) + (float)b * ((float)b/(float)y) + .5f);

// write CLUT entry as 1 + one half of brightness
clut[x] = (USHORT)(1 + (bright>>1));
}
}

这段代码来源于“地球人”,用查询表创建了一个文本框。为了安全起见,随处都使用了类型修饰。这段代码还能再快一些,但我没有很认真的优化,因为我只在游戏的最开始的部分调用了它一次。首先,红、绿、蓝的亮度值被提取出来,由于是16-bit模式,注意我们用了一个color_depth变量检测了显示卡是555还是565格式。然后,用下面公式计算了象素的亮度:

y = r + g + b;
brightness = r*(r/y) + g*(g/y) + b*(b/y);

这是一个理想的平均值。我不能确定是否颜色亮度值这样得到就正确,但它看起来符合逻辑,并且实际效果很好。在公式的最后我加了一个.5,因为当你把浮点数变为整数时,小数部分被去掉,加上.5使其凑整。最后,我把亮度除以2再加上1,这样不会使文本框太亮,加1使文本框不会全黑。由于16-bit模式的低位是蓝色,我可以只把颜色设置为蓝色,就不用宏了。理解了吗?最后,结束之前,我给你演示怎样创建文本框:

int Text_Box(USHORT *ptr, int pitch, LPRECT box)
{
int x, y, jump;
RECT ibox;

// leave room for the border
SetRect(&ibox, box->left+3, box->top+3, box->right-3, box->bottom-3);

// update surface pointer and jump distance
ptr += (ibox.top * pitch + ibox.left);
jump = pitch - (ibox.right - ibox.left);

// use CLUT to apply transparency
for (y=ibox.top; y<ibox.bottom; y++)
{
for (x=ibox.left; x<ibox.right; x++, ptr++)
*ptr = clut[*ptr];
ptr += jump;
}
return(TRUE);
}

这就是一个查询表,看起来更象淡出操作的代码了,就是查询表的控制值与前面的不一样了。这里用一个计算代替了20。^_^ 顺便说一下,对于查询表的一个声明,象下面这个:

USHORT clut[65536];

使用它,你可以做出更有趣的效果。
在你自己动手前,请先看看针对本章编写的例程代码,它可以从http://www.aeon-software.com/downloads/pixels.zip 下载。你应该试着改动它一下,用象素填满屏幕,然后在上面绘制一个透明的文本框。

☆ 总结

本章是为以象素为基础的图形服务的。下一章,我们将学习位图的知识。不管你信不信,使用位图要比象素简单多了,以后你就知道了。下一章将是学习DirectX基础知识的最后一章,在此之后,我们将编写一个RPG游戏。细节到时候你就知道了。

作者的邮箱:ironblayde@aeon-software.com 要用英文给作者发信哦!
作者的ICQ:53210499
【傻马乱踢:howsee@163.com】
 楼主| 发表于 2006-4-19 16:09:23 | 显示全部楼层
★ DirectDraw的位图化图形

☆ 简介

终于,你已经掌握了制作一个完整游戏的基础知识了,只不过你现在还只能使用GDI。今天,我们就学习使用DirectX来执行每一件你以前用GDI完成的工作,以及一些关于DirectX其它的东东。具体内容是:装载(调用)位图,使用位块传输,填充表面,使用剪裁板、颜色键等拷贝位图。
你可以在不了解前一章内容的基础上学习本章,但象素格式是很重要的,我将经常直接或间接的提到它,所以你至少应该看看上一章关于象素格式的部分!^_^ 另外,我假设你已经本系列的第一、二、三、四章,并且拥有一个DirectX SDK游戏开发平台。准备好了吗?发动引擎吧,女士们、先生们!

☆ 装载位图

不管你信不信,你的确已经知道了把位图装载到DirectDraw表面的大部分知识。怎么会这样呢?Well,在Windows GDI下装载位图同在DirectDraw下极其相似,只是有一点点不同。轻轻的回忆一下,我们曾经使用LoadImage()函数得到位图的句柄,然后把位图选入到内存设备上下文中,最后利用BitBlt()函数把图形从内存设备上下文中拷贝到显示设备上下文中,设备上下文可以用GetDC()函数得到。如果这个承担显示任务的就是DirectDraw表面(现在我们就是要用它),我们就可以针对性的得到DirectDraw表面的设备上下文!感谢上帝,IDirectDrawSurface7接口提供了一个极其简单的函数来得到这个设备上下文:

HRESULT GetDC(HDC FAR *lphDC);

该函数的返回类型同所有DirectDraw函数的返回类型相同。如果函数调用成功,参数就是一个HDC类型的设备上下文的指针,很简单吧!本章就是从把一个位图装载到DirectDraw表面讲起的。千万要记住使用完了表面设备上下文后,你一定要释放它哦!你可能已经想到了,用表面接口函数ReleaseDC()完成:
HRESULT ReleaseDC(HDC hDC);
你不用回头去看关于GDI部分的位图调用,我将把适合于DirectDraw的位图调用展现给你。唯一不同的是:不是直接把设备上下文作为一个参数,而是用一个DirectDraw表面指针取代了它,然后函数从表面得到设备上下文,用它来拷贝图形,最终释放设备上下文。(可能这里我说的有些混乱,但你看一下下面的程序代码就都明白了^_^):

int LoadBitmapResource(LPDIRECTDRAWSURFACE7 lpdds, int xDest, int yDest, int nResID)
{
HDC hSrcDC; // source DC - memory device context
HDC hDestDC; // destination DC - surface device context
HBITMAP hbitmap; // handle to the bitmap resource
BITMAP bmp; // structure for bitmap info
int nHeight, nWidth; // bitmap dimensions

// first load the bitmap resource
if ((hbitmap = (HBITMAP)LoadImage(hinstance, MAKEINTRESOURCE(nResID), IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION)) == NULL)
return(FALSE);

// create a DC for the bitmap to use
if ((hSrcDC = CreateCompatibleDC(NULL)) == NULL)
return(FALSE);

// select the bitmap into the DC
if (SelectObject(hSrcDC, hbitmap) == NULL)
{
DeleteDC(hSrcDC);
return(FALSE);
}

// get image dimensions
if (GetObject(hbitmap, sizeof(BITMAP), &bmp) == 0)
{
DeleteDC(hSrcDC);
return(FALSE);
}

nWidth = bmp.bmWidth;
nHeight = bmp.bmHeight;

// retrieve surface DC
if (FAILED(lpdds->GetDC(&hDestDC)))
{
DeleteDC(hSrcDC);
return(FALSE);
}

// copy image from one DC to the other
if (BitBlt(hDestDC, xDest, yDest, nWidth, nHeight, hSrcDC, 0, 0, SRCCOPY) == NULL)
{
lpdds->ReleaseDC(hDestDC);
DeleteDC(hSrcDC);
return(FALSE);
}

// kill the device contexts
lpdds->ReleaseDC(hDestDC);
DeleteDC(hSrcDC);

// return success
return(TRUE);
}

上面这段代码被设计成从资源调用位图,但你可以很容易就把它修改成从外部文件调用位图,或者更理想的是,首先你从资源调用位图,如果失败,再试图从外部文件调用位图。从外部调用,需要记住的是调用LoadImage()函数时加上LR_LOADFROMFILE标志。最美妙的事情是,函数BitBlt()自动完成象素格式的转换。举例说,当我们把24-bit的位图放入内存设备上下文,再把它传送(拷贝)到16-bit色彩深度的表面,所有的颜色将得到正确的显示,不用顾忌象素格式是555还是565,很方便吧,哦?
如果你要控制位图传递的实际过程,而不是使用BitBlt()这样简单的函数,你有两个选择。第一个,你可以修改这个函数,需要利用BITMAP结构的bmBits成员,它是一个组成图象的位的LPVOID指针变量。第二种方法,如果你真的想控制图象的调用过程,你可以自己编写函数,思路是使用标准的I/O函数来打开图象文件,然后读取它。要这样做,你需要了解位图文件的结构。我们将不涉及这种函数的编写,因为目前的对我们来说已经足够了,但我还是要为你将来的大展鸿图做一点点铺垫。^_^

☆ 位图格式

令人高兴的是,要自己写一个调用位图的函数,有一个Win32结构的位图头文件可以利用。读取这个头文件的信息,用fread()这样简单的函数就可以了。所有的位图文件都有这样一个头文件,它包含了位图的全部信息。BITMAPFILEHEADER就是这个头文件结构的名字,下面是它的原形:

typedef struct tagBITMAPFILEHEADER { // bmfh
WORD bfType; // file type - must be "BM" for bitmap
DWORD bfSize; // size in bytes of the bitmap file
WORD bfReserved1; // must be zero
WORD bfReserved2; // must be zero
DWORD bfOffBits; // offset in bytes from the BITMAPFILEHEADER
// structure to the bitmap bits
} BITMAPFILEHEADER;

我就不详细介绍这些成员了,因为注释里已经说得很清楚了,只要使用fread()读取它们就可以了。注意要检测bfType成员是否等于字符“BM”,若是,说明你正在处理一个有效的位图。在此之后,有另一个头文件需要读取,它包含位图的尺寸、压缩类型等图象信息。以下是它的结构:

typedef struct tagBITMAPINFOHEADER{ // bmih
DWORD biSize; // number of bytes required by the structure
LONG biWidth; // width of the image in pixels
LONG biHeight; // height of the image in pixels
WORD biPlanes; // number of planes for target device - must be 1
WORD biBitCount; // bits per pixel - 1, 4, 8, 16, 24, or 32
DWORD biCompression; // type of compression - BI_RGB for uncompressed
DWORD biSizeImage; // size in bytes of the image
LONG biXPelsPerMeter; // horizontal resolution in pixels per meter
LONG biYPelsPerMeter; // vertical resolution in pixels per meter
DWORD biClrUsed; // number of colors used
DWORD biClrImportant; // number of colors that are important
} BITMAPINFOHEADER;

只有几个成员需要解说一下。第一个,注意压缩格式。大多数的位图你都需要做解压缩的操作。最普通的位图压缩格式是run-length编码(RLE),但只能应用于4-bit或8-bit图象,在此情况时,成员biCompression将分别是BI_RLE4和BI_RLE8,我们就不讨论这种压缩格式了,但它真的很简单,很容易理解,你如果要了解它是不会有任何麻烦的。^_^
第二个,对于高色彩的位图,biClrUsed和biClrImportant这两个成员通常设置为0,所以不用太在意它们。对于BI_RGB这种未压缩格式的位图,成员biSizeImage也将被设置为0。最后,针对我们的目的,其它的结构成员都不是很重要的,我们只需要注意位图的长、宽和色彩的深度(biWidth、biHeight、biBitCount)。
读取完了这些头文件的信息后,如果位图是8-bit或者以下色彩深度的(也就是调色板模式),调色板的信息会紧跟在这些信息之后。也许出乎你的意料,调色板的信息不是存储在PALETTEENTRY结构中,而是在RGBQUAD结构中。RGBQUAD结构如下:

typedef struct tagRGBQUAD { // rgbq
BYTE rgbBlue;
BYTE rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
} RGBQUAD;

不要问我为什么红、绿、蓝以倒序方式排列,事实就是这样!读取RGBQUAD中的数据,把数据传递给DirectDraw调色板的数组。记得要把每个PALETTEENTRY的peFlag设置成PC_NOCOLLAPSE。
之后呢(调色板信息不一定存在,因为高彩模式下就没有),你将发现图象位(image bits),你可能会想到建立一个指针,在内存中分配足够的空间来控制这些图象位数据,然后读取它们。对极了,我正要这样干。假设把存储在BITMAPINFOHEADER结构中的信息头文件称作info,你的图象位指针称作fptr,实施的代码如下:

UCHAR* buffer = (UCHAR*)malloc(info.biSizeImage);
fread(buffer, sizeof(UCHAR), info.biSizeImage, fptr);

要记住,在一些情况下,biSizeImage的值可能为0,所以有必要在上面的代码运行前检测它一下。如果它被设置为0,你将不得不计算图象由多少个象素构成,每个象素需要多少个字节。
写你自己的位图调用函数,并非什么难事儿。但你觉得不需要,就用我们开始介绍的方法好了。这个话题告一段落,下面让我们看看DirectDraw的精华:使用位块传输。

☆ 使用位块传输

位块传输是显示卡操控位图数据的一部分,你同样可以用它来进行颜色填充。就像我们过一会儿看到的,随着硬件的性能提高,会有很多经典的技巧。DirectX有权使用硬件的加速功能,但要记住,如果DirectX使用的加速功能不被机器硬件支持,将自动启用硬件仿真层(HEL),但这也并非万无一失,因为有些功能靠硬件仿真层是无法实现的(否则谁还买3D加速卡^_^),所以你需要检测你的函数是否调用成功。
GDI位块传输可以在DirectDraw编程中使用,而且有时也的确是这样做的。然而DirectDraw具有其自身的位块传输函数,它们通常更加适合于编程环境,而且比GDI的相应的函数执行得更快。DirectDraw位块传输函数名为Blt()和BltFast(),都是有IDirectDrawSurface7接口提供的。两者不同处是BltFast()不处理剪切、放缩等其它Blt()做的有趣的事情。如果在硬件仿真层上,BltFast()要比Blt()快10%左右,但如果有硬件加速卡支持(硬件加速卡主要就是为位块传输服务的),二者的速度就差不多了,而且现在大多数的机器都有硬件加速卡,所以我总是使用Blt()。让我们仔细看看这个神奇的东东:

HRESULT Blt(
LPRECT lpDestRect,
LPDIRECTDRAWSURFACE7 lpDDSrcSurface,
LPRECT lpSrcRect,
DWORD dwFlags,
LPDDBLTFX lpDDBltFx
);

由于Blt()所拥有的最后一个参数,使其能做很多特殊的事儿。该参数配有一个标志常量列表,我将会向你介绍其中最有用的几个。另外,注意在把位图从一个表面向另一个表面传递时,你应该调用目的表面的Blt(),不是源表面的。好了吗?以下是函数的参数说明:
※ LPRECT lpDestRect:参数lpDestRect为指向结构RECT的指针,它给出了位块传输操作的目标表面的左上角和右下角的坐标。如果源表面和目标(目的)表面的大小不一致,Blt()将把源表面的图象自动按照比例适应目标表面的大小。如果此参数为NULL,则使用整个目标表面。
※ LPDIRECTDRAWSURFACE7 lpDDSrcSurface:参数lpDDSrcSurface为指向DirectDraw表面的指针,该DirectDraw表面为位块传输之源表面。如果你只是要用颜色填充目的表面,你可以把它设置为NULL。
※ LPRECT lpSrcRect:参数lpSrcRect为指向结构RECT的指针,它给出了位块传输(有的书上也叫作“位转换”)操作的源表面的左上角和右下角的坐标。如果此参数为NULL,则使用整个源表面。
※ DWORD dwFlags:对于这个参数有一个巨大的标志常量列表,可以用“|”组合使用标志常量。其中一些是为Direct3D服务的,所以我将把我们常用的列出来:

◎ DDBLT_ASYNA:位块传输异步的以先入先出(FIFO)的顺序接收。如果没有空间可用于FIFO硬件,则该调用失败。
◎ DDBLT_COLORFILL:使用DDBLTFX结构的数据成员dwFillColor作为RGB颜色填充目标表面的矩形。
◎ DDBLT_DDFX:DDBLTFX结构的dwDDFX成员指定了位块传输的使用效果。
◎ DDBLT_DDROPS:DDBLTFX结构的dwDDROP成员指定了光栅操作(ROPS),该操作不是Win32 API的一部分。
◎ DDBLT_KEYDEST:颜色键与目标表面相关联。
◎ DDBLT_KEYDESTOVERRIDE:DDBLTFX结构的dckDestColorkey成员是目标表面的颜色键。
◎ DDBLT_KEYSRC:颜色键与源表面相关联。
◎ DDBLT_KEYSRCOVERRIDE:DDBLTEX结构的dckSrcColorkey成员是源表面的颜色键。
◎ DDBLT_ROP:DDBLTFX结构的dwROP成员是位块传输的ROP(光栅操作代码),这些ROP与Win32 API中定义的那些相同。
◎ DDBLT_ROTATIONANGLE:DDBLTFX结构的dwRotationAngle成员是表面的旋转角度,其单位为1/100度。
◎ DDBLT_WAIT:在位块传输器忙的情况下,推迟DDERR_WASSTILLDRAWING返回值(位块传输函数调用失败返回的值之一),而当位块传输开始或发生另一个错误时立即返回。

我几乎总是使用DDBLT_WAIT标志。颜色键标志也是很重要的,我们过一会儿再说它。现在,还有最后一个Blt()参数需要说一下:
※ LPDDBLTFX lpDDBltFX:这是一个指向DDBLTFX结构的指针,它可以包含各种特殊要求的信息。如果没有什么特殊要求,你就设置为NULL好了。让我们仔细看看这个结构。我警告你,它是很魁梧的:^_^

typedef struct _DDBLTFX{
DWORD dwSize;
DWORD dwDDFX;
DWORD dwROP;
DWORD dwDDROP;
DWORD dwRotationAngle;
DWORD dwZBufferOpCode;
DWORD dwZBufferLow;
DWORD dwZBufferHigh;
DWORD dwZBufferBaseDest;
DWORD dwZDestConstBitDepth;

union {
DWORD dwZDestConst;
LPDIRECTDRAWSURFACE lpDDSZBufferDest;
};

DWORD dwZSrcConstBitDepth;

union {
DWORD dwZSrcConst;
LPDIRECTDRAWSURFACE lpDDSZBufferSrc;
};

DWORD dwAlphaEdgeBlendBitDepth;
DWORD dwAlphaEdgeBlend;
DWORD dwReserved;
DWORD dwAlphaDestConstBitDepth;

union {
DWORD dwAlphaDestConst;
LPDIRECTDRAWSURFACE lpDDSAlphaDest;
};

DWORD dwAlphaSrcConstBitDepth;

union {
DWORD dwAlphaSrcConst;
LPDIRECTDRAWSURFACE lpDDSAlphaSrc;
};

union {
DWORD dwFillColor;
DWORD dwFillDepth;
DWORD dwFillPixel;
LPDIRECTDRAWSURFACE lpDDSPattern;
};

DDCOLORKEY ddckDestColorkey;
DDCOLORKEY ddckSrcColorkey;
} DDBLTFX, FAR* LPDDBLTFX;
 楼主| 发表于 2006-4-19 16:10:22 | 显示全部楼层
如果我整个详细的介绍这个结构,恐怕我们都会受不了的,并且也没有这个必要。所以我只告诉你一些重点的部分。谢天谢地,该结构的大部分都是为z缓冲区(z-buffers)和α消息服务的,我们不用理会它。嘻嘻,我的工作量变得很小了:^_^
※ DWORD dwSize:象所有的DirectX的结构一样,当你初始化这个结构时,该成员放置结构的大小。
※ DWORD dwDDFX:这些是位块传送所能接受的一些特殊操作。列表并不长,别担心喔!

◎ DDBLTFX_ARITHSTRETCHY:位块传输时,在Y轴算术拉伸位图。
◎ DDBLTFX_MIRRORLEFTRIGHT:y轴上的镜像变换。表面从左到右完成镜像效果。
◎ DDBLTFX_MIRRORUPDOWN:x轴上的镜像变换。表面从上到下完成镜像效果。
◎ DDBLTFX_NOTEARING:把动画图像块转移到前段缓存时可以使用这个参数,这样位块传输操作的时间会与屏幕刷新率相一致,并使画面撕裂的可能性减小到最小。
◎ DDBLTFX_ROTATE180:位块传输时,把表面顺时针旋转180度。
◎ DDBLTFX_ROTATE270:位块传输时,把表面顺时针旋转270度。
◎ DDBLTFX_ROTATE90:位块传输时,把表面顺时针旋转90度。

需要详细解释的可能只有DDBLTFX_NOTEARING。游戏离不开动画,动画制作者主要关心的通常是动画的速度和性能,速度太快会导致图象质量的恶化。光栅扫描显示系统(我们基本上用的都是这种显示器)利用电子束扫描每一条水平线上的屏幕象素点。象素行从屏幕的左上角开始更新,到屏幕的右下角结束。各象素行都被称为扫描线。电子束在每一行扫描线的末端被关掉,而电子枪重新瞄准下一行的起始点,这个过程成为水平回扫。当过程执行到屏幕扫描线的最后一行时,电子束再次被关掉,电子枪重新瞄准屏幕的左上角。电子枪从屏幕的右下角重新瞄准到左上角的过程所需的时间被称为垂直回归或者屏幕空白周期。如果在视频控制器显示视频数据的同时,视频数据被CPU做了更改,这时就会产生问题。在PC机中,屏幕的刷新率通常在60Hz到100Hz之间,而现在的CPU则可以在每秒处理成百上千的指令,这样就很可能导致位于视频内存区的图象在视频系统完成显示之前发生更改。图象断裂的结果被称为图象撕裂。就我的经验而言,使用了DDBLTFX结构的DDBLTFX_NOTEARING后,图象撕裂就不是什么问题了。^_^
※ DWORD dwROP:使用这个标志来指定Win32模式的光栅操作代码。同GDI函数BitBlt()和StretchBlt()中的相对应参数的功能一样。可以通过IDirectDraw7::GetCaps()函数得到可能的光栅操作列表,可以通过“|”组合标志常量,确定源矩形表面和目标矩形表面是怎样结合的。
※ DWORD dwRotationAngle:这是用来旋转位图角度的,可以旋转任意角度。这是非常棒的,但不幸的是,它只能在HAL层(硬件抽象层)上工作,这就意味着用户的显示卡要支持加速旋转,否则……,但不能保证每个用户都有这种高档的显示卡,所以你需要考虑周全。如果你真的需要旋转处理,你只好自己写这样的函数了,这可是一个大话题,需要另写一部指南了,所以我们就越过它。但请注意,如果是90度倍数的角度,你可以使用DDBLTFX_ROTATE90等。这将使你回避显示卡不干活的风险。^_^
※ DWORD dwFillColor:如果你要使用位块传输来填充颜色,你必须把颜色放入到这个参数中。
※ DDCOLORKEY ddckDestColorKey,ddckSrcColorKey:你要使用颜色键时必须要指定这些成员。这两个家伙是很重要的。但暂时我们还不讨论它们,因为我们过一会儿才讲颜色键。

以上这些就是DDBLTFX结构中比较有用的成员了,这就意味着你现在拥有足够的知识进行位块传输了!如果你现在感觉有些混乱,不要紧,你实践过一段时间就会好了。让我们看看几个例子。假设你已经有了一个叫作lpddsBack的后缓冲区,你想把它里面的内容传输到主表面,很简单的,你看:

lpddsPrimary->Blt(NULL, lpddsBack, NULL, DDBLT_WAIT, NULL);

再轻轻回忆一下,第一个参数和第三个参数分别是位块传输的目标矩形和源矩形。由于我把它们都设置为NULL,就说明是对全部的表面进行拷贝。现在,让我们在看看,假设你有一个在离屏表面里的名字叫作lpddsTileset的16×16大小的图形,你想把它传输到后缓冲区,变成32×32大小的,你需要这样做:

RECT dest, src;
SetRect(&src, 0, 0, 16, 16); // the coordinates of the tile
SetRect(&dest, 0, 0, 32, 32); // where you want it to end up on the back buffer
lpddsBack->Blt(&dest, lpddsTileset, &src, DDBLT_WAIT, NULL);

这个例子同上一个例子不同处在与这个例子设置了位块传输的坐标。由于这两个矩形区的大小不同,Blt()依照比例适当的变更了图形的大小。最后,我们还得举例说明一下DDBLTFX结构,就做一个颜色填充的例子吧。假设你在16-bit色彩模式下,是565象素格式,你要把你的后缓冲区填充为蓝色。下面就是你应该做的:

DDBLTFX fx;
INIT_DXSTRUCT(fx); // zero out the structure and set dwSize
fx.dwFillColor = RGB_16BIT565(0, 0, 31); // set fill color to blue
lpddsBack->Blt(NULL, NULL, NULL, DDBLT_WAIT | DDBLT_COLORFILL, &fx);

注意参数的设置,前三个都是NULL,你自己想想原因吧!^_^ 好了,让我们看看另一个位块传输函数BltFast()吧。它只是Blt()的简化版本,所以我们不需要太多的时间解释它。下面是它的原形:

HRESULT BltFast(
DWORD dwX,
DWORD dwY,
LPDIRECTDRAWSURFACE7 lpDDSrcSurface,
LPRECT lpSrcRect,
DWORD dwTrans
);

你可以看得出来,它同Blt()极其相似。它也是IDirectDrawSurface7接口的成员函数,被目标表面调用。来看看它的参数:
※ DWORD dwX,dwY:这是Blt()和BltFast()之间不同的地方。是目标表面上进行位块传输的x和y坐标。如果源矩形大于目标矩形,则调用失败,因为BltFaxt()不能干按比例变换的事儿及其它能够通过Blt()完成的工作。
※ LPDIRECTDRAWSURFACE7 lpDDSrcSurface:这是源表面,同Blt()的一样。
※ LPRECT lpSrcRect:这个也同Blt()的一样。是在源表面中定义了矩形左上角和右下角的RECT结构。
※ DWORD dwTrans:定义了位块传输类型,它的标志列表很简单,只有四个标志:

◎ DDBLTFAST_DESTCOLORKEY:使用目标颜色键的透明位块传输。
◎ DDBLTFAST_NOCOLORKEY:没有透明的普通复制位块传输。
◎ DDBLTFAST_SRCCOLORKEY:使用源颜色键的透明位块传输。
◎ DDBLTFAST_WAIT:如果位块传输忙的话,不产生DDERR_WASSTILLDRAWING消息。一旦位块传输能够开始或者发生另一个错误时返回。

就这些!BltFast()支持颜色键。下面让我们看一个简单的示例。把整个后缓冲区拷贝到主表面:

lpddsPrimary->BltFast(0, 0, lpddsBack, NULL, DDBLTFAST_WAIT);

到现在为止,你已经是一个位块传输的专家了,还有几件事情对于DirectX程序很重要:颜色键和剪裁板。你可能知道在某些情况下关于颜色键有上百万种标志,那么到底怎样使用它呢?让我们一起看看吧!

☆ 颜色键

颜色键使一个位图被拷贝到另一个位图上时,不使所有的象素都显现。例如:当你把一个精灵(游戏中会动的对象一般都称作精灵)拷贝到地图上(背景上)时,这个精灵位图一般不会是一个精灵形状的位图,它通常都是一个矩形位图,位图里包含你所需要的精灵(除非你的精灵就是一个矩形机器人^_^),不使用颜色键拷贝的结果如图一:


【图一】
这并不是我们想在游戏中得到的效果。游戏中,这个精灵是不会有那个黑色的底框的。地图是先于精灵显示的,那么精灵走到树后时,还应有相应被遮挡的部分,这个先不讨论,下一节再说。现在,对我们更重要的是,如果不应用颜色键,这个精灵将永远带着这个黑色底框,这是绝对不能容忍的。
为了解决这个问题,我们使用源颜色键。这个源颜色键告诉你精灵矩形的哪些颜色将不被拷贝(当然我们是让黑色不被拷了^_^)。一个颜色键由两个值组成:一个低位颜色值,一个高位颜色值。当一个颜色键被申请使用时,在两个值之间的颜色,包括这两个值的颜色都将不会被拷贝。在DirectX中有一个结构用来处理它,叫作DDCOLORKEY,看看吧:

typedef struct _DDCOLORKEY{
DWORD dwColorSpaceLowValue;
DWORD dwColorSpaceHighValue;
} DDCOLORKEY, FAR* LPDDCOLORKEY;

很简单的结构,我就不解释了。我将展示给你使用了颜色键之后的效果。我使用颜色键的高位和低位两个值仅仅把黑色包括在它们之间。因此,黑色是唯一不会被拷贝的颜色。图二就是使用颜色键的结果:

【图二】
好多了,是不是?这就是我们想得到的结果!现在,在我告诉你怎样建立和使用颜色键之前,我还有说一说目标颜色键,尽管我们的确我们不常用到它(我们常用的是源颜色键)。鉴于源颜色键定义了哪些颜色键不能被拷贝,目标颜色键定义了哪些颜色不能被写入(覆盖)。听起来很怪异,是不是?我也有同感。^_^ 举个实例你就明白了。当你要把A位图拷贝到B位图的下面,意思就是把A位图作为背景,例如由于某种理由,需要把一个文本框拷贝到空的后缓冲区,然后再把背景画面拷贝到这个后缓冲区,但你又不能覆盖先前的文本框。因此,在后缓冲区里除了文本框的那些黑色的部分才能被写入象素。看看图三,真相大白:

【图三】
我也不清楚你什么时候需要处理这种情况,但是你的确可能用到(一旦你用到了,可千万要告诉我哦,我一直没有遇到这种情况呢^_^)。现在,你已经知道什么是颜色键了,让我们看看怎样使用它们吧!

☆ 设置颜色键

在DirectDraw中有两种方法使用颜色键。第一种,你可以链接一个颜色键(或者两个,如果你同时使用源和目标颜色键)到表面,然后在位块传输时定义DDBLT_KEYSRC,DDBLT_KEYDEST,DDBLTFAST_SRCCOLORKEY或DDBLTFAST_DESTCOLORKEY标志,具体使用哪个标志,取决于你使用哪个位块传输函数和使用哪种颜色键。第二种,你可以创建一个颜色键,然后通过DDBLTFX结构传送给位块传输操作。当你不断地需要使用颜色键时,我向你推荐第一种方法;反之,当你偶然要使用一次颜色键,就用第二种方法吧!
你可以把颜色键链接到已经建立好了的表面,也可以在建立表面的同时建立颜色键。两种方法我都将详细告诉你。假设你工作在16-bit显示模式下,是565象素格式,你要在后缓冲区使用一个仅包含黑色的源颜色键。如果你的后缓冲区已经建立好了,你就可以简单建立一个DDCOLORKEY结构,然后把它传递给IDirectDrawSurface7::SetColorKey()函数,如下所示:

HRESULT SetColorKey(
DWORD dwFlags,
LPDDCOLORKEY lpDDColorKey
);

记住要用FAILED()宏检测这个函数的返回值,保证一切都在计划之中。函数的参数很简单:
DWORD dwFlags:决定所使用颜色键类型的标志。以下三个是你将用到的:

◎ DDCKEY_COLORSPACE:该结构包含一个颜色范围,如果结构包含的是单独的颜色键则不作设置。
◎ DDCKEY_DESTBLT:该结构指定颜色键或者颜色范围作为用于位块传输操作的目标颜色键。
◎ DDCKEY_SRCBLT:该结构指定颜色键或者颜色范围作为用于覆盖操作的颜色键。

※ LPDDCOLORKEY lpDDColorKey:这是指向DDCOLORKEY结构的指针。

就这么多。你可以根据你所需要使用的颜色键适当地定义位块传输的标志。注意,一个颜色键链接到表面,并不意味着你每一次必须使用它。如果你只定义了DDBLT-WAIT或DDBLTFAST_WAIT标志,颜色键将被忽略。下面是设置颜色键的方法:

DDCOLORKEY ckey;
ckey.dwColorSpaceLowValue = RGB_16BIT565(0, 0, 0); // or we could just say '0'
ckey.dwColorSpaceHighValue = RGB_16BIT565(0, 0, 0);
if (FAILED(lpddsBack->SetColorKey(DDCKEY_SRCBLT, &ckey)))
{
// error-handling code here
}

如果你要为已经建立的颜色键链接一个表面,有几件事情你需要做。首先,当你定义DDSURFACEDESC2结构的有效成员时,你需要使dwFlags成员包含DDSD_CKSRCBLT或者DDSD_CKDESTBLT标志,具体使用哪个标志,取决于你要使用哪种颜色键。回头再看看DDSURFACEDESC2结构,它包含两种DDCOLORKEY结构。一种称为ddcdCKSrcBlt,另一种称为ddcdCKDestBlt,填写适当的结构来创建表面。你就需要干这么多!以下是关于640×480大小的离屏表面的实例代码:

// set up surface description structure
INIT_DXSTRUCT(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT | DDSD_CKSRCBLT;
ddsd.dwWidth = 640; // width of the surface
ddsd.dwHeight = 480; // and its height
ddsd.ddckCKSrcBlt.dwColorSpaceLowValue = RGB_16BIT(0,0,0); // color key low value
ddsd.ddckCKSrcBlt.dwColorSpaceHighValue = RGB_16BIT(0,0,0); // color key high value
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; // type of surface

// now create the surface
if (FAILED(lpdd7->CreateSurface(&ddsd, &lpddsBack, NULL)))
{
// error-handling code here
}

关于颜色键的部分到此结束。现在我们可以进行本章最后一项了——剪切。

☆ IDirectDrawClipper接口

假设你有一个图形,而你却只想把它的一部分显示在屏幕上。你应该怎样做呢?如果你曾经在DOS下编写过游戏,你可能对剪切望而生畏。Well,在DirectX下,这只是小菜一碟!首先的首先,这的确是很容易做到的,因为DirectX用矩形来做位块传输,改变矩形的坐标要比指定内存中的哪一部分图形被拷贝(就像在DOS下所做的)要容易的多。其次,DirectDraw还为此提供了一个接口——IDirectDrawClipper。
DirectDraw的剪切性能完全可以满足你的要求,你不但可以剪切矩形区域,你还可以剪切任意多边形区域!真的是很棒!如果你在屏幕上同时要显示一个主窗口,在屏幕的一边显示一个状态栏,在屏幕的底部显示一个文字提示栏,并且用黑色分隔开这些区域,你可以用DirectDraw的剪切功能完成它,非常容易的!
做这样的操作你需要分几步走。首先你得得到一个指向IDirectDrawClipper接口的指针。没什么难的,只需要调用IDirectDraw7::CreateClipper(),如下:

HRESULT CreateClipper(
DWORD dwFlags,
LPDIRECTDRAWCLIPPER FAR *lplpDDClipper,
IUnknown FAR *pUnkOuter
);

在你调用这个函数前,你应该首先声明一个LPDIRECTDRAWCLIPPER类型的指针,这样你才能把它的地址传递给这个函数。记着要检测函数的调用是否成功哦!以下是函数参数的解释:
DWORD dwFlags:简直就是幸福——这个参数还没有使用过,设置为0。^_^
LPDIRECTDRAWCLIPPER FAR *lplpDDClipper:把你的LPDIRECTDRAWCLIPPER指针的地址传递给它。
IUnknown FAR *pUnkOuter:你知道怎么做,设置为NULL。^_^

一旦你有了自己的接口指针,下一件事就是创建剪切列表(clip list)。多个剪切的矩形组成了剪切列表。需要使用到RGNDATA结构,它包含了足够的信息来定义一个任意的区域,看看它的原形吧:

typedef struct _RGNDATA {
RGNDATAHEADER rdh;
char Buffer[1];
} RGNDATA;

我需要详细解说一下它的参数。
RGNDATAHEADER rdh:它是RGNDATA结构中嵌套的一个结构。它包含了第二个参数——Buffer的所有信息。它定义了需要剪切区域里的矩形的数目,整个区域的形状等信息。我们过一会儿再具体讨论它。
char Buffer[1]:这并不意味着是只有一个值得数组;它将是在内存中任意大小的区域来控制着实际的剪切区域数据。同样的,对于RGNDATA结构,我们要声明一个指向该结构的指针,然后使用malloc()函数为RGNDATAHEADER设置足够的内存空间,也就是为剪切列表设置足够的空间。有一件事我要提醒你:剪切列表里的矩形按从上到下,然后从左到右排列,不能交迭。

我已经意识到你有些胡涂了,不要紧,继续学习,一切会好起来的。下面是RGNDATAHEADER结构的原形,它比较好理解:

typedef struct _RGNDATAHEADER {
DWORD dwSize;
DWORD iType;
DWORD nCount;
DWORD nRgnSize;
RECT rcBound;
} RGNDATAHEADER;

DWORD dwSize:结构的大小。简单的使用sizeof(RGNDATAHEADER)好了。
DWORD iType:它描述了每个区域的外形。它是另有玄机的,以后我们再把它扩展开来讨论。现在,你只要把它设置为RDH_RECTANGLES就好了,这也正是我们需要的。
DWORD nCount:这是组成该区域的矩形的数量。换句话说,就是你的剪切列表理的矩形数。
DWORD nRgnSize:为缓冲区的大小设置它,将得到自身的区域数据。由于我们使用n个矩形组成了剪切区域,所以它的大小应该是sizeof(RECT) *nCount。
DWORD rcBound:这是一个矩形类型,包含了剪切列表里的所有矩形。通常你把它设置成表面上需要剪切部分的尺寸。
 楼主| 发表于 2006-4-19 16:11:06 | 显示全部楼层
现在,我们可以建立一个剪切列表了。首先我们声明一个LPRGNDATA的指针,分配给剪切列表足够的内存空间;然后根据上面我们所学的设置每个成员。让我们看看最简单的实例,你可能经常要用到它哦!它只有一个剪切区域,并且,就是整个屏幕,是640×480显示模式的。以下就是代码:

// first set up the pointer -- we allocate enough memory for the RGNDATAHEADER
// along with one RECT. If we were using multiple clipping area, we would have
// to allocate more memory.
LPRGNDATA lpClipList = (LPRGNDATA)malloc(sizeof(RGNDATAHEADER) + sizeof(RECT));

// this is the RECT we want to clip to: the whole display area
RECT rcClipRect = {0, 0, 640, 480};

// now fill out all the structure fields
memcpy(lpClipList->Buffer, &rcClipRect, sizeof(RECT)); // copy the actual clip region
lpClipList->rdh.dwSize = sizeof(RGNDATAHEADER); // size of header structure
lpClipList->rdh.iType = RDH_RECTANGLES; // type of clip region
lpClipList->rdh.nCount = 1; // number of clip regions
lpClipList->rdh.nRgnSize = sizeof(RECT); // size of lpClipList->Buffer
lpClipList->rdh.rcBound = rcClipRect; // the bounding RECT

一旦有了剪切列表,你需要把它作为你的剪裁板。你将调用IDirectDrawClipper接口的函数SetClipList()。就是下面这个东东:

HRESULT SetClipList(
LPRGNDATA lpClipList,
DWORD dwFlags
);

你所要做的就是把RGNDATA的指针传递给它。参数dwFlags没有用,设置为0。现在,剪切列表设置好了,还需要一步,就是把剪切列表链接到你所要控制的表面上,这需要调用SetClipper()函数,它将表面指针作为其唯一的参数:

HRESULT SetClipper(LPDIRECTDRAWCLIPPER lpDDClipper);

你知道应该怎样做:就是把你已经设置好的接口的指针传递给它。任何时候,你要位块传输一个有剪裁板相关联的表面,剪裁板将做所有的工作。所以如果你要在屏幕上显示一个贴片的一部分,例如传输一个矩形坐标为{-10,-10,6,6},或者类似的矩形贴图,都不会有麻烦的。很不错吧,嗯?
关于剪切的最后一件事情是必须要用free()函数释放你用malloc()设置的内存空间。还有,就是由于某种原因在调用SetClipList()或SetClipper()失败后,在返回错误代码前或你要根据失败的结果进行操作前,要释放内存空间。在你完成用LPRGNDATA的指针设置剪切列表后,这个指针就没有存在的意义了,所以它占用的内存空间将被立即释放。

☆ 总结

到此,关于DirectDraw的部分就讨论完了!你真的从这六章里学到了很多的知识,如果你坚持到现在,那么祝贺你,你真的已经走了很长一段路了。^_^ 对于这一章我们所学习的,我将把它们组合在一起,给你一个Demo程序,它将从资源调用位图,使用位块传输位图,颜色填充,放缩位图比例,使用剪切功能。这个就是源代码下载的地址:http://www.aeon-software.com/downloads/clipscale.zip
仍然有些东东我们应该讨论,例如页面的切换(flipping),双缓冲区的应用等,无论如何,我们还将继续,所以不必担心我们会遗漏重要的内容。^_^
现在,最初的原始积累已经结束,我们以后的焦点会从创建Windows程序转移到创建一个贴图基础的RPG游戏。以后的章节将包括用DirectInput建立一个好的输入机制,写一个基础的引擎脚本,播放背景音乐和配音,等等。下一章,我们将学习为贴图游戏制作一个简单的卷轴游戏引擎,很容易的,没有你想象的那么难哦!

★ 第七章 游戏的结构

☆ 简介

上一章的结尾,我曾经承诺将在本章教你写一个小的游戏引擎。但在结束上一章,然后我在考虑怎样更好的完成这个系列教程时,我感到这并不是一个好主意。(别,别,先把臭鸡蛋放下)让我告诉你我的真实想法。
有几次,我曾经间接或直接的提到你不能象在DOS模式下一样编写你的Windows程序,你必须组织好每一件事。因为全部的主循环在每一帧至少要被执行一次,所以你千万不能漏掉对Windows发给你的每一个请求的跟踪。
现在,你可能对我以前给你的Demo代码了解了一些,但你可能还没有把它扩充为一个大个儿的游戏程序,或者你还没有开始根据本教程系列编写自己的游戏,太好了!?现在,在我教你具体怎样编程前,我得出了一个结论,就是——现在,我们最后拿出一点儿时间来学习一下游戏的结构,看看一个游戏最终是怎样由各个部分组成的。
本章不同于以往,你将很少看到程序代码,所以,哈哈,如果你对前几章关于DirectDraw的部分还不是很明白,或者还有一些其它的事不是很清楚,不要紧,因为我们要先暂停一下前面的部分,开始一个新的话题。你只需要了解一些Windows编程的知识,所以看过或了解第一、二章的内容就可以了。
还有一件事情我要说一下:对于本章以及后面的一些章节,我都将以Terran(地球人)这个小游戏为蓝本讲解,所以你最好下载这个Demo(http://www.aeon-software.com/downloads.html)来更好的理解我所要讲的。目前的版本一,可能不能满足所有的显示卡,版本二出来后可能会好些(已经完成)。废话不说了,让我们开始吧!

☆ 概览

总之,在你开始编程前,你应该对你的游戏逻辑和具体操作方式有一个详细的方案。这将保证你在实施代码编辑时,不会出大的过错。否则,随着代码的编写,你本来清晰的思路会被意料外的问题和更新的创意搞得乱七八糟,最后使你自己都迷失在自己的代码中了。相信我,我是有惨痛教训的。^_^
你应该从WinMain()函数着手,它是程序开始的地方。这一点你可能认为理所应当,但是很多人从编写退出程序,或者从编写冲突检测函数,或者一些其它的细节处入手,而不是从WinMain()处开始,这是一种倒序的编程方法。对于初学者,理想的顺序还是从头到尾的正序方式。你首先应该从主函数开始,然后在主函数里调用每一个子函数,以及处理一些其它的事情。这样做,至少有两个理由:
第一, 每一件事情都可以立竿见影。如果你从细节函数开始编写,你很难检测和实时演示这些函数的功能,因为你的主环境还没有构造好(编写好)。另一方面,如果你按照从头到尾的正序编写程序,你可以随时随地检测你编写的结果。先有主干,再一步一步的添枝加叶,完善程序,你的游戏就会从开始走向完成。
第二, 有步骤的完善你的游戏。如果你先从细节函数着手,很有可能在稍后你有了新的想法或又想添加一点儿功能,于是,你又写了另一个细节函数,最终,乱糟糟的一堆函数,你都分不清应该先调用哪个,后调用哪个。所以,最好的办法是,从简单到复杂一步一步的添加,条理清晰。我想你已经理解我的意思了,就不多说了。
我在Terran中所做的是先把主程序分成五个部分,然后每一个部分按照从头到尾的方式设计。其中四个是实际的游戏内容:卷轴引擎,脚本引擎,战斗引擎和菜单系统。第五部分就是利用Windows和DirectX使它们按照我的想法运转起来,这包括初始化、退出和从全屏模式变为Windows模式。游戏的四个部分的每一个部分都由多种函数完成,我将稍后详细介绍它们,所以你可以清楚的看到每一部分是怎样有机的组合的。希望真正的给你启迪,并且我将尽量说得清楚详细,使你更好的理解。

☆ 实例:Terran

首先,给你看一下Terran的设计图,好明确一下我将要将的。
如【图一】

这个图表没有详细描绘出游戏中的所有函数,但是它介绍了一些基本的事情,让我们仔细看看:
表中从Initialization(初始化)开始,也就是WinMain()中首先要调用的部分。当然,现在表中的描述很简单,实际上有好多函数会在初始化阶段被调用。我没有详细的列出所有的函数,原因是那样做后,你将根本无法看明白。^_^ 你当然已经很熟悉这些DirectX和Windows的函数——它们建立窗口,还要用到DirectX的接口。还有很多接口我们还没有学习到,但是这对于本章并不重要,我们将随用随学。这个Load Game Data(读取游戏数据)部分将从外部文件读取所有的有用信息,包括:游戏中可利用的分类数据,脚本引擎要操作的数据,图形系统数据,等等。

 楼主| 发表于 2006-4-19 16:11:38 | 显示全部楼层
初始化部分的最后一件事情,调用一个游戏真正开始的脚本。Terran中,几乎每一部分都是由脚本引擎操控的。我将稍后详细介绍它。我将会在本系列教程安排一章来专心介绍怎样为你的游戏创建一个基础的,十分有用的脚本引擎。
初始化完成后,程序进入主循环(Main Loop)。主循环将在你玩儿完游戏或你退出游戏后停止。在表中,你可以看到每一次循环都经历4个基本步骤。
第一步,启动输入设备驱动程序。在Terran中,有两种输入设备(也就是操纵游戏的工具),键盘和游戏棒。它们都具有同样的两种功能,一个是检测当前的移动状态,一个是检测是否有即时的按键按下。第一种是持续的按键,例如人物在地图上的移动,第二种检测是否有突发的事件,例如进入了菜单选项。
第二步,主循环装载音乐平台。基本上,载入音乐平台所做的就是在游戏进程没有结束前,播放音乐,然后根据不同的场景,选择相应的音乐。不用担心具体的细节——我们将会对音乐部分有一个专题讲解。^_^
第三步是最重要的一步:主循环在此被分成了五个部分,地图部分、菜单系统、战斗系统、脚本和结束部分。字面上你可以大概的了解每一部分的功能。在图标中你可以看出,只有结束部分(Shutdown)是单独存在的,没有其它的子系统,一旦它被调用,ok,一切都将结束。其它的四个部分还需要费一点儿口舌。
这四个部分都有自己的子部分,在图上你一眼就能看出来。每一部分【例如:WorldMapMain()】都先完成该部分的通用功能,然后就进入分支子部分,完成具体的功能。有些时候,这些子功能通过if或switch就可以实现,但有些时候必须把子功能做成相应的函数来调用。
地图部分(World Map):地图部分很简单。首先,它被相应的脚本调用;然后,基于子功能,或者相应当前的用户输入,或者忽略;最后,就是屏幕上的人物更新,地图被显示,再根据用户的输入,地图不断的得到更新。就这些!
菜单系统(Menu System):这部分有一点儿复杂,因为它要掌控每一件事情,包括加载游戏、创建角色,设置硬件等等。主函数根据菜单的不同功能,把菜单设置成不同的颜色,然后子菜单根据需要再有自己的子菜单。菜单通常都通过指针数组来实现。游戏中的子系统通常都是一个整数标识,从而方便数组的索引。就像下面这样:

(*lpfnMenuSubfunctions[substate.current])();

如果你对上面这一行感到困惑,那么你有必要复习一下C语言的指针函数部分,此时,结合实际的情况,你可能对指针的理解更深刻一些。每一个子菜单都操控着一个细节。当玩家选择不同的菜单条,就要有程序实现其相对应的功能,并保证在当前的帧得到显示。当然,如果有必要显示的话。做到这些,主菜单函数需要运行当前的脚本,申请(调用)相应的动作,从而显示相应的画面。如显示当前的金块儿数量,或者是游戏角色的状态等等。
战斗系统(Battle System):战斗系统和菜单系统紧密的联合在一起。战斗系统是一个半自动系统(semi-active),就像《最终幻想》系列(如果你还不知道《最终幻想》这个游戏,我、我、我倒),任何时候都是弹出菜单系统,来半控制角色进行打斗(因为具体的打斗动作,是由脚本控制的),而战斗系统也同时控制菜单系统,因此,战斗系统和菜单系统相辅相成。战斗的逻辑如下:
首先,判断所有的游戏角色是否通敌人的距离足够近。如果是敌人主动靠近游戏角色,则敌人的AI(人工智能)接管,并为敌人做出一个选择;如果是游戏角色主动靠近敌人,并且当前的菜单系统不是战斗模式,那么,战斗系统菜单启动,然后由玩家进一步操控游戏角色。如果此时菜单系统被另一个游戏角色操控,那么把当前要调用的菜单放入队列,等待调用。一旦前一个菜单调用完成,计算机(也就是你的程序)要检查该队列,如果不为空,就启动相应的菜单。
以上的任何一种情况,即敌人走近游戏角色或游戏角色走近敌人,一旦成立,则进入下一个环节——调用动作队列。每一次战斗逻辑(可能来自战斗系统调用,也可能来自菜单系统的调用)需要判断两件事:如果有战斗动作正在进行中,则由脚本控制完成;如果没有战斗动作发生,则检测战斗动作队列,如果队列中有下一个动作,就交给脚本控制完成。
与普通敌人的战斗(Regular Fight)和与Boss(关头人物)的战斗(Boss Fight)不同之处在于,与Boss战斗时,游戏角色不能逃跑——你必须血战到底(很残酷哦!),用一个简单的if结构就完成了这个设定。第三个子菜单是胜利(Victory),它有一点儿不同,它报告战斗的成果,不用有什么逻辑判断,通常都是由脚本控制这一步。
脚本(Scripts Only):这是游戏主要部分的最后一个部分了。它很简单:就是运行被调用的脚本,仅此而已!它通常都不被玩家所觉察,除了在游戏初始化的时候,有的游戏打出一些初始化序列的字幕。不过,无论是在游戏过程中正常出现无显示画面的时候,或者是计算机处理后台作业的时候,它始终是工作的。(无名英雄)
游戏结束(Shutdown):顾名思义,就是结束游戏:它使程序退出主循环,释放所有在游戏中创建的DirectX的对象,释放所有的游戏占用的内存,然后结束游戏。
主循环的最后一步是显示当前的帧,就是简单的把后缓冲区的内容拷贝到屏幕。如果是在Windows模式下,还得使用双缓冲区;如果游戏是在全屏模式下,还得需要页面切换。我想我们还没有具体的讲过页面切换,不过这一时刻不会太远了。^_^

☆ 游戏部分的秘诀

待续。。。。。。。
作者的邮箱:ironblayde@aeon-software.com 要用英文给作者发信哦!
作者的ICQ:53210499
【傻马乱踢:howsee@163.com】
【水平有限,诚心一片◆傻马乱踢】
 楼主| 发表于 2006-4-19 15:55:13 | 显示全部楼层

创建窗口

好消息,创建窗口你所要做的只是调用一个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
);

首先的首先:函数的返回值。也就是函数的类型。是不是所有创建窗口用的函数的类型的讨厌样子都感觉亲切了一点儿?还没有?不要紧,你会习惯的,肯定比你想象的速度要快。这里返回的类型是HWND,是一个窗口的句柄(句柄就是窗口的标识符)。你将把CreateWindowEx()的返回值传递给一个窗口的句柄,就像一个参数一样。现在,我们来琢磨一下这些参数,很多根据名字就知道它是干什么的了。
DWORD dwExStyle:扩充的窗口风格。你将很少使用扩充的窗口风格,所以多数时间你会把它设置为NULL。如果有兴趣,查一下帮助文件,可以一试由WS_EX_打头的扩充风格。
LPCTSTR lpClassName:还记得你的窗口类的名称吗?再用一次。
LPCTSTR lpWindowName:将显示在窗口的标题栏里的简短文字。
DWORD dwStyle:窗口的风格。它将允许你详细的描绘你所要创建的窗口的风格。有很多风格你可以利用哦,都是以WS_打头的,你可以利用(|)符号组合利用它们。我将在这儿介绍几个常用的。
◎ WS_POPUP 指定一个弹出的窗口。
◎ WS_OVERLAPPED 指定一个具有标题栏和边界的重叠窗口。
◎ WS_OVERLAPPEDWINDOW 指定一个具有所有标准控件的窗口。
◎ WS_VISIBLE 指定一个初始时可见的窗口。
看得出,WS_OVERLAPPEDWINDOW是一个组合体。简单的说,你可以按照如下规律:如果你要创建一个可以最大化、最小化、随意改变大小等等地窗口,就选择WS_OVERLAPPEDWINDOW;如果你只想要一个具有标题栏、可改变大小的窗口,就选择WS_OVERLAPPED;如果你只想要一个光秃秃的窗口,就选择WS_POPUP;如果你只想显示一个黑色的大方框,可能你要用它写一个全屏的游戏,选择WS_VISIBLE是没错的。
int x,y:你所要创建的窗口的左上角的坐标。
int nWidth,nHeight:猜也猜到了,窗口的长和高,单位是『象素』。
HWND hWndParent:指向父窗口的句柄。你若想在窗口下再建立一个窗口,那么第一个窗口就叫父窗口。咱先建立一个主窗口,所以设置为NULL,也就意味着Windows桌面是父窗口。
HMENU hMenu:这是用在窗口上的菜单句柄。若你学会建立和使用资源,即建立自己的菜单,你可以用LoadMenu()函数调用自己的菜单资源。目前,咱先设为NULL。
HINSTANCE hInstance:是一个名柄,它指向由Windows传递给WinMain()的实例。
LPVOID lpParam:对于游戏编程来说,没有什么用的东西,只有简单的窗口程序用到它。设置为NULL好了。
同志们,我们现在万事具备,东风也有了。我先给个示例:

HWND hwnd;
if (!(hwnd = CreateWindowEx(NULL, // extended style, not needed
"Sample Class", // class identifier
"Sample Window", // window title
WS_POPUP | WS_VISIBLE,// parameters
0, 0, 320, 240, // initial position, size
NULL, // handle to parent (the desktop)
NULL, // handle to menu (none)
hinstance, // application instance handle
NULL))) // who needs it?
return(0);

你可能会在游戏编程中用上这这段代码,因为它是一个弹出式窗口。注意,我用了if形式,目的是一旦CreateWindowsEX()函数失灵,返回一个NULL,也就意味着如果窗口由于某种原因不能被建立,那么WinMain()就被简单的返回,程序结束。
现在我们学会了足够的知识建立一个小有功能的窗口了。还记得我们建立窗口类“sample class”时,一个指向“CALLBACK”类型函数的指针吗?对,是“lpfnWndProc”。要想让你的窗口真正做点事儿,我们还得来处理一下它指向的“窗口过程”函数。

☆ 显示窗口
CreateWindowEx()从内部创建窗口,但并不显示它。要显示这个窗口,必须调用另外两个函数:ShowWindow()和UpdateWindow()。头一个设置窗口的显示状态,后一个则更新窗口的客户区。对于程序的主窗口,ShowWindow()必须被调用一次,调用代码如下:

ShowWindow(hwnd,nCmdShow);

第一个参数是由CreateWindowEx()函数返回的窗口句柄;第二个参数就是窗口的显示模式参数,在☆WinMain()函数中提到过,就不重复了。UpdateWindow()函数的调用代码如下:

UpdateWindow(hwnd);

参数hwnd同ShowWindow()函数的hwnd一样。

☆ 消息的处理
我已经说过消息在窗口里的作用了,下面让我们来仔细学习一下它。处理消息的函数结构如下:

LRESULT CALLBACK MsgHandler( 【有时被命名为WndProc,随便你】
HWND hwnd, // window handle
UINT msg, // the message identifier
WPARAM wparam, // message parameters
LPARAM lparam // more message parameters
);

这个LRESULT类型要求返回一个32位的整数。实际取值依赖于消息,但是这个值很少在应用程序代码中得到应用。以前我们谈到过一点CALLBACK协定,它的参数很简单:
HWND hwnd:是接收消息的窗口的句柄,也是由CreateWindowEx()函数返回的句柄。
UINT msg:这是一个消息标识符,都是以WM_打头的符号常量,意思是“Windows Message”。很多的,这里只介绍一些常用的:
◎ WM_ACTIVATE:一个新窗口被激活。
◎ WM_CLOSE:一个窗口被关闭。
◎ WM_COMMAND:一个菜单功能被选择。
◎ WM_CREATE:一个窗口被建立。
◎ WM_LBUTTONDBLCLK:鼠标左键被双击。
◎ WM_LBUTTONDOWN:鼠标左键被按下。
◎ WM_MOUSEMOVE:鼠标被移动。
◎ WM_MOVE:一个窗口被移动。
◎ WM_PAINT:窗口的一部分需要重画。
◎ WM_RBUTTONDBLCLK:鼠标的右键被双击。
◎ WM_RBUTTONDOWN:鼠标的右键被按下。
◎ WM_SIZE:窗口的大小被改变。
◎ WM_USER:干你想干的。
WPARAM wparamLPARAM lparam:消息参数。它们提供有关消息的附加信息,这两个值对于每条消息来说都是特定的。
你要把所有要发生的消息都写进程序代码的话,我想你可能已经累疯了。我想我会的。感谢上帝,Windows提供了默认消息处理,如果你没有任何特殊的消息需要处理了,你总是要用DefWindowPorc()函数的,下面给一个最简单的例子,没有任何特定的消息要处理的例子:

LRESULT CALLBACK MsgHandler(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
return(DefWindowProc(hwnd, msg, wparam, lparam));
}

简单吧!但通常你都需要处理一些自己的消息,你要写自己的程序代码,然后返回0,来告诉程序你干完了。下面是一个例子,当窗口建立时,你调用了一个初始化的函数Initialize_Game(),然后返回0,最后告诉程序自己处理那些默认的消息吧:

LRESULT CALLBACK MsgHandler(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
if (msg == WM_CREATE)
{
Initialize_Game();
return(0);
}

return(DefWindowProc(hwnd, msg, wparam, lparam));
}

你很可能需要一个“switch”结构来手动完成你想要控制的消息,然后把剩下的交给DefWindowProc()去做。大功告成前,我不得不提醒您一件事,就是怎样使你的消息控制得到响应呢?

☆ 读取消息队列
我先给你一个switch结构的例子吧:(感性的)

LRESULT CALLBACK MsgHandler(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
switch(msg)
{
case WM_CREAT:
[初始化游戏]
return 0;
case WM_PAINT:
[画一架飞机]
return 0;
case ……………………
……………………
}

return(DefWindowProc(hwnd, msg, wparam, lparam));
}

在进入程序的主循环前,你需要看看你的消息控制(就是你在switch结构里编的那些),尤其是还没有用到的消息控制是否被机器存了起来,以备一旦用到,马上响应。做到正确的响应,你需要做几件事。首先你需要PeekMessage()函数。下面是它的原形:

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
);

这是一个布尔类型,也就是一个int型,不过只有两个值,TRUE和FALSE,如果有一条消息在队列中等待,函数返回TRUE,否则,返回FALSE。它的参数很简单:
LPMSG lpMsg:这是一个MSG类型的指针变量。如果有消息在等待,消息信息将被填入该变量。
HWND hWnd:你所要检查的消息队列的窗口的句柄。
UINT wMsgFilterMinwMsgFilterMax:索引第一个和最后一个消息,一般你都从第一个消息开始检索,所以把它们都设置为0好了。
UINT wRemoveMsg:一般来说,它有两个指,PM_REMOVE或者PM_NOREMOVE。使用前者会在消息被读取后从队列中移除,后者是继续保留。通常,我们选择前者PM_REMOVE。
真正处理消息时,你需要做两件事,很简单,第一件是TranslateMessage(),第二件是DispatchMessage()。它们的原形很相似:

BOOL TranslateMessage(CONST MSG *lpmsg);
LONG DispatchMessage(CONST MSG *lpmsg);

头一个是把消息翻译过来,第二个是从MSG结构中调用相应的信息。你只需要知道这么多。伴随着程序主循环的反复执行,如果有消息出现,你就调用这两个函数,函数MsgHandler()会安排好一切的。下面是个例子:

if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))【现在有时用GetMessage(&msg,NULL,0,0)】
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

没问题,你现在完全可以写一个窗口程序了。不坏吧?在结束本章前,我还有几点要提醒你。还记得我们在谈论☆消息时,说要在后面进一步讨论它吗?你忘了?我可没有忘记。怎样主动向Windows发送消息呢?

☆ 发送消息
有两种办法可以做到。PostMessage()函数或SendMessage()函数。它们的原形很相似:

BOOL PostMessage(
HWND hWnd, // handle of destination window
UINT Msg, // message to post
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);

LRESULT SendMessage(
HWND hWnd, // handle of destination window
UINT Msg, // message to post
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);

它们的参数相同,并且和前面讲过的函数MsgHandler()的参数功能相同,就不重复了。我只谈谈它们之间的区别。
PostMessage()被经常用来向队列中加入消息,成功,返回TRUE,否则,返回FALSE。它只是简单的把消息加入到队列中,然后返回。多数情况下,调用它将返回TRUE。
SendMessage()则有些不同,它并不是把消息加入到队列里,而是直接翻译消息和调用消息处理,直到消息处理完成后才返回。所以,SendMessage()比PostMessage()有更高的应急性。你想立刻干的事情,就应该调用它。
消息是DOS和Windows编程之间重要的区别标志。

☆ 程序的流程
在DOS中,我们不必担心消息这种东西,不必担心多个程序同时运行,但在Windows里,你必须考虑这些。在Windows平台上编程,有一些不同于DOS下编程的地方。让我们看看下面这段虚拟的代码:

// main game loop
do
{
// handle messages here

// ...

// update screen if necessary
if (new_screen)
{
FadeOut();
LoadNewMap();
FadeIn();
}

// perform game logic
WaitForInput();
UpdateCharacters();
RenderMap();

} while (game_active);

假设FadeOut()函数这样工作:当函数被调用,在一秒内屏幕图象暗淡下来,当屏幕完全黑了,函数返回。LoadNewMap()调用一个新的图象;FadeIn()使屏幕逐渐亮起来,好显示新图象。当有键子按下,调用WaitForInput()函数,再继续调用下去。这在DOS游戏编程里是合情合理的,但在Windows下不行。
为什么呢?让我们看看新画面诞生的过程。画面逐渐变黑,调用图片,逐渐恢复。这大概要2秒钟,用户可以等待,也可能要移动一下窗口,但程序只专心的干调用图片的工作,不会对窗口的移动作出反应。这是很糟糕的,你干了机器不知道的事情,这可能导致系统崩溃,我们必须要让机器对用户的任何操作作出正确的反应。不多说了,总之你要换一换脑筋,如果你从来就没在DOS下编过程序,那正好,你赶上潮流了!

☆ 总结
本章我们讲了Windows编程的基础,虽然只是一个空白的窗口,但包含了最基本的东西。下一章我们将学习创建资源和利用资源,你就可以用有自己风格的光标、图标、声音、菜单等等,还要生成一个EXE文件呢!
待续。。。。。。

 楼主| 发表于 2006-4-19 15:55:40 | 显示全部楼层

第二章 使用Win32程序资源

☆ 简介
欢迎继续收看!通过本章题目可能你已经猜出了本章论题,我将教会你在Windows程序中使用资源。简单的讲,资源即数据,它们通常是和程序的EXE文件相关联的,但是它们又是独一无二的。首先,资源在运行过程中不能被修改。它们实际上都是只读文件,而且程序代码不能够直接访问它们。另外,资源并不在程序的数据区内。在装入时,程序资源通常在某个磁盘文件中,直到程序需要它们时才被装入。使用资源是一件很容易的事情,并且它的妙处无穷。Windows为我们提供了大量的资源类型,但我们这里只学一些最常用,最容易的:图标(icon)、光标(cursor)、位图(bitmap)、菜单(menu)和字符串(string)。此后,我还将教你建立自己风格类型的资源,使你为所欲为。(好像挺诱人哦!)
重复一下,要想看懂本章,你得有点C语言的基础。C++有时用一用,但不影响你学习本章内容。并且我假定你已经读过了上一章内容“Windows编程基础”。我还是用Microsoft Visual C++的编译器,怎么样?出发吧!

☆ 资源脚本
在进行细节之前,我们要先搞懂怎样要编译器知道它所要编译的资源类型。方法是使用称之为资源脚本的特殊文件,它是一个简单的文本文件,可以手工编辑,也可以让Visual C++自动编辑,或者你用其它的自动编辑器编辑。无论如何,资源脚本文件要有一个.rc的扩展名。大多数的脚本文件都从定义资源行开始,最简单的资源行通常要用到资源类型,就像这样:

[identifier] [resource type] [filename]
【标识符】 【资源类型】 【文件名称】

标识符可以用两种方式表示:一种是能表示资源意思的字符串,另一种是在资源相对应的头文件中用#define定义过的数字常量。如果你选择数字常量,这通常是一个好主意,别忘了把相应的头文件加入到你的资源脚本。资源脚本使用C语言风格的文件格式好像比较容易理解。以下是一个比较简单的资源脚本实例:

#include "resource.h"

// icons
ICON_MAIN ICON myicon.ico

// bitmaps
IMG_TILESET1 BITMAP tileset.bmp
IMG_TILESET2 BITMAP tileset2.bmp

好理解吧!但有一件事可能把人弄胡涂。例子中的ICON_MAIN和IMG_TILESET是字符串呢,还是数字常量?但这无伤大雅,编译器编译的时候会自己判断。如果发现在头文件中有#define的定义,那就认为是字符常量,否则,就是字符串。
如果有些迷茫,不要紧。我将解释我们要用到的每一个资源类型。什么?觉得麻烦?OK,OK,让我们用全自动的资源插入系统吧!(在Visual C++中,在“插入”下拉菜单中,选择“资源”)我还是喜欢在记事本中用手工输入的模式,别问我为什么,我也不知道。:)现在你知道了建立资源脚本的基础知识,让我们开始进一步的行程吧!

☆ 图标和光标
你每天在使用的大多数的Windows程序,都有自己的图标,简单的说,就是EXE文件同这个图标资源相关联了,独特风格的光标也是如此。你已经知道图标的脚本行样子了,光标的和它很相似,看看吧:

[identifier] CURSOR [filename]
[identifier] ICON [filename]

增加了一行脚本行后,也就是意味着你的EXE文件又多了一个关联。也就是说你的EXE文件要根据标识符去相应的位置寻找相应的文件[filename]。你可以使用任何你喜欢用的图标/光标编辑器去编辑相应的文件。我通常利用Visual C++中的编辑器。
把资源脚本做出来后,并没有完事儿,因为你还不知道怎么调用相应的资源,要想知道图标和光标是怎样在你的程序中被调用的,让我们回过头来,看一看上一章中的窗口类(windows class)文件:

WNDCLASSEX sampleClass; // declare structure variable

sampleClass.cbSize = sizeof(WNDCLASSEX); // always use this!
sampleClass.style = CS_DBLCLKS | CS_OWNDC | CS_HREDRAW | CS_VREDRAW; // standard settings
sampleClass.lpfnWndProc = MsgHandler; // message handler function
sampleClass.cbClsExtra = 0; // extra class info, not used
sampleClass.cbWndExtra = 0; // extra window info, not used
sampleClass.hInstance = hinstance; // parameter passed to WinMain()
sampleClass.hIcon = LoadIcon(NULL, IDI_WINLOGO); // Windows logo
sampleClass.hCursor = LoadCursor(NULL, IDC_ARROW); // standard cursor
sampleClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); // a simple black brush
sampleClass.lpszMenuName = NULL; // no menu
sampleClass.lpszClassName = "Sample Class" // class name
sampleClass.hIconSm = LoadIcon(NULL, IDI_WINLOGO); // Windows logo again

还记得它吧?这个hIcon用来表示整个程序;hIconSm用来出现在开始菜单和窗口的标题栏里;hCursor用来表示在你所创建的窗口中的光标的样子。我向你保证,我们要实现自己的风格一点都不复杂。下面是它们的原形:

HICON LoadIcon(
HINSTANCE hInstance, // handle to application instance
LPCTSTR lpIconName // icon-name string or icon resource identifier
);

HCURSOR LoadCursor(
HINSTANCE hInstance, // handle to application instance
LPCTSTR lpCursorName // name string or cursor resource identifier
);

返回的类型是它们自己相对应的类型。其内部的参数都很直观:
HINSTANCE hInstane:但程序执行时,把图标或光标相对应的句柄传递给WinMain()函数。若要使用Windows的标注图标或光标,就把它设置为NULL。
LPCTSTR lpIconNamelpCursorName:是你要调用的资源的标识符字符串。如果你在脚本文件中用字符串直接作为标识符,就直接传送它好了;如果你是用数字常量,就要使用一个Windows头文件里的宏MAKEINTRESOURCE()来把它们协调一致。
让我们看一看下面的资源脚本,是关于图标和光标的:

#include "resource.h"

ICON_MAIN ICON myicon.ico
CURSOR_ARROW CURSOR arrow.cur

如果标识符ICON_MAIN合CURSOR_ARROW在头文件resource.h中没有被#define定义过,那么我们将直接传递它给资源调用函数,象这样:

sampleClass.hIcon = LoadIcon(hinstance, "ICON_MAIN");

如果它们在头文件resource.h中这样定义过:

#define ICON_MAIN 1000
#define CURSOR_ARROW 2000

你就必须用宏MAKEINTRESOURCE()把它们转变为LPCTSTR类型。下面给出你几种意义相同的调用方法,都是正确的喔!

sampleClass.hIcon = LoadIcon(hinstance, MAKEINTRESOURCE(ICON_MAIN));
or...
sampleClass.hIcon = LoadIcon(hinstance, MAKEINTRESOURCE(1000));
or...
int ident = 1000;
sampleClass.hIcon = LoadIcon(hinstance, MAKEINTRESOURCE(ident));

关于图标和光标的调用,你学的差不多了。就这个话题,我还想告诉你一件事儿。如果你除了在程序的开始设置光标外,在程序中还要设置光标,有一个简单的Windows函数可以完成它:

HCURSOR SetCursor(HCURSOR hCursor);

仅仅一个参数,它是一个句柄,是在调用LoadCursor()时得到的,函数返回调用的上一个光标的句柄,如果没有设置过上一个光标,返回值是NULL。有点胡涂?无关大局,以后会明白。让我们看一看更有趣的吧!

☆ 位图
想要往程序里添加图象,通过位图资源可能是最简单的办法了。位图是Windows之本,当然提供了一些函数来处理位图,请记住,如果你使用了太多的位图,你的EXE文件将要非常巨大。在资源脚本中设置位图同图标和光标没什么区别:

[identifier] BITMAP [filename]

有一个函数LoadBitmap(),同LoadCursor()和LoadIcon()的用法很相似,它将得到一个句柄,由于我还没有讲过图形(graphics),就不具体说函数的功能了,你可以猜一猜它是怎样工作的,一旦你得到了图形句柄,你将怎样使用它呢?更多的留待以后再讲。不要担心,现在只是要你有点儿准备。下面看看我们还应该学点儿什么。

☆ 字符串表格

 楼主| 发表于 2006-4-19 15:56:02 | 显示全部楼层

☆ 字符串表

字符串表是我最喜欢的资源类型。正象你所想的:一个充满字符串的庞大表格。字符串表有很多用处。你可以用它存储你的文件名称,游戏中的人物对话,消息框中的文本,菜单中的文本等等。在资源脚本里建立一个字符串表很容易,就像这样:

STRINGTABLE
{
// entries go here
}

一个字符串表由几部分组成:一个标识字符串的数字;紧跟着一个逗号;然后是加了双引号的字符串本身。字符串表里的字符串被允许使用溢出符号,如
或 。注意,字符串表本身并没有标识符,所以每个程序只能有一个字符串表。一个简单的字符串表可能象下面这个样子:

// program information
STRINGTABLE
{
1, "3D Space Game v1.0"
2, "Written by The Masked Coder"
3, "(C) 2000 WienerDog Software"
}

从程序的字符串表里调用字符串,将使用——你可能猜到了——LoadString()函数。这是它的原形:

int LoadString(
HINSTANCE hInstance, // handle to module containing string resource
UINT uID, // resource identifier
LPTSTR lpBuffer, // pointer to buffer for resource
int nBufferMax // size of buffer
);

函数返回的实数是字符的数量,不包括空字符,它将被赋值到程序数据段的缓冲区中去,相当于字符串的长度。如果你调用了一个空字符串或者调用失败,都将返回0。下面来看看具体参数:
HINSTANCE hInstance:同以前的一样,你所有操纵项目的句柄。
UINT uID:你想要调用的字符串的数码标识符。
LPTSTR lpBuffer:指向接收字符串的字符数组的指针。
int nBufferMax:缓冲区的字节长度。如果被调用的字符串的长度大于缓冲区的长度,字符串将被按照缓冲区的大小缩减。
例如,调用“WienerDog Software’s copyright”的信息,代码应该如下:

char buffer[80];
LoadString(hinstance, 3, buffer, sizeof(buffer));

尽管在资源脚本中字符串使用数字声明,而不是标识符,但我通常在使用字符串表时,习惯于在头文件中用#define定义一下字符串的声明数字。针对上面的代码,我可能加一行:

#define ST_WIENERDOGCOPYRIGHT 3

这样一来,用LoadString()函数时,你的程序代码更容易读懂,也使你的思路更加清晰。但也并不是意味着你必须为字符串表里的每一个字符串都定义一个常量标识符。当字符串表相当大时,或者你感觉记不清时,就应该定义常量标识符。我通常在每个常量标识符的前面加上一个前缀ST_。具体的说,ST_FILENAMES作为存储文件名称字符串的索引,ST_DIALOGUE作为人物对话字符串的索引,等等。

☆ 菜单
这是我们要讲的最后一个Windows资源,当然,不是为了凑数才讲的哦。窗口的菜单条紧接在标题栏的下发显示,这个菜单有时被称为“主菜单”或“顶层菜单”。菜单通常在建立窗口类时被调用。还记得吗?上一章中窗口类建立过程中,有这样一行:
sampleClass.lpszMenuName = NULL;
如果你正在建立一个窗口程序,并希望有菜单,你就得需要用到菜单资源。它的脚本文件可能要复杂一点儿,但下面是一个最基本的框架:

[identifier] MENU
{
POPUP [menu name]
{
MENUITEM [item name], [identifier]
}
}

[identifier]标识符是你知道的:一个字符串或一个数字常量。在MENU的大括号中,可以有一个或者几个POPUP(弹出式)菜单,每一个都有一个下拉菜单,[menu name]中填入菜单名称。在POPUP的大括号中,可以有一个或者多个菜单条,[item name]中填入菜单条名称,后面必须跟着一个数字常量的标识符。(所谓数字常量的标识符,就是用#define定义过的标识符。如:#define MENUID_NEW 101)如果你还想在菜单里建立热键,就要用(&)符号。在你想成为热键的字符前加上&,例如,你想用Alt+F代替用鼠标点击File按钮,你就应该写成 &File ,菜单的名称都要用双引号引上。看看下面的例子就更清楚了:

MAIN_MENU MENU
{
POPUP "&File"
{
MENUITEM "&New", MENUID_NEW
MENUITEM "&Open...", MENUID_OPEN
MENUITEM "&Save", MENUID_SAVE
MENUITEM "Save &As...", MENUID_SAVEAS
MENUITEM "E&xit", MENUID_EXIT
}

POPUP "&Help"
{
MENUITEM "&Contents", MENUID_CONTENTS
MENUITEM "&Index...", MENUID_INDEX
MENUITEM "&About", MENUID_ABOUT
}
}

你还可以在POPUP下建立子菜单,你自己琢磨吧,我就不讲了,我们还得往下进行。获得菜单资源的句柄,我们需要用LoadMenu()函数,它的原形如下:

HMENU LoadMenu(
HINSTANCE hInstance, // handle to application instance
LPCTSTR lpMenuName // menu name string or menu-resource identifier
);

现在你应该已经熟悉这些参数了。第一个参数是你的程序实例的句柄,第二个是你的菜单资源的标识符。如果你使用了数字常量作为标识符,别忘了使用MAKEINTRESOURCE()这个宏转换一下哦!现在,你有两个方法为窗口创建菜单。第一个方法是在创建窗口类时直接设置:

sampleClass.lpszMenuName = LoadMenu(hinstance, MAKEINTRESOURCE(MAIN_MENU));

第二个方法是在设置窗口类时,让lpszMenuName等于NULL,以后再加入菜单。当你要建立两个独立的菜单,而又不想定义不同的窗口类时,这个选择是很有意义的,你需要用SetMenu()函数:

BOOL SetMenu(
HWND hWnd, // handle to window
HMENU hMenu, // handle to menu
);

如果创建菜单的功能实现了,将返回TRUE,否则返回FALSE。它的参数是很容易理解的:
HWND hWnd:是你所要创建菜单的那个窗口的句柄。也就是你在调用CreateWindowEx()时产生的那个窗口句柄。
HMENU hMenu:识别菜单,使用它的形式是:hMenu=LoadMenu(hInstance,菜单标识符),所以它得到的是LoadMenu()函数返回的菜单句柄。如果给它赋值NULL,指定窗口的菜单将被移除。
资源是个好东西,因为它使我们很容易就生成了菜单。但是当我们点击了菜单上的选项,将会发生什么呢?答案是Windows将会发出一个WM_COMMAND的消息给程序,程序再让Windows作出相应的反应。让我们具体看一看吧!

☆ 控制菜单事件
你可能记得,Windows的消息都是通过CALLBACK函数控制的,通常它是这个样子:WindowProc()或类似的样子。我们在上一章中用到的是这个样子:MsgHandler()。它的原形如下:

LRESULT CALLBACK MsgHandler(
HWND hwnd, // window handle
UINT msg, // the message identifier
WPARAM wparam, // message parameters
LPARAM lparam, // more message parameters
};

当一个菜单消息被送到,msg将等于WM_COMMAND,所选择的菜单项目将被包含进wparam。这就是为什么菜单的标识符不能是字符串的原因,它需要适合wparam参数。更特别的是,菜单标识符只占用wparam的低位字。WPARAM,LPARAM,int等都是32位,分高、低位字的变量。Windows提供了宏LOWORD()和HIWORD()分别来提取变量中的低位字和高位字,原形如下:

#define LOWORD(l) ((WORD) (l))
#define HIWORD(l) ((WORD) (((DWORD) (l) >> 16) & 0xFFFF))

LOWORD()宏的实际情况是,由于简单的定义为WORD,就自然的取得了低端的16位。HIWORD()函数把高端的16位向右移,然后同0xFFFF之间调用了逻辑“和”(AND),确保把高于16位的字节变为0。可能你不太熟悉>>和<<操作符号,它们是位移操作符。“<<”操作符把变量中的每一个字节中的数字向左移动,“>>”就是向右移动。例如,我们有一个16位的变量x,它的值是224,二进制表示为0000 0000 1111 0100。下面是一个关于位移的例子:

short int x = 244, y;
y = x << 4;

Contents(内容) of x: 0000 0000 1111 0100
Contents (内容)of y: 0000 1111 0100 0000

总之,使用LOWORD()宏你得到了wparam的低端字,也就是说你得到了被选择菜单的ID(标识符)。所以,在你的MsgHandler()函数中,你应该这样做:

// handle menu selections
if (msg == WM_COMMAND)
{
switch (LOWORD(wparam))
{
case MENUID_NEW:
// code to handle File->New goes here
break;
case MENUID_OPEN:
// code to handle File->Open goes here
break;

// the rest of the option handlers go here

}

// tell Windows you took care of it
return(0);
}

当然,还有一些其它的资源类型,如加速表(快捷键)、HTML页、WAV文件等。但我想以上这些是最有用,最要紧学习的。在结束之前,我还要告诉你Windows编程的一大强力特色——定制自己的资源类型。

☆ 定制资源
标准的程序资源给我们带来了很大方便。但不仅仅是这些标准的类型,你还可以创建自己的资源类型。资源可以是你希望的任何一种数据。使用自己定制的资源需要多付出一点劳动,因为你必须手工定位和读取资源数据。比想象的要容易,因为你已经习惯了定义资源的格式:

[identifier] [resource type name] [filename]

[resource type name]资源类型名称是让你命名的一个字符串。还是举例说明吧:假设我们要用到plconfig.dat文件作为资源,它包含初始化游戏人物的必需信息。我们将把它定义为CHARCONFIG资源类型,脚本文件应该是这个样子:

DATA_PLAYERINIT CHARCONFIG p1config.dat

很简单,是不是?现在,你已经拥有了数据(plconfig.dat),你还必须分三步使一个指针指向资源数据。这包括我们还没有提到过的需要调用的函数让我们一起解决。第一步,我们必须调用FindResource()函数去发现资源。函数原形如下:

HRSRC FindResource(
HMODULE hModule, // module handle
LPCTSTR lpName, // pointer to resource name
LPCTSTR lpType // pointer to resource type
);

返回值是一个资源信息块儿的句柄,如果调用失败,返回NULL。参数意义如下:
HMODULE hModule:HMODULE相当于HINSTANCE。不要问我为什么换了另一个名字,你只要把你的程序实例句柄传送给它就好了,你不需要什么类型转换,它们是相同的。
LPCTSTR lpName:这个是资源的标识符。如果你使用了数字的常量作为标识符,别忘了使用MAKEINTRESOURCE()宏。
LPCTSTR lpType:这个是资源的类型,你需要把你定义的资源类型名称的字符串传递给它。我们的是CHARCONFIG。
调用函数方式如下:

HRSRC hRsrc = FindResource(hinstance, MAKEINTRESOURCE(DATA_PLAYERINIT), "CHARCONFIG");

这是信息块儿所在资源的句柄。下一步是要得到指向数据的指针。需要把句柄传递给LoadResource()函数,来调用数据。这将产生一个资源本身的句柄。下面是函数的原形:

HGLOBAL LoadResource(
HMODULE hModule, // resource-module handle
HRSRC hResInfo // resource handle
);

返回类型HGLOBAL是一个普通句柄类型,是相对于我们说过的那些HBITMAP或HICON等句柄类型。如果调用函数失败,将返回NULL。参数解释如下:
HMODULE hModule:老东西,程序实例的句柄。
HRSRC hResInfo:把FindResource()得到的句柄传递给它。
现在,我们有了资源的句柄,就可以得到指向数据(自定义的)的指针了,这需要调用LockResource()函数来完成。原形如下:

LPVOID LockResource(HGLOBAL hResData);

仅仅把调用LoadResource()函数得到的句柄传递给它就万事大吉了。如果返回值是NULL,说明函数调用失败。否则,我们就得到梦寐以求的指针!现在我们可以自由得处理数据了。注意:返回的类型是LPVOID,(相当于void*),所以若你想把指针指向队列符号,你还要注意转换成类似BYTE*型的哦!现在,我们完成了所有的步骤,这里,我将展示给你一个指针指向特殊资源的实例:

UCHAR* LoadCustomResource(int resID)
{
HRSRC hResInfo;
HGLOBAL hResource;

// first find the resource info block
if ((hResInfo = FindResource(hinstance, MAKEINTRESOURCE(resID), "CUSTOMRESOURCETYPE")) == NULL)
return(NULL);

// now get a handle to the resource
if ((hResource = LoadResource(hinstance, hResInfo)) == NULL)
return(NULL);

// finally get and return a pointer to the resource
return ((UCHAR*)LockResource(hResource));
}

☆ 总结
好了,以上就是关于资源的部分。看,Windows编程比想象的容易吧。学了这么多,好像还是不能做什么,所以,下一章,我将向你介绍一些基本的Windows图形设备接口函数,你就可以利用我们所学过的所有东西作一点作品出来了。

 楼主| 发表于 2006-4-19 15:56:33 | 显示全部楼层

第三章 跟踪你的窗口和使用GDI

☆ 简介
如果你看过了头两章,你或许已经在问我什么时候能给你讲一点有成就感的东东呢?OK,时候到了。这次我们将学习WINDOWS GDI(图形设备接口)和其它一些相关的东西,象响应用户输入和处理Windows产生的一些消息。至于显示图形,我们将接触三个课题:文本显示,绘制象素,显示位图。我们先来研究一下几个Windows消息的细节。
重复的话:你需要C语言的基础知识,最好看过上两章。由于本章将使你能做一个具体的图形DEMO,有一个源代码例程附在本章后面。是用Visual C++写的和编译的,所以吗,最好同我保持一致哦!好了,开动吧!

☆ 设备上下文
在第一章里,我们创建和注册了一个窗口类,其中有一行定义了窗口的风格(功能),是这个样子:

sampleClass.style = CS_DBLCLKS | CS_OWNDC | CS_HREDRAW | CS_VREDRAW; // standard settings

其中三个属性是很一般的,但这个——CS_OWNDC,需要解释一下。如果你记得,我曾经告诉过你这个属性允许窗口有自己独特的设备上下文,但直到现在,我们还没有具体的讲,OK,时间到了,开讲!
设备上下文是一个结构,是一个表现一组图形对象和属性的结构,还有一些输出设备的设置和属性。使用设备上下文允许你直接操纵图形,不用考虑低级细节。Windows GDI是一个图形翻译系统,是介于应用程序和图形硬件之间的一层。GDI可以输出到任意的兼容设备,不过最常使用的设备是视频监视器、图形硬拷贝设备(如打印机或绘图仪),或者是内存中的图元文本。GDI函数能够绘制直线、曲线、封闭的图形和文本。所有访问GDI的Windows函数都需要一个设备上下文句柄作为参数。感谢上帝,这是非常容易做到的。你若想得到一个窗口的设备上下文句柄,你可以用这个函数:

HDC GetDC(
HWND hWnd // handle to a window
);

很简单,是不是?所有你做的是,把要操作的窗口的句柄传递给它,然后返回一个设备上下文句柄。如果你传递的是NULL,将返回整个屏幕的设备上下文(DC,以后都用DC表示)句柄。如果函数调用失败,将返回NULL。
设备上下文不仅仅处理图形,但我们习惯于泛泛的认为它是处理图形的。处理显示图形的DC类型,称作显示DC,处理打印的,称作打印DC;处理位图数据的,称作内存DC,还有其它一些设备DC。感觉有点复杂吧,不要紧,这是Windows,它的主要功能就是迷惑群众。^_^一旦我们接触一些代码,就不会觉得难了。
当你结束使用DC时,一定要释放它,也就是释放它占用的内存空间。要把这种思想贯穿到以后的编程中去,占用了内存,不用时要释放,切记!释放DC是一个很简单的函数:

int ReleaseDC(
HWND hWnd, // handle to window
HDC hDC // handle to device context
);

若成功释放,返回值是1,否则是0。参数有注释,我还是说一下:
HWND hWnd:你所要控制的那个窗口的句柄。如果你开始传递的是NULL,现在还要传递NULL。
HDC hDC:DC的句柄。
在用DC和GDI进行图形显示前,我们先看看创建窗口实例时要遇到的几条重要的消息。我将要提到的四条消息是:WM_MOVE、WM_SIZE、WM_ACTIVATE、WM_PAINT。

☆ 追踪窗口状态
头两个是很简单的。当窗口被用户移动时将发送WM_MOVE消息,窗口新位置的坐标储存在lparam中。(还记得吗,消息在lparam和wparam中被进一步描述,它们是消息控制函数的参数)lparam的低端字中存储窗口客户区左上角的坐标x,高端字中存储坐标y。
当窗口的大小被改变时,将发送WM_SIZE消息。同WM_MOVE消息差不多,lparam的低端字中存储客户区的宽度,高端字存储高度。同WM_MOVE不同的是,wparam参数也控制了一些重要的东西。它可以是下列中任意一个值:
※ SIZE_MAXHIDE:其它的窗口被最大化了。
※ SIZE_MAXIMIZED:本窗口被最大化了。
※ SIZE_MAXSHOW:其它的窗口被还原了。
※ SIZE_MINIMIZED:本窗口被最小化了。
※ SIZE_RESTORED:窗口被改变了尺寸,但既没最大化,也没有最小化。
当我编写窗口实例时,我通常喜欢把窗口的当前位置和大小保留在几个全局变量里。假设我们命名这些全局变量为xPos,yPos,xSize和ySize,你最好这样控制WM_SIZE和WM_MOVE这两个消息:

if (msg == WM_SIZE)
{
xSize = LOWORD(lparam);
ySize = HIWORD(lparam);
}
if (msg == WM_MOVE)
{
xPos = LOWORD(lparam);
yPos = HIWORD(lparam);
}

现在轮到WM_ACTIVATE消息了。它告诉你一个新窗口被激活。这是很有用的,因为如果出现优先的申请,你就不可能处理程序里的所有逻辑。有时,例如写一个全屏的DIRECTX程序,忽略WM_ACTIVATE消息将导致你的程序出现致命的错误,可能它做了一些你不希望它做的事情。在任何情况下,守候WM_ACTIVATE消息从而采取行动,是一个好主意。
窗口被激活和被解除激活都会发出WM_ACTIVATE消息,我们可以通过检测wparam的低端字来得知是被激活还是被取消。这将有三种可能的值:
※ WA_CLICKACTIVE:窗口被鼠标激活。
※ WA_ACTIVE:窗口被其它东西激活。(键盘、函数调用、等等)
※ WA_INACTIVE:窗口被解除激活。
为了处理这个消息,我保留了另一个全局变量bFocus,当接收到WM_ACTIVATE消息,它的值将改变。示例如下:

if (msg == WM_ACTIVATE)
{
if (LOWORD(wparam) == WA_INACTIVE)
focus = FALSE;
else
focus = TRUE;

// tell Windows we handled it
return(0);
}

有两个相关联的消息WM_KILLFOCUS和WM_SETFOCUS,在窗口接收到输入焦点的时候,Windows消息WM_SETFOCUS被发送给它,在失去焦点的时候则发送WM_KILLFOCUS消息。应用程序可以截取这些消息以得知输入焦点的任何改变情况。什么是输入焦点呢?存有输入焦点的应用程序(窗口)就是被激活的那个窗口。你就认为被激活的窗口就是输入焦点就行了。因为可能出现没有窗口具有输入焦点,所以我建议用WM_ACTIVATE消息跟踪你的窗口状态。(有些胡涂?不要紧,你就记住用WM_ACTIVATE就行了)往下进行。

☆ WM_PAINT 消息
WN_PAINT消息通知程序,全部或部分客户窗口需要重新绘制。当用户在最小化、重叠或调整客户窗口区域的时候,就会产生这条消息。重新绘制,你需要做两件事,首先是要用到WM_PAINT消息专用的一对函数,第一个是BeginPaint()函数,这是它的原形:

HDC BeginPaint(
HWND hwnd, // handle to window
LPPAINTSTRUCT lpPaint // pointer to structure for paint information
);

在我告诉你返回值是什么之前,让我们先看看参数:
HWND hwnd:需要重绘的窗口的句柄。你应该已经对于这种参数比较熟悉了。
LPPAINTSTRUCT lpPaint:这是很重要的一个。是指向PAINTSTRUCT结构的指针,该结构包含所有的要被重绘区域的信息。
继续之前,我应该给你看看PAINTSTRUCT结构:

typedef struct tagPAINTSTRUCT { // ps
HDC hdc;
BOOL fErase;
RECT rcPaint;
BOOL fRestore;
BOOL fIncUpdate;
BYTE rgbReserved[32];
} PAINTSTRUCT;

结构内的成员如下:
HDC hdc:哈哈,有理由复习一下设备上下文(DC,有的书中叫“设备描述表”)了。这是要被刷新区域的句柄。
BOOL fErase:指明应用程序是否应该抹去背景。如果是FALSE,说明系统已经删除了背景。还记得在Windows类中我们曾经用黑色画刷定义了一个背景吗?这就意味着系统将用这个画刷抹去无效的区域。
RECT rcPaint:这是最重要的一个成员。它将告诉你需要被重绘的无效区域的矩形。我将稍后告诉你RECT结构。
BOOL fRestore,BOOL fIncUpdate,BYTE rgbReserved[32]:好消息,这些是保留成员,为老Windows服务的,所有你我都不必管它们。:)
现在我已经给你看了这么多,这就是BeginPaint()函数的全部。它做了三件事儿。首先,它使窗口再次有效,直到下一次被改变,WM_PAINT消息发出前,这个窗口都是有效的。第二,如果在窗口类(Windows class)里定义了背景画刷,就像我们做过的那样,就用这个画刷重绘无效的区域。(所谓无效,就是被改变的)第三,返回了被重绘区域的DC句柄。重绘的区域,是由RECT结构定义的:

typedef struct _RECT {
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT;

我们已经指出这个结构描绘了一个矩形,但是有一件事情需要说说。RECT包含左上角,但不包含右下角。什么意思呢?让我们先定义一个RECT对象:

RECT myRect = {0, 0, 5, 5};

这个RECT包含象素(0,0),但是没有达到(5,5),所以矩形的右下角实际是(4,4)。看起来没有什么意义,但是你得习惯它。
现在,还记得我所说的关于使用DC的事儿吗?一旦你用完了,你就必须释放它。OK,你记起来了,用EndPaint()函数释放。回应WM_PAINT消息,每次调用完BeginPaint()函数,必须匹配一个EndPaint()函数释放DC。这是函数的原形:

BOOL EndPaint(
HWND hWnd, // handle to window
CONST PAINTSTRUCT *lpPaint // pointer to structure for paint data
);

函数通过返回TRUE或FALSE分别表明成功还是失败。有两个简单的参数:
HWND hWnd:就是窗口的句柄。
CONST PAINSTRUCT *lpPaint:指向PAINTSTRUCT类型的结构变量地址。同BeginPaint()的第二个参数是一回事。不要被CONST迷惑了,它只是保证和确认函数没有改变结构的内容。
你还可以通过调用ValidateRect()函数代替BeginPaint()函数使得窗口再次有效。但你得手工操作一切。可能我们真的什么时候就要用到它。所以给你它的原形:

BOOL ValidateRect(
HWND hWnd, // handle of window
CONST RECT *lpRect // address of validation rectangle coordinates
);

通过返回TRUE或FALSE来确定函数调用成功还是失败。参数很简单:
HWND hWnd:烦不烦,我不说了:)
CONST RECT *lpRect:是指向RECT结构是否有效的指针。如果你传递NULL,则整个客户区域都是有效的。
现在把以上讲到的做个样子给你看吧,假设我们已经定义了一个全局的变量hMainWindow作为我们的窗口句柄。

if (msg == WM_PAINT)
{
PAINTSTRUCT ps; // declare a PAINTSTRUCT for use with this message
HDC hdc; // display device context for graphics calls

hdc = BeginPaint(hMainWindow, &ps); // validate the window

// your painting goes here!

EndPaint(hMainWindow, &ps); // release the DC

// tell Windows we took care of it
return(0);
}

这段代码很简单,也没做什么令人兴奋的事儿!OK,如果你不想用窗口类里的默认画刷重绘窗口,你必须自己做一些事情,包括使用我们还没有讲的图形部分。永远不要害怕,我们过一会儿就讲。现在我们在讨论消息,还有一些事情我必须解释。

☆ 关闭你的应用程序(关闭窗口)

 楼主| 发表于 2006-4-19 15:57:42 | 显示全部楼层
☆ 关闭你的应用程序(关闭窗口)

有三个消息看起来差不多,都是处理关闭的事情的。它们是WM_DESTROY,WM_CLOSE,和WM_QUIT。它们的确很相似,但你需要知道它们之间的不同!一个窗口或者应用程序应该被关闭时发出WM_CLOSE消息,当接收到WM_CLOSE消息时,如果你愿意,向用户提出是否真的退出。你知道让用户作确认或有错误出现或有什么应该注意的事情发生的时候,往往弹出一个消息框。消息框的制作是很容易的,由于它用途广泛,我们还是介绍一下:

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 hWnd:过一会儿我将向你介绍一个不含有它的函数,我保证。
LPCTSTR lpText:这是将要显示在消息框里的文本。你可以用\n等调整一下格式。
LPCCTSTR lpCaption:这是显示在消息框标题栏里的文本。
UINT uType:这个参数可以联合使用几个不同的标记。这些标记可以根据你的目的选择,有好多MB_打头的标记供你选择,联合使用时要用“|”分隔开。下面列出了一些常用的:

……按钮类
◎ MB_ABORTRETRYIGNORE:建立有“Abort”、“Retry”、“Ignore”按钮的消息框。
◎ MB_OK:建立有“OK”按钮的消息框。
◎ MB_OKCANCEL:建立有“OK”和“Cancel”按钮的消息框。
◎ MB_RETRYCANCEL:建立有“Retry”、和“Cancel”按钮的消息框。
◎ MB_YESNO:建立有“Yes”和“NO”按钮的消息框。
◎ MB_YESNOCANCEL:建立有“Yes”、“No”和“Cancel”按钮的消息框。

……图标类
◎ MB_ICONEXCLAMATION:加个惊叹号图标。
◎ MB_ICONINFORMATION:加个消息图标。(好像是个问号)
◎ MB_ICONQUESTION:加个问号图标
◎ MB_ICONSTOP:加个停止图标。

……默认按钮标志
◎ MB_DEFBUTTON1:设置第一个按钮为默认按钮。(默认按钮即消息框弹出后,直接敲回车就被按下的那个按钮)
◎ MB_DEFBUTTON2:第二个为默认按钮。
◎ MB_DEFBUTTON3:第三个为默认按钮。
◎ MB_DEFBUTTON4:第四个为默认按钮。

……其它的标志
◎ MB_HELP:添加一个帮助按钮。通常按下该按钮或者敲F1键都将产生WM_HELP消息。
◎ MB_RIGHT:文本右对齐。
◎ MB_TOPMOST:设置消息框总在窗口的最上面。

我不知道你是怎么想的,但是我想Microsoft一定有一个程序员除了一件事,其它什么都不做,那就是全天写#define声明!^_^ 如果消息框建立失败,返回值为0,否则是下列任一个值:
◎ IDABORT:“Abort”按钮被选择。
◎ IDCANCEL:“Cancel”按钮被选择。
◎ IDIGNORE:“Ignore”按钮被选择。
◎ IDNO:“No”按钮被选择。
◎ IDOK:“OK”按钮被选择。
◎ IDRETRY:“Retry”按钮被选择。
◎ IDYES:“Yes”按钮被选择。
以上说了这么多,我几乎都忘了我们原来的话题了。总之,当收到WM_CLOSE消息,你可以做两件事儿。一件是你接受默认的处理返回一个值,你若这样做了,应用程序或窗口按照计划关闭;再者,你返回0,应用程序或窗口将保持原样。以下是代码的基本部分:

if (msg == WM_CLOSE)
{
if (MessageBox(hMainWindow, "Are you sure want to quit?", "Notice", MB_YESNO | MB_ICONEXCLAMATION) == IDNO)
return(0);

// otherwise, let the default handler take care of it
}

WM_DESTROY消息有点儿不同。它是窗口正在关闭时发出的。当得到WM_DESTROY消息的时候,窗口已经从视觉上被删除。一个主窗口被关闭,并不意味着应用程序结束了,它将在没有窗口的条件下继续运行。然而,当一个用户关闭了主窗口,也就意味着他要结束应用程序,所以如果你希望应用程序结束,在收到WM_DESTROY消息的时候,你必须发出一个WM_QUIT消息。你可以使用PostMessage()函数,但由于这是一个特殊的情况,就为它准备了一个特殊的函数:

VOID PostQuitMessage(int nExitCode);

参数nExitCode是你的应用程序返回给Windows的一个退出代码(通常是0)。记住,WinMain()返回的是一个int(实数),不是void(空的)。nExitCode参数的值被赋值给wparam。WM_QUIT消息表示要关闭应用程序,所以得到这个消息后,你应跳出循环,把wparam返回给Windows。下面是一个简单的WinMain()函数实例:

int WinMain(HINSTANCE hinstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
{
// initialization stuff goes here

// main loop - infinite!
while (TRUE)
{
// check the message queue
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT) // exit main loop on WM_QUIT
break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}

// main program logic goes here
}

// perform any shutdown functions here - releasing objects and such

return(msg.wparam); // return exit code to Windows
}

对不起,罗罗嗦嗦讲了这么多,但这些都是你必须要了解的。继续你对我的耐心,让我们看看基础的GDI图形。
 楼主| 发表于 2006-4-19 15:58:10 | 显示全部楼层
☆ 绘制象素
终于到了本章的戏肉儿!只要你取得了显示设备上下文的句柄,就可以用GDI绘制象素了。记住,要调用GetDC()取得句柄。绘制单个的象素,不出意外的话,你就用SetPixel()函数:

COLORREF SetPixel(
HDC hdc, // handle to device context
int X, // x-coordinate of pixel
int Y, // y-coordinate of pixel
COLORREF crColor // pixel color
);

返回类型COLORREF是我们没有讲过的。它不是一个结构,但是在表格0x00bbggrr里是一个32位的值。Bb是蓝色成分的8位值,gg是绿色,rr是红色。高字节00没有用,总是00。让我们看看函数里面的参数:
HDC hdc:你将要GetDC()得到的DC句柄。(DC就是设备上下文)你只能调用GetDC()一次,然后其它的函数也可以用这个DC句柄了。你每次绘制单个象素不用再取得新的DC句柄了。
int X,Y:是象素的x和y坐标。是在客户区的坐标,(0,0)即是窗口客户区左上角的坐标,不是屏幕左上角的坐标。
COLORREF crColor:象素的颜色。设置象素的颜色,用RGB()宏最简单。RGB()括号中的三个值分别是红色、绿色和蓝色,各个颜色的值可从0-255间取,不同的值组合成不同的颜色。
如果函数调用成功,将返回一个颜色,就是你要绘制的象素的颜色。由于视频硬件的限制,返回的颜色可能与调用函数时要求的颜色并不一致。如果调用失败,返回-1。如果你要设置窗口客户区的左上角为红色,函数将是这样:

SetPixel(hdc, 0, 0, RGB(255,0, 0));

是在假设你已经取得了DC句柄hdc后,调用这个函数。很简单,是不是?这个函数的更快的版本是SetPixelV():

BOOL SetPixelV(
HDC hdc, // handle to device context
int X, // x-coordinate of pixel
int Y, // y-coordinate of pixel
COLORREF crColor // new pixel color
);

参数相同,但返回类型不同。SetPixelV()返回的是布尔类型,成功,TRUE;失败,FALSE。如果你没有必要使用SetPixel()提供的额外信息,那我们当然是选择使用SetPixelV()了。(我们总是希望快一些)
还有一件事情,我们需要得到绘制好的象素的值(颜色)。没问题,使用GetPixel()函数就解决了:

COLORREF GetPixel(
HDC hdc, // handle to device context
int XPos, // x-coordinate of pixel
int nYPos // y-coordinate of pixel
);

很明显,返回值是象素所在位置的颜色。如果坐标出了DC句柄控制的区域,返回值是CLR_INVALID。参数同SetPixel()差不多:DC句柄,象素的x,y坐标。绘制象素就说到这吧,下面看看GDI的文本输出。

☆ GDI的文本输出函数
有两个关于绘制文本的函数,其中简单一点儿的是TextOut()函数,原形是:

BOOL TextOut(
HDC hdc, // handle to device context
int nXStart, // x-coordinate of starting position
int nYStart, // y-coordinate of starting position
LPCTSTR lpString, // pointer to string
int cbString // number of characters in string
);

我们已经看过很多返回类型是布尔类型的函数了,意思都一样:成功TRUE,失败FALSE。函数的参数如下:
HDC hdc:DC句柄。它既可以使GetDC的返回值,也可以是在处理WM_PAINT消息时BeginPaint的返回值。
int nXStart,nYStart:定义客户区内字符串开始的位置。字符串中第一个字符的左上角位于坐标(xXStart,nYStart)。在默认DC中,客户区的左上角为(0,0)。
LPCTSTR lpString:要显示的字符串。由于下一个参数给出字符串的长度,所有字符串不需要空的中止符。
int cbString:串中字符的个数。(字符串的长度)
TextOut()函数使用当前的文本颜色,背景颜色和背景类型。你若想自己改变这些,就得用如下的函数:

COLORREF SetTextColor(
HDC hdc, // handle to device context
COLORREF crColor // text color
);

COLORREF SetBkColor(
HDC hdc, // handle of device context
COLORREF crColor // background color value
);

SetTextColor()函数设置当前文本颜色,SetBkColor()函数设置当前背景颜色。参数的解释是显而易见的。第一个是DC句柄。第二个是颜色,你当然记得可以用RGB()宏来设置颜色。例如设置文本为蓝色:SetTextColor(hdc,RGB(0,0,255))。设置背景为白色:SetBkColor(hdc,RGB(255,255,255))。最后,设置背景类型用SetBkMode()函数:

int SetBkMode(
HDC hdc, // handle of device context
int iBkMode // flag specifying background mode
);

第一个参数就不讲了。第二个参数iBkMode可以分别取两个值:TRANSPARENT或OPAQUE。如果取TRANSPARENT,文本不会对背景造成影响。如选择OPAQUE,则相反。函数的返回值是背景的模式。
关于TextOut()函数还有一点要说,你可以控制文本的排列方式,用SetTextAlign()实现:

UINT SetTextAlign(
HDC hdc, // handle to device context
UINT fMode // text-alignment flag
);

参数如下:
HDC hdc:DC句柄。没有什么特别的。
UINT fMode:一个标志或一组标志(用“|”分开)将决定TextOut()输出文本的对齐模式。你不可以随便组合,要合理组合。这些标志如下:
◎ TA_BASELINE:参考点将在文本的基线。
◎ TA_BOTTOM:参考点将在矩形范围的底边。
◎ TA_TOP:参考点将在矩形范围的顶部。
◎ TA_CENTER:参考点将在矩形范围的中心。
◎ TA_LEFT:参考点将在矩形范围的左边。
◎ TA_RIGHT:参考点将在矩形范围的右边。
◎ TA_NOUPDATECP:当前的位置没有被文本输出函数调用,参考点被每次调用传递。
◎ TA_UPDATECP:当前的位置被每次文本函数输出调用,当前的位置作为参考点。
默认的设置是TA_LEFT|TA_TOP|TA_NOUPDATECP。如果你设置成TA_UPDATECP,后来调用的TextOut()函数将忽略nXStart和nYStart这两个参数,把文本放置在……。OK,告一段落,我们来看看TextOut()函数的兄弟DrawText():

int DrawText(
HDC hDC, // handle to device context
LPCTSTR lpString, // pointer to string to draw
int nCount, // string length, in characters
LPRECT lpRect, // pointer to struct with formatting dimensions
UINT uFormat // text-drawing flags
);

这个家伙有点儿复杂。DrawText()函数能格式化文本,多种排列方式。返回值是文本象素的高度。返回0,说明调用失败。让我们看看它的参数:
HDC hDC:一样的东东。我们的好朋友DC句柄。
LPCTSTR lpString:要显示的字符串。用双引号引起来。
int nCount:字符串中字符的数量。(字符串长度)
LPRECT lpRect:是RECT类型结构的地址,该结构包含了将要显示字符串的区域的逻辑坐标。
UINT uFormat:文本格式选项,你可以用“|”符号组合。下面列出最常用到的标志:
◎ DT_BOTTOM:指定底部对齐文本。必须与DT_SINGLELINE组合使用。
◎ DT_CALCRECT:返回矩形的宽度和高度。在多文本行的情况下,DrawText()将使用lpRect所指向的矩形的宽度,并扩展矩形的底部直到包含文本的最后一行。在单文本行的情况中,DrawText()将改变矩形的右边界,使它包含最后一个字符。不管在那种情况下,DrawText()都返回格式化后的文本高度,但是不重新绘制文本。
◎ DT_CENTER:文本水平居中。
◎ DT_EXPANDATABS:扩充Tab键跳跃的字符数,默认情况下,每按一次Tab键跳跃8个字符。
◎ DT_LEFT:指定文本左对齐。
◎ DT_NOCLIP:绘制屏幕时无需剪切。当使用DT_NOCLIP后,程序性能提高。
◎ DT_RIGHT:指定文本右对齐。
◎ DT_SINGLELINE:指定单行文本,忽略回车和换行。
◎ DT_TABSTOP:设置Tab键停止。在uFormat的低端字的高阶字节(15-8)中存放Tab键每按一次跳跃的字符数。默认是8。
◎ DT_TOP:顶部对齐文本(仅用于单行文本)。
◎ DT_VCENTER:指定垂直居中(仅对单行文本)。
还有一些其它的标志,但你看到的已经足够了。有了这些,你就可以轻松驾驭文本了,但记住,是以牺牲函数速度为代价的。你可以选择比较常用的TextOut()函数。文本输出就说道这吧,让我们去学点儿令人兴奋的东东吧!
 楼主| 发表于 2006-4-19 15:59:49 | 显示全部楼层
☆ 用GDI显示位图
记得我告诉过你位图是很容易操纵的,因为Windows本身就是位图。现在让我们看看到底有多容易吧!用GDI显示位图需要四个基本的步骤:
1、得到你要操作的窗口的DC句柄。
2、获得位图的句柄。
3、为位图创建设备上下文。
4、传送位图。
你已经知道第一步怎么做了,以前我也间接提到过第二步的做法,但没有具体说。我说过通过函数LoadBitmap()可以得到位图资源的句柄,但它有些过时了,有一个更灵活的函数LoadImage()取代了它,现在让我们看看怎么使用这个新函数。原形如下:

HANDLE LoadImage(
HINSTANCE hinst, // handle of the instance containing the image
LPCTSTR lpszName, // name or identifier of image
UINT uType, // type of image
int cxDesired, // desired width
int cyDesired, // desired height
UINT fuLoad // load flags
);

如果函数调用失败,返回NULL。成功,你得到位图的句柄,意味着你就可以从资源或外部文件调用位图了。注意,这个函数还可以取得光标、图标的句柄,所以返回类型只是简单的HANDLE。在Visual C++6.0中,你需要用HBITMAP类型定义位图的句柄变量,否则编译器会生气的。例如:

HBITMAP hBitmap;
hBitmap =LoadImage(……);

下面是LoadImage()函数的参数说明:
HINSTANCE hinst:如果你从资源调用位图,这应该是你的应用程序的实例。如果你要从外部文件调用位图,就把它设置为NULL。
LPCTSTR lpszName:这个要么是资源标识符,记住用MAKEINTRESOURCE()宏转变数字常量;要么就是你要调用的图象的完整文件名称。
UINT uType:根据你的调用对象来决定。应该是IMAGE_BITMAP、IMAGE_CURSOR和IMAGE_ICON中的一种。
int cxDesired,cyDesired:这是你希望的图象的尺寸。如果你都设置为0,将是图象的真实尺寸。
UINT fuLoad:这是可以组合用的标志,当然是用“|”来连接。以下是一些常用的标志:
◎ LR_CREATEDIBSECTION:如果uType是IMAGE_BITMAP,将导致函数返回一个DIB(DIB,设备无关的位图)。基本的意思就是返回一个不依赖于显示设备的位图。
◎ LR_DEFAULTSIZE:对于光标和图标,如果cxDesired和cyDesired都设置为0,这个标志将启用Windows的默认尺寸,而不是图形的实际尺寸。
◎ LR_LOADFROMFILE:如果你要从外部文件中调入图象,你就必须用这个标志。
条件允许的话,你应当尽量使用LR_CREATEDIBSECTION和LR_LOADFROMFILE这两个标志。现在,你已经得到了图象(过去我们总说位图,好像不太准确,毕竟有时我们不从资源里调用)的句柄,下一步你必须建立设备上下文把图象放进去。位图的应该独有的特点是:它只能被选入到内存设备上下文中。内存设备上下文被定义为一个具有显示表面的设备上下文。它只存在于内存中,并且与特定的设备上下文相关。使用内存设备上下文,你必须首先创建它。CreateCompatibleDC()函数正是用于这个目的。它的一般形式如下:

HDC CreateCompatibleDC(HDC hdc);

函数唯一的参数是与内存设备上下文相兼容的设备上下文句柄。如果内存设备上下文与视频屏幕兼容,则这个参数可以为NULL。我们就用NULL。如果函数失败,返回NULL。现在,我们把位图(或图象)放入内存失败上下文,我们用这个函数:

HGDIOBJ SelectObject(
HDC hdc, // handle to device context
HGDIOBJ hgdiobj // handle to object
);

函数的返回类型HGDIOBJ是一个比HBITMAP更通用的类型。不用担心,HGDIOBJ和HBITMAP的一致性没有任何问题。以下是函数的参数说明:
HDC hdc:是设备上下文的句柄。要调用图象,必须是内存设备上下文的句柄。
HGDIOBJ hgdiobj:要调用对象的的句柄。可调用的有位图、画刷、字体、画笔等。这里是位图(图象)的句柄。
返回值是要调入设备上下文中的对象的句柄。这里是位图的句柄。如果失败,返回NULL。
现在你已经把位图装入设备上下文,你还需要进行最后的一步:把内存DC里的内容拷贝到显示设备上下文中。然而,我们首先要得到位图的一些信息,如尺寸,这是显示图象时必须的。所以,我们还需要另一个函数GetObject(),它可以用于获得对象的信息,当然,这里我们是要获得位图的信息。函数的一般形式如下:

int GetObject(
HGDIOBJ hgdiobj, // handle to graphics object of interest
int cbBuffer, // size of buffer for object information
LPVOID lpvObject // pointer to buffer for object information
);

返回值是一个字节数。如果失败,返回0。当GetObject()调用的目标是位图时,返回的信息是与位图的宽度、高度和颜色格式有关的结构成员。参数说明如下:
HGDIOBJ hgdiobj:要得到信息的对象的句柄。这里我们传送位图的句柄。
int cbBuffer:存放调用返回的信息的缓冲区的大小。对于位图,我们将得到BITMAP类型结构,所以这里设置成sizeof(BITMAP)。
LPVOID lpvObject:指向存放由调用返回的信息的缓冲区的指针。
你需要定义一个BITMAP结构类型的变量,调用GetObject()函数放入缓冲区的信息。由于BITMAP结构对我们来说是一个新的结构,所以就介绍一下:

typedef struct tagBITMAP { // bm
LONG bmType;
LONG bmWidth;
LONG bmHeight;
LONG bmWidthBytes;
WORD bmPlanes;
WORD bmBitsPixel;
LPVOID bmBits;
} BITMAP;

很多成员,但我们实际上只对其中的两个有兴趣。但我们还是都介绍一下:
LONG bmType:指定位图类型,必须为0。有用吧!
LONG bmWidth,bmHeight:分别是位图的宽度和高度,以象素为单位。必须都大于0。
LONG bmWidthBytes:指定每一行扫描线中的字节数。因为Windows假定位图是字对齐的,所以这个值必须能够被2整除。
LONG bmPlanes:指定颜色面的数目。
LONG bmBitsPixel:指定表述象素颜色所需的位数。(好像没什么用)
LPVOID bmBits:如果你想存取实际的位图数据,这个指针指向位图位值得位置。
OK,差不多了。一旦位图被选入内存设备上下文,且代码已经得到了位图宽度和高度的必要信息后,我们就可以将内存中存储的位图通过位块传输到达屏幕,然后在任意位置对它进行显示。我说过处理位图是很容易的,对吧?(如果你觉得不容易,只能怪我说的不够好)有两个函数需要说明,先说第一个:

BOOL BitBlt(
HDC hdcDest, // handle to destination device context
int nXDest, // x-coordinate of destination rectangle's upper-left corner
int nYDest, // y-coordinate of destination rectangle's upper-left corner
int nWidth, // width of destination rectangle
int nHeight, // height of destination rectangle
HDC hdcSrc, // handle to source device context
int nXSrc, // x-coordinate of source rectangle's upper-left corner
int nYSrc, // y-coordinate of source rectangle's upper-left corner
DWORD dwRop // raster operation code
);

BitBlt()函数是执行位图显示操作最简单且最直接的方法。根据函数调用的成功或失败,返回值是TRUE或FALSE。布尔函数都是这样。有很多参数,但很好理解:
HDC hdcDest:目标设备上下文句柄。根据我们的情况,应该是显示设备上下文句柄。
int nXDest,nYDest:目标矩形左上角的x、y坐标,也就是被显示的位图左上角的屏幕位置。
int nWidth,nHeight:位图的宽度和高度。
HDC hdcSrc:原来的设备上下文句柄。根据我们的情况,应该是内存设备上下文句柄。
int nXSrc,nYSrc:源位图的x、y坐标。由于进行位块传输的矩形必须与在参数nWidth和参数nHeight中定义的尺寸相同,所以通常都设为0。但不一定总是这样,例如你只想显示位图的一部分,就不能都设置为0。
※ DWORD dwRop:有很多光栅代码你可以选择,但只有一个我们感兴趣,SRCCOPY。它直接把源DC内的内容拷贝到目标DC中。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|3D数字艺术论坛 ( 沪ICP备14023054号 )

GMT+8, 2024-5-21 21:00

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表