第1章关于Windows大家好,欢迎开始DirectX103D游戏高级开发的学习之旅,这里将是我们深入学习的起点.
要学好3D游戏编程,建立完善的知识体系,首要任务是了解与Windows有关的基本知识.
我们将先用较短的时间来学习一些简单的操作,如程序的打开和关闭、基本输入的处理、基本图元的绘制等.
如果您对WindowsAPI已经很熟悉,在阅读本章内容时将倍感轻松;否则,请仔细阅读本章!
本章主题:Windows的一些理论知识以及如何使用Win32API进行程序开发Win32游戏开发和标准的Windows编程有何不同消息及消息处理标准消息泵和实时消息泵Win32编程COM,即组件对象模型(ComponentObjectModel)其他更丰富的内容1.
1关于Windows经过15年的发展,Windows拥有一套非常标准的API,即应用程序编程接口(ApplicationProgrammingInterface).
1992年,我们为Windows3.
11操作系统所编写的程序,理所当然地仍旧可以在WindowsVista下运行.
所以,我们在本章所学习的内容作为Windows编程标准已经存在十多年了.
本书的所有代码都是基于DirectX10的,因此仅能运行于WindowsVista及后续版本的操作系统,也就是说WindowsVista是保证本书代码能够正常运行的最低配置的操作系统.
当然,绝大多数内容只需稍加修改就能适用于之前版本的Windows和DirectX.
现在的Windows程序同古老的DOS程序或Win16程序相比,在很多方面都有着根本性的变革.
在过去,运行一个程序将占用100%的CPU时间,且程序员对所有的设备和文件有100%的控制权.
因此您还必须熟知用户机器中的设备的诸多细节(您可能还记得那些神秘的DOS或Win16游戏,它们几乎总是要求您输入某些设备的DMA和IRQ配置,如声卡的配置).
这样当游戏出错时,整个系统都会随之崩溃.
对于终端用户而言,这意味着只能选择重新启动机器.
谢天谢地,在WindowsVista和DirectX10中,上述情形完全不会出现.
当您的程序执行时,它会和许多其他进程一起共享CPU,并发(在同一时间)地运行.
因此,您无法对声卡、显卡、硬盘以及其他所有的系统资源施以完全的控制.
输入、输出也已被抽象出来.
这无疑是一件非常好的事,但这也需要您在最开始时多花费一些学习时间.
一方面,Windows应用程序都具有一致的界面(lookandfeel).
所以,几乎所有开发人员创建的Windows应用程序都自动地被Windows用户所熟悉.
他们早就了解如何使用菜单和工具栏,因此,如果使用Windows的基本框架来构建应用程序,用户很快就知道如何使用这个用户界面.
另一方面,应该对Windows和其他应用程序抱有足够的信心.
在DirectX出现之前,我们只能使用一些Windows默认的绘图命令(被称为GDI或图形设备接口)来绘图.
虽然GDI可以自动处理任意位深度的图像,并且能在任何监视器上工作,但是效率非常低.
WindowsVista采用了全新的显示驱动模型,该模型完全重写了使用DirectX的用户接口.
经过这种改进后,效率比以往的Windows版本快了许多,但对于格斗游戏来说仍不是特别理想.
由于这个原因,很多DOS程序开发人员发誓不在Windows上工作.
在渲染复杂场景时,最好的方案是将场景绘制到一幅位图中,然后再将该位图呈现到某一窗口中,这是一个非常缓慢的过程.
编写Windows应用程序时,我们必须放弃许多东西.
然而,很多在Windows中能做到的事情,对于DOS环境来说简直就是个噩梦.
在Windows编程中,我们可以仅使用一行简单的代码(PlaySound函数)来实现音效,查询时间戳计数器,使用健壮的TCP/IP网络栈,访问虚拟内存等.
虽然需要花点时间到处找这些功能函数,但是Windows的优势远远胜过它的劣势.
本书所有应用程序的代码都是在WindowsVista环境下用VisualC++2005速成版(可从微软官方网站免费下载)编写的.
我们将使用C++编写游戏程序,并且还要用到WindowsSDL(SimpleDirectMediaLayer)平台下的Win32API.
Win32API就是一组C语言函数,开发人员可以使用这组函数来构建依赖于Windows平台的应用程序.
它把一些如多任务和内存保护一类的复杂操作都抽象出来,并且向用户提供高层概念的接口.
此外,Win32API还提供了对菜单、对话框和多媒体的支持.
我们可以把Windows看成一套非常丰富的API集合.
从播放视频到加载网页,您可以无所不为.
而且,对于每一项任务,都可以有很多种不同的方法去实现它.
目前,有很多大部头的著作都着眼于讲解Windows编程的更基础的概念,我们则只关注本书用得着的知识.
因此,对于诸如创建带树控件的对话框、打印文档、读写注册表中的键值之类的大段知识,我们是不会花时间来学习的.
我们需要处理的只是最简单的情况:创建一个能够用来绘图的窗口,并把输入数据传递给程序,这样我们至少已经开始了和操作系统之间的良性互动.
如果您需要了解更多Windows编程的知识,有很多资源可以供您学习.
我特别推荐的是CharlesPetzold的《Windows程序设计(第5版珍藏版)》.
1.
2匈牙利命名法在Windows领域中对变量进行命名一般都采用所谓的匈牙利命名法.
这个名字源自它的发明者CharlesSimonyi,他是个匈牙利人.
匈牙利命名法(HungarianNotation)是一种编码约定,就是在变量名称前面加上一些前缀字母,用来标识这些变量的类型.
有了匈牙利命名法,我们阅读其他人的代码就变得更加容易,并且更能保证给函数传递的是正确格式的变量.
不过,对于没有接触过匈牙利命名法的人来说,可能还是会感到一些困惑.
表1.
1给出了一些常用前缀,它们在本书的绝大多数代码中会被用到.
表1.
1常用的匈牙利符号前缀常用前缀示例说明bbActiveBOOL类型的变量,它是C++中布尔类型的前身.
其值可以是true或者false.
llPitch长整型变量.
dwdwWidthDWORD类型或者是无符号长整型变量.
wwSizeWORD类型或者是无符号整型变量.
szszWindowClass指向字符串的指针变量,该字符串以0结束(即标准的C风格字符串).
p或lplpData指针变量(lp是由以往的远指针演化来的,也就是长指针,即longpointer).
一个指向指针的指针用pp或者lplp来表示,依此类推.
hhInstanceWindows句柄变量.
1.
3Windows的一般概念记事本程序可以说是Windows简单程序的一个很好的范例.
它允许基本文本输入,能够让您进行基本文字处理,如查找和使用剪贴板,同时通过它您也可以打开、保存和打印一个文件.
该程序如图1.
1所示.
图1.
1记事本——最基本的窗口我要教您创建的窗口和这个窗口很类似.
这样的窗口可被划分成几个不同的区域(如图1.
2所示).
Windows会管理其中的一些区域,剩下部分的就需要您的程序来管理.
图1.
2GUI窗口组件窗口的主要部分如下所示.
标题栏该区域在大多数窗口中都会有.
它给出了窗口的名字并且提供系统按钮,如关闭、最小化或者最大化应用程序.
您可以在创建窗口的过程中通过一些标记来控制标题栏,如可以让它消失、不带系统图标显示或者更窄一些.
菜单栏菜单是GUI程序中最主要的一种交互形式.
它提供了命令列表,用户可以在任何时候执行这些命令.
当然,您也只需要创建菜单并且定义好命令,其他的事情就交给Windows来处理.
大小调整框大小调整框允许用户在屏幕上调整窗口的大小.
如果想把窗口的大小固定,可以在创建窗口的时候关闭相应的选项.
客户区客户区是您所要处理的区域.
Windows实际上在该区域为您提供了一个用于操作的沙箱.
这里就也是您绘制场景的地方.
Windows也可以在这个区域的某些部分进行绘图.
当应用程序中带有滚动条或工具条时,可以说它们占用了客户区的一部分.
1.
4Windows中的消息处理Windows中有个概念,叫焦点(focus).
在同一时刻只能有一个窗口获得焦点.
只有获得焦点的窗口才能与用户进行交互,而其余的窗口都只相当于背景,并且标题栏颜色与当前的窗口不同.
因此,只有一个应用程序可以获知键盘的当前状态.
那么,应用程序是怎么获取这些信息的它如何知道它是否获得了焦点或者是否有用户点击了它应用程序又如何知道它的窗口在屏幕的什么位置当某事件发生时,Windows都会告知应用程序.
当然,如果某个事件发生时,您也可以通知其他窗口(通过这种方式,不同的窗口之间就可以相互通信了).
那么Windows又是如何通知应用程序这些事件的这些内容对于习惯于控制台编程的人们来说可能比较陌生,但对于Windows的工作方式却是极为重要的.
这里的小窍门就是,Windows(以及其他应用程序)通过来回发送被称为消息(Message)的数据包来共享信息.
一个消息其实就是一个结构体,该结构体包含了消息本身和一些载有信息的参数.
Windows消息的结构体如下所示:typedefstructtagMSG{HWNDhwnd;UINTmessage;WPARAMwParam;LPARAMlParam;DWORDtime;POINTpt;}MSG;参数说明如下.
hwnd接收消息的窗口的句柄.
message消息标识符.
例如,当窗口要调整大小的时候,应用程序将收到一个msg对象,此时该成员变量被设置为常数WM_SIZE.
wParam与消息有关的信息,取决于消息的类型.
lParam与消息有关的附加信息.
time指定传递消息的时间.
pt传递消息时的鼠标位置.
消息的处理Windows中最重要的概念之一就是HWND,它基本可理解为一个代表窗口句柄的整数.
您可以把HWND看成唯一标识窗口的条码.
每个窗口都有自己独一无二的HWND.
实际上,当一个Windows应用程序需要通知其他的窗口执行某种操作或者需要访问一个volatile系统对象(如磁盘上的文件)时,Windows既不会改变应用程序的指针,也不会给它机会去侵占其他应用程序的内存空间.
所有的操作都是靠对象的句柄来完成的.
它允许应用程序给对象发送消息,并指导对象如何去做.
注意:由于Win32API比面向对象编程出现得早,因此没有使用像异常处理那样较新的编程概念.
Windows中大多数函数都会返回一个错误码(被称作HRESULT),它将告诉调用函数该函数是否顺利完成操作.
如果返回的HRESULT是非负值,则表明函数调用成功.
如果函数返回一个负数,则表明发生了错误.
如果HRESULT是负数,那么宏FAILED()就会返回true.
函数返回的错误多种多样,E_FAIL(一般性错误)和E_NOTIMPL(函数没有执行)就是两个例子.
每个函数都返回错误码也有令人厌烦的副作用:对于那些有返回消息的调用,需要为其传递一个填充数据的指针(而不仅仅只是返回所需的数据).
从"绘制自身"到"已经失去焦点",再到"用户在(x,y)处执行了双击操作",消息可以告诉窗口任何事情.
每发送一条消息给窗口,它就被添加到Windows内部的消息队列里面.
每个窗口都有与之相关联的本地消息队列.
即使当一个消息到来的时候,应用程序正忙着处理其他消息,消息队列也能保证每个消息都会被处理,这种处理是按照它们被接收的顺序来进行的.
实际上,您会注意到,当大多数Windows应用程序陷入死循环或者停止工作的时候,它们将停止处理消息,因此也不会再进行重绘或对输入做出响应.
那么,应用程序如何处理消息的呢Windows定义了一个所有的程序都必须实现的函数,它叫做窗口程序(windowprocedure,或者简称WndProc).
当创建一个窗口时,我们是以函数指针的形式向Windows传递WndProc()函数的.
随后,在处理消息时,消息以参数的形式传递给WndProc函数,然后在该函数内被处理.
举个例子,当WndProc函数得到一个传递过来的消息,如"绘制自身"的时候,这就是给了窗口一个信号,要求它重绘自身.
因此,您可以在WndProc函数内加入重绘窗口的代码,对窗口重绘进行控制.
当Win32应用程序发送一条消息的时候,Windows先检查消息所提供的窗口句柄,并用它来找出消息发送的目的地.
消息ID用于描述所发送的消息,消息ID的参数包含在消息结构体的两个成员——wParam和lParam中.
在16位操作系统时代,wParam是一个16位(word类型)整数,lParam是一个32位(long类型)整数,但在Win32中,它们都是32位的.
消息会一直在队列中等待,直到应用程序接收它们.
当处理完一个消息后,窗口程序会返回0值.
所有未被处理的消息则被传递给默认的窗口过程DefWindowProc().
对于那些我们没有处理的消息,如果DefWindowProc()也没有接受到,那么Windows的运行会出现异常.
1.
5HelloWorld——Windows风格为了帮助解释上述的概念,我给大家展示一下最简单的Win32程序,并分析它是如何工作的.
先用VisualC++2005速成版自动生成的默认的"HelloWorld"代码,然后加以改动形成下面的代码.
*Advanced3DGameProgrammingwithDirectX10.
0*Title:HelloWorld.
cpp*Desc:Simplewindowsapplication*copyright(c)2007byPeterWalsh,Wordware#include"stdafx.
h"#include"HelloWorld.
h"#defineMAX_LOADSTRING100//GlobalVariables:HINSTANCEhInst;//currentinstanceTCHARszTitle[MAX_LOADSTRING];//ThetitlebartextTCHARszWindowClass[MAX_LOADSTRING];//themainwindowclassname//Forwarddeclarationsoffunctionsincludedinthiscodemodule:ATOMMyRegisterClass(HINSTANCEhInstance);BOOLInitInstance(HINSTANCE,int);LRESULTCALLBACKWndProc(HWND,UINT,WPARAM,LPARAM);INT_PTRCALLBACKAbout(HWND,UINT,WPARAM,LPARAM);intAPIENTRY_tWinMain(HINSTANCEhInstance,HINSTANCEhPrevInstance,LPTSTRlpCmdLine,intnCmdShow){UNREFERENCED_PARAMETER(hPrevInstance);UNREFERENCED_PARAMETER(lpCmdLine);//TODO:Placecodehere.
MSGmsg;HACCELhAccelTable;//InitializeglobalstringsLoadString(hInstance,IDS_APP_TITLE,szTitle,MAX_LOADSTRING);LoadString(hInstance,IDC_HELLOWORLD,szWindowClass,MAX_LOADSTRING);MyRegisterClass(hInstance);//Performapplicationinitialization:if(!
InitInstance(hInstance,nCmdShow)){returnFALSE;}hAccelTable=LoadAccelerators(hInstance,MAKEINTRESOURCE(IDC_HELLOWORLD));//Mainmessageloop:while(GetMessage(&msg,NULL,0,0)){if(!
TranslateAccelerator(msg.
hwnd,hAccelTable,&msg)){TranslateMessage(&msg);DispatchMessage(&msg);}}return(int)msg.
wParam;}////FUNCTION:MyRegisterClass()////PURPOSE:Registersthewindowclass.
////COMMENTS:////Thisfunctionanditsusageareonlynecessaryifyouwantthiscode//tobecompatiblewithWin32systemspriortothe'RegisterClassEx'//functionthatwasaddedtoWindows95.
Itisimportanttocallthis//functionsothattheapplicationwillget'wellformed'smallicons//associatedwithit.
//ATOMMyRegisterClass(HINSTANCEhInstance){WNDCLASSEXwcex;wcex.
cbSize=sizeof(WNDCLASSEX);wcex.
style=CS_HREDRAW|CS_VREDRAW;wcex.
lpfnWndProc=WndProc;wcex.
cbClsExtra=0;wcex.
cbWndExtra=0;wcex.
hInstance=hInstance;wcex.
hIcon=LoadIcon(hInstance,MAKEINTRESOURCE(IDI_HELLOWORLD));wcex.
hCursor=LoadCursor(NULL,IDC_ARROW);wcex.
hbrBackground=(HBRUSH)(COLOR_WINDOW+1);wcex.
lpszMenuName=MAKEINTRESOURCE(IDC_HELLOWORLD);wcex.
lpszClassName=szWindowClass;wcex.
hIconSm=LoadIcon(wcex.
hInstance,MAKEINTRESOURCE(IDI_SMALL));returnRegisterClassEx(&wcex);}////FUNCTION:InitInstance(HINSTANCE,int)////PURPOSE:Savesinstancehandleandcreatesmainwindow////COMMENTS:////Inthisfunction,wesavetheinstancehandleinaglobalvariableandcreateand//displaythemainprogramwindow.
//BOOLInitInstance(HINSTANCEhInstance,intnCmdShow){HWNDhWnd;hInst=hInstance;//StoreinstancehandleinourglobalvariablehWnd=CreateWindow(szWindowClass,szTitle,WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,0,CW_USEDEFAULT,0,NULL,NULL,hInstance,NULL);if(!
hWnd){returnFALSE;}ShowWindow(hWnd,nCmdShow);UpdateWindow(hWnd);returnTRUE;}////FUNCTION:WndProc(HWND,UINT,WPARAM,LPARAM)////PURPOSE:Processesmessagesforthemainwindow.
////WM_COMMAND-processtheapplicationmenu//WM_PAINT-paintthemainwindow//WM_DESTROY-postaquitmessageandreturn////LRESULTCALLBACKWndProc(HWNDhWnd,UINTmessage,WPARAMwParam,LPARAMlParam){intwmId,wmEvent;PAINTSTRUCTps;HDChdc;TCHARstrOutput[]=L"HelloWorld!
";switch(message){caseWM_COMMAND:wmId=LOWORD(wParam);wmEvent=HIWORD(wParam);//Parsethemenuselections:switch(wmId){caseIDM_ABOUT:DialogBox(hInst,MAKEINTRESOURCE(IDD_ABOUTBOX),hWnd,About);break;caseIDM_EXIT:DestroyWindow(hWnd);break;default:returnDefWindowProc(hWnd,message,wParam,lParam);}break;caseWM_PAINT:hdc=BeginPaint(hWnd,&ps);RECTrt;GetClientRect(hWnd,&rt);DrawText(hdc,strOutput,(int)wcslen(strOutput),&rt,DT_CENTER|DT_VCENTER|DT_SINGLELINE);EndPaint(hWnd,&ps);break;caseWM_DESTROY:PostQuitMessage(0);break;default:returnDefWindowProc(hWnd,message,wParam,lParam);}return0;}//Messagehandlerforaboutbox.
INT_PTRCALLBACKAbout(HWNDhDlg,UINTmessage,WPARAMwParam,LPARAMlParam){UNREFERENCED_PARAMETER(lParam);switch(message){caseWM_INITDIALOG:return(INT_PTR)TRUE;caseWM_COMMAND:if(LOWORD(wParam)==IDOK||LOWORD(wParam)==IDCANCEL){EndDialog(hDlg,LOWORD(wParam));return(INT_PTR)TRUE;}break;}return(INT_PTR)FALSE;}以上是我们所能写的最简单的Windows程序,而其代码长度都已经超过了200行,大家很可能因此担心.
值得庆幸的是,在所有的Windows程序中,上述代码都或多或少是相同的.
大多数Windows程序员都不记得代码中每段代码的准确顺序,他们只是从以前的应用程序中复制那些可用的Windows初始化代码.
代码诠释每个C/C++程序都以main()函数为其入口点,并且通过它获得来自操作系统的控制信息.
但是在Windows中,情况有所不同.
在运行代码之前,Win32API还会先运行一些其他的代码.
实际的main()函数则是被深深地隐藏在Win32的动态链接库(DLL)中.
然而,这个应用程序是从另外一个入口,即一个称为WinMain()的函数开始执行程序.
当您的应用程序开始运行时,Windows先要进行配置工作,然后就调用WinMain()函数.
这就是为什么当您调试一个Windows应用程序的时候,会发现"WinMain"没有在调用堆栈底部的原因;而调用WinMain()函数的DLL函数则会在调用堆栈的底部.
传递给WinMain()的参数如下(按顺序列出).
应用程序实例(另外一个句柄,代表一个运行的实例).
每个进程都有不同的实例句柄,用来唯一标识Windows的进程.
这与窗口句柄是不同的,每个应用程序可能控制多个窗口.
而您却需要牢牢抓住这个实例,因为WindowsAPI函数需要知道是哪个实例在调用它.
我们可以把实例看成可执行的程序在内存中的一个副本或映像.
每个可执行程序都有一个句柄,这样Windows才能够区分、管理或对它们进行其他操作.
HINSTANCE类型,当前运行的应用程序的另一个副本.
在以往机器的内存很小,Windows中的一个程序可能有多个实例来共享一段内存空间.
现在每个进程都有自己独立的内存空间,那么这个参数则总是设置为NULL.
该参数之所以还保留下来,是为了让以往的Windows应用程序能够继续工作.
指向命令行字符串的指针.
当用户把一个文件拖到资源管理器中的一个可执行文件上时(而非该程序的一个运行副本),Windows便会运行该程序,并把被拖动文件的路径和文件名作为命令行的第一个参数.
一组标志,用于描述窗口应如何被初始化(如全屏、最小化等).
从概念上来说,WinMain函数的工作流程如下:WinMain注册Windows的应用程序类创建主窗口while(没有通知退出)处理Windows发送的所有消息MyRegisterClass()接收应用程序的实例,并向Windows通知该应用程序(其实就是注册).
InitInstance()用于创建屏幕上的主窗口,并开始绘制.
然后,代码将进入while循环,直到程序退出.
GetMessage()函数用来从消息队列中取得消息.
该函数总是返回1,除非它在队列中遇到如下这个特定消息:"现在退出"消息,其消息ID是WM_QUIT.
只要消息队列中还有消息存在,GetMessage()就会从队列中移出消息,并用它来填充消息结构体,即前面提到的msg变量.
在while循环内部,您首先需要做的是获取消息,并使用TranslateMessage()函数翻译该消息.
这是一个非常便捷的函数.
当接收到某个按键被按下或者释放的消息时,您将得到该按键对应的虚拟键码.
这些虚拟键码的实际ID值可以是任意的,但是其命名空间是我们所关心的:当字母a被按下的时候,其中相应的消息参数就等价于#defineVK_A.
当您想要做一些像处理文本输入一类的事情时,上述的命名法处理起来就会使人非常痛苦.
TranslateMessage()就可以给您帮忙了,它可以把参数从VK_A转化为(char)'a',这就简化了对常规文本输入的处理.
对于那些没有等价ASCII码的键,如PageUP和LeftArrow,在TranslateMessage()函数中将继续保留它们的虚拟键码(分别为VK_PRIOR和VK_LEFT).
其他所有的消息经过TranslateMessage()处理后,都不会有任何改变.
第二个函数DispatchMessage(),是一个实际处理消息的函数.
在内部,它会查看哪个函数(在MyRegisterClass中)被注册用于处理消息,然后再把消息传递给那个函数.
您将注意到,我们的程序从来没有真正调用过窗口过程.
这是因为当您把它指定给DispatchMessage()函数时,Windows已经为您调用了.
我们可以把While循环看成是所有Windows程序的中枢神经系统,它不断地从消息队列里取出消息并且以最快的速度进行处理.
由于它如此重要和常见,因此它还具有一个特殊的名字:消息泵(messagepump).
1.
注册应用程序MyRegisterClass()函数会填充一个包含了一些Windows需要获取的关于应用程序的信息的结构,并将其传递给Win32API,此后我们才能创建窗口.
在MyRegisterClass()函数里,您可以告知Windows您想为程序任务栏上的图标设置多大的尺寸(hIcon表示大尺寸,hIconSm表示小尺寸).
如果想要在应用程序中使用一个菜单栏,也可以在这里给菜单栏命名(眼下没有,把它置为0).
您需要告诉Windows这个应用程序的实例是什么(WinMain中接收的参数之一),即hInstance参数.
您也要告诉它处理消息的时候需要调用什么函数,即lpfnWndProc参数.
窗口类也需要一个名称,即lpszClassName参数,在后面的CreateWindow()函数中,需要用它引用该类.
警告:窗口类与C++中的类是两个完全不同的概念.
Windows早在C++语言普及前就已经有了.
因此,一些命名上可能会有冲突.
2.
窗口的初始化InitInstance()用于创建窗口并开始绘制过程.
调用CreateWindow函数可以创建窗口,其函数原型如下:HWNDCreateWindow(LPCTSTRlpClassName,LPCTSTRlpWindowName,DWORDdwStyle,intx,inty,intnWidth,intnHeight,HWNDhWndParent,HMENUhMenu,HANDLEhInstance,LPVOIDlpParam);参数说明如下所示.
lpClassName字符串,用于表示使用RegisterClass()注册的窗口类的类名.
它定义了窗口的基本形式,以及用哪个WndProc()来对消息进行处理(您可以为每个应用程序创建多个窗口类).
lpWindowName窗口的标题.
它会在窗口的标题栏和任务栏中显示.
dwStyle一组描述窗口风格的标志(如细边框,不能调整大小,等等).
对于这里的讨论,窗口化的应用程序都使用WS_OVERLAPPEDWINDOW窗体风格(这是标准外观的窗口,可以调整边框大小,有系统菜单和标题栏,等等).
此外,全屏的应用程序使用WS_POPUP风格(它无Windows的特征,甚至没有边框,而仅仅只有客户区).
x,y相对于显示器左上角(向右x递增,向下y递增)的x,y坐标的位置,该位置用来表示放置窗口的位置.
nWidth,nHeight窗口的宽和高.
hWndParent一个可以有子窗口的窗口(想象一个像PaintShopPro那样的绘图程序,每个图像文件都拥有自己的窗口).
如果正在创建一个子窗口,那么该参数就是父窗口的HWND.
hMenu如果一个应用程序有菜单(我们现在还没有),那么就在这里把菜单的句柄传递给它.
hInstance由WinMain捕获的应用程序实例.
lpParam一个指向创建窗口所需的额外数据的指针,这些数据可能在更高级的场合中派上用场(现在,我们只需要把它置成NULL).
您传递给该函数的窗口宽度和高度是整个窗口的宽度和高度,而不只是客户区的宽度和高度.
如果您希望将客户区指定为某一尺寸,如640*480,就需要对标题栏和窗口边框的宽度和高度(单位为像素)进行调整.
可以使用AdjustWindowRect()函数来解决该问题(在本章的后面会讨论它).
您为AdjustWindowRect()函数传入一个表示预期客户区矩形的矩形结构.
此函数便会根据您传递给它的窗口风格(一般传递给CreateWindow的风格都一样)来调整窗口的尺寸.
由于使用WS_POPUP创建的窗口没有额外的Windows用户界面特征,因此窗口将不会变化.
而使用WS_OVERLAPPEDWINDOW创建的窗口就不得不加上两边的大小调整框和顶部的标题栏所占的空间.
如果CreateWindow()函数调用失败(如果有太多的窗口或者接收到错误的输入参数,就可能会发生这种情况,例如它的hInstance与MyRegister()函数所提供的hInstance都不一样),您就不能处理任何窗口消息(因为根本就没有窗口了!
),因此会返回false.
在程序进入消息泵阶段之前,这些情况会由WinMain()函数来处理,处理的结果就是退出应用程序.
通常,在退出程序之前,我们应该弹出一个窗口来通知用户发生了错误.
最好不要什么都没有就结束程序.
如果CreateWindow()函数调用成功,就需要调用ShowWindow()函数来设置刚被创建的窗口的显示状态(显示状态被当作WinMain()函数的最后一个参数来传递),然后就调用UpdateWindow()来向窗口发送绘制消息,于是,窗口自己就可以进行绘制了.
警告:CreateWindow()函数在其结束之前需要多次调用WndProc()函数!
这可能会给某些Windows程序的正常运行带来困难.
在函数返回并得到窗口句柄之前,WM_CREATE、WM_MOVE、WM_SIZE和WM_PAINT(以及其他)都要通过WndProc()发送给程序.
如果您要使用任何需要某个程序的HWND的组件去执行工作(DirectX窗口就是一个不错的例子,无论何时只要得到了WM_SIZE消息,窗口的大小都要重新调整),那么这就需要您很小心,以免在它被初始化之前就试图调整它的大小.
一种解决上述问题的方法就是在WM_CREATE内记录我们窗口的HWND,之所以可以这样,是因为传递给WndProc()函数的参数之一就是我们用来接收消息的窗口句柄.
当出现某些事件时(如产生了错误),您可能还不知道该如何通知用户.
在此类情况下,我们需要建立一个对话框来向用户展示信息,如展示程序为何失败的信息.
对于游戏编程来说,创建一个有按钮、文本框和其他各式组件的复合对话框,往往是没必要的(通常都是您在游戏中创建自己的界面).
然而,我们可以利用那些Windows自动生成的基本对话框,如当您试图退出某文档编辑软件时候,就弹出一个对话框,提示您"在退出之前是否保存xx文档",并且还有两个按钮,即"是"和"否".
MessageBox()就是我们要用来自动生成对话框的函数.
它是Windows函数中应用得最广泛,也是最有用的函数.
我们来看看它的函数原型:intMessageBox(HWNDhWnd,LPCTSTRlpText,LPCTSTRlpCaption,UINTuType);参数说明如下.
hWnd窗口所有者的句柄(一般是应用程序的窗口句柄).
lpText消息对话框里面的文字.
lpCaption消息对话框的标题.
uType一组用来描述消息对话框行为的标志.
对它们的详细描述参见表1.
2.
除非关闭弹出的对话框,否则MessageBox()函数会一直将它显示在桌面上.
表1.
2一些在MessageBox()函数里常用的标志标志名称说明MB_OK消息对话框只有一个"确定"按钮.
这是默认值.
MB_ABORTRETRYIGNORE显示三个按钮——终止、重试和忽略.
MB_OKCANCEL显示两个按钮——确定和取消.
MB_RETRYCANCEL显示两个按钮——重试和取消.
续表标志名称说明MB_YESNO显示两个按钮——是和否.
MB_YESNOCANCEL显示三个按钮——是、否和取消.
MB_ICONEXCLAMATION,MB_ICONWARNING显示一个感叹号的图标.
MB_ICONINFORMATION,MB_ICONASTERISK显示一个信息图标(一个圆圈内有个小写字母i).
MB_ICONQUESTION显示一个问号.
MB_ICONSTOP,MB_ICONERROR,MB_ICONHAND显示一个停止标记的图标.
选择哪个键直接决定了MessageBox()函数的返回值.
表1.
3给出了一些可能的返回值.
注意,该函数并不返回HRESULT类型的值,这在Windows函数中还是比较少见的.
表1.
3MessageBox()函数的返回值返回值说明IDABORT选择的是"终止"键.
IDCANCEL选择的是"取消"键.
IDIGNORE选择的是"忽略"键.
IDNO选择的是"否"键.
IDOK选择的是"确定"键.
IDRETRY选择的是"重试"键.
IDYES选择的是"是"键.
3.
WndProc——消息泵WndProc()是窗口过程,这也是Windows应用程序中所有事情发生的地方.
由于我们的这个应用程序非常简单,它只能处理两个消息(更加复杂的Windows程序需要处理相当多的消息).
大多数Win32的应用程序都会处理的两个消息是WM_PAINT(当窗口要重新绘图时,发送此消息)和WM_DESTROY(当窗口要被销毁时,发送此消息).
有一个很重要的问题需要特别注意,那就是在switch语句中没有被显式处理的消息都将被送到DefWindowProc()函数中,该函数将会对每个Windows消息以默认方式进行处理.
为了让应用程序能够正确运行,任何没有被处理的消息都必须被送往DefWindowProc()函数进行处理.
系统消息都是由Windows内部发出的,例如在创建和销毁时窗口所接收到的消息.
如果您想发送消息到自己的应用程序(或其他应用程序),就需要用到如下两个函数:PostMessage()和SendMessage().
PostMessage()将消息加到应用程序的消息队列中,这样就可以在消息泵中处理了.
SendMessage()则是以消息作为参数来调用WndPro().
进行Windows编程的时候,有一点很重要,那就是并不需要把上述的全部知识都记下来.
几乎不会有人记得住每一个Windows函数及其全部参数;人们在编程的时候往往都会在MSDN上查找,或是复制已有的代码,或是用项目向导来自动生成代码.
因此,不要因为记不住这些新知识就气馁了.
我做过的最有用的投资之一就是买了第二台显示器.
这样,我就可以在主屏幕上进行编程,而在另外一个显示器上查看MSDN,这就意味着我不需要老是在应用程序之间来回切换.
您可能已经注意到即使是一个简单的"Hello,World!
"程序也需要很多的代码.
其中大部分的代码都是任何Windows程序所需要的.
所有的应用程序都需要自我注册,如果还需要一个窗口,它们就需要自己创建一个窗口,此时,它们也都需要窗口程序.
虽然程序有些长,但是它确实实现了很多功能.
例如,可以改变窗口大小,可以在屏幕内拖动窗口,可以让别的窗口遮挡它,可以最小化、最大化,等等.
Windows用户把这些都看成是理所当然的,但是这些功能背后确实需要很多代码.
1.
6对窗口几何参数的操作到目前为止,我们对Windows应用程序的使用还是严格受限的,所以我们只需要关心在几何函数中要用到的两个基本的Windows结构体:POINT和RECT.
在Windows中,有两个坐标系.
一个是客户区坐标系,它的原点(0,0)在窗口中客户区的左上角.
当窗口在屏幕上移动时,相对于客户区的坐标是不会发生变化的.
另一个坐标系就是桌面坐标系.
这是一个绝对坐标系,其原点在屏幕的左上角,因此该坐标系也被称作屏幕坐标系.
Windows用结构体POINT来表示二维坐标.
它包括两个长整型变量,一个用于表示水平分量,另一个用于表示垂直分量.
typedefstructtagPOINT{LONGx;LONGy;}POINT;既然所有的窗口都是矩形,Windows设置了一个表示矩形的结构体.
我们可以发现该结构的本质就是两个端点,一点描述了矩形区域的左上角,另一点描述了矩形区域的右下角.
typedefstruct_RECT{LONGleft;LONGtop;LONGright;LONGbottom;}RECT;参数描述如下.
left窗口的左边.
top窗口的顶部.
right窗口的右边(宽度就是right-left).
bottom窗口的底边(高度就是bottom-top).
我们可以使用GetClientRect()函数来获得一个窗口的客户区矩形.
左上角总是0,右边和底部的数值就是窗口的宽度和高度.
BOOLGetClientRect(HWNDhWnd,LPRECTlpRect);参数描述如下.
hwnd窗口的句柄,我们想要知道的就是该窗口的信息.
lpRect指向RECT结构体的指针.
使用客户区矩形来给该RECT结构体赋值.
当我们知道了客户区矩形的大小时,可能还希望知道客户区的这些点和桌面坐标系的相对位置.
ClientToScreen()函数为我们提供了该功能,其函数原型如下:BOOLClientToScreen(HWNDhWnd,LPPOINTlpPoint);参数描述如下.
hwnd用户点所在窗口的句柄.
lpPoint指向用户点的指针,这个点的坐标将被转换到屏幕坐标系中.
为了将您从GetClientRect()函数产生的矩形坐标转换为屏幕坐标系下的矩形坐标,只需要在bottom和right这两个矩形成员变量上使用ClientToScreen()函数.
虽然该方法有些笨拙,但是确实有效.
当我们在决定窗口大小时,有件事可能会把窗口结构搞得一团糟.
例如需要大小为800*600的客户区,但是当我们用CreateWindow()创建窗口时,给出的是整个窗口的尺寸,这里面包括了大小调整框、标题栏和菜单栏.
值得庆幸的是,AdjustWindowRect()可以帮助我们把客户区矩形的大小转化为窗口的尺寸大小.
它根据CreateWindow()中设置的窗口风格dwStyle,把所有的坐标都向外延展.
如果不是弹出式风格的窗口,窗口的左边和顶部坐标经过调整后可能为负数.
BOOLAdjustWindowRect(LPRECTlpRect,DWORDdwStyle,BOOLbMenu);参数说明如下.
lpRect指针,指向要调整的RECT结构体.
dwStyle窗口的风格,它表明每个坐标需要调整多少.
如果窗口的风格为WS_POPUP,就不用调整.
bMenu布尔型变量,标识窗口是否有菜单.
如果像我们的程序那样没有菜单,该参数就设定为false.
Windows有一个完善的图形库,用来在图形设备的句柄上执行操作.
这个库叫做GDI,即图形设备接口(GraphicalDeviceInterface).
它允许用户绘制直线、椭圆、位图、文本(在后面的章节中,我们将看到它绘制文本的能力)和其他一些对象等.
一个简单的示例就是用它在屏幕上画出文本"Hello,World".
在本书后面的章节中,我会给大家介绍更多的GUI函数.
1.
7重要的窗口消息本书中大多数的代码都以Windows作为一个出发点(一种在屏幕上建立窗口,以供您绘图的方式).
为了防止冗长的消息类别列表给您带来困扰,我特意从中精选了一些常见的窗口消息进行介绍.
表1.
4描述了一些重要的消息以及它们的参数.
表1.
4一些重要的窗口消息名称说明WM_CREATE在Windows完全创建了窗口之后,在开始绘图之前,发送此消息给应用程序.
这是应用程序第一次可以看到它的窗口HWND值.
WM_PAINT当Windows想要窗口开始绘图时,发送消息给应用程序.
参数:(HDC)wParam可以用于绘图的设备环境句柄.
WM_ERASEBKGND当我们想要擦除用户窗口的背景时,传递此消息.
如果没有把这个消息传递给DefWindowProc(),而是对该消息做了处理,那么Windows会允许您擦除窗口背景(稍后,我们会看到这样做的好处).
参数:(HDC)wParam用于画图的设备环境句柄.
续表名称说明WM_DESTROY当窗口被销毁时,传递此参数.
WM_CLOSE当窗口被要求关闭的时候,传递此消息.
在这里,您可以在关闭窗口之前,要求用户确认.
WM_SIZE调整窗口大小的时候,传递此消息.
若仅仅是变化窗口尺寸,窗口左上角的坐标是不会改变的.
(所以当您想从左上角来改变窗口的大小时,需要同时传递WM_MOVE和WM_SIZE两个消息.
)参数:wParamWM_SIZE大小调整的标识.
当窗口被最小化时,该值为SIZE_MINIMIZED.
当然还有其他很多值.
LOWORD(lParam)客户区新的宽度(不是整个窗口的).
HIWORD(lParam)客户区新的高度(不是整个窗口的).
WM_MOVE当窗口被移动的时候,传递此消息.
参数:(int)(short)LOWORD(lParam)客户区左上角新的x坐标.
(int)(short)HIWORD(lParam)客户区左上角新的y坐标.
WM_QUIT它是应用程序可以得到的最后一个消息,当得到这个消息后,应用程序就结束了.
您不用处理这个消息,实际上它也不会经过WndProc().
不过,它将在WinMain()的消息泵中被处理,其结果就是程序跳出循环,随后结束.
WM_KEYDOWN一有键按下,就传递此消息.
持续按住键不放,就会连续传递此消息.
(int)wParam这是该按键的虚拟键码.
在处理它之前,如果调用了TranslateMessage(),并且它有等价的ASCII码(字母、数字、标点符号),那么它就会转换成该键实际的ASCII字符.
WM_KEYUP当按键被放开时,传递此消息.
参数:(int)wParam这是该按键的虚拟键码.
续表名称说明WM_MOUSEMOVE它是一个最经常需要被接收的消息.
鼠标每次在窗口的客户区移动的时候,应用程序就得到鼠标在客户区中的新位置.
参数:LOWORD(lParam)鼠标在客户区中的x坐标.
HIWORD(lParam)鼠标在客户区中的y坐标.
wParam键标志.
它可以帮助您了解键盘的状态,看看是否按下了什么特殊键(如Alt键和鼠标左键的组合).
键标志有如下的一些值.
MK_CONTROL:表明按下了Ctrl键.
MK_LBUTTON:表明按下了鼠标左键.
MK_MBUTTON:表明按下了鼠标中键.
MK_RBUTTON:表明按下了鼠标右键.
MK_SHIFT:表明按下了Shift键.
WM_LBUTTONDOWN当在客户区按下鼠标左键的时候,传递此消息.
与键盘的按键不同,按住鼠标左键不放只会传递一个此消息.
参数:LOWORD(lParam)鼠标在客户区中的x坐标.
HIWORD(lParam)鼠标在客户区中的y坐标.
wParam键标志.
它可以帮助您了解键盘的状态,看看是否按下了什么特殊键(如Alt键和鼠标左键的组合).
键标志有如下的一些值.
MK_CONTROL:表明按下了Ctrl键.
MK_MBUTTON:表明按下了鼠标中键.
MK_RBUTTON:表明按下了鼠标右键.
MK_SHIFT:表明按下了Shift键.
续表名称说明WM_MBUTTONDOWN当在客户区按下鼠标中键的时候,传递此消息.
与键盘的按键不同,按住鼠标中键不放只会传递一个此消息.
参数:LOWORD(lParam)鼠标在客户区中的x坐标.
HIWORD(lParam)鼠标在客户区中的y坐标.
wParam键标志.
它可以帮助您了解键盘的状态,看看是否按下了什么特殊键(如Alt键和鼠标左键的组合).
键标志有如下的一些值.
MK_CONTROL:表明按下了Ctrl键.
MK_LBUTTON:表明按下了鼠标左键.
MK_RBUTTON:表明按下了鼠标右键.
MK_SHIFT:表明按下了Shift键.
WM_RBUTTONDOWN当在客户区按下鼠标右键的时候,传递此消息.
与键盘的按键不同,按住鼠标右键不放只会传递一个此消息.
参数:LOWORD(lParam)鼠标在客户区中的x坐标.
HIWORD(lParam)鼠标在客户区中的y坐标.
wParam键标志.
它可以帮助您了解键盘的状态,看看是否按下了什么特殊键(如Alt键和鼠标左键的组合).
键标志有如下的一些值.
MK_CONTROL:表明按下了Ctrl键.
MK_LBUTTON:表明按下了鼠标左键.
MK_MBUTTON:表明按下了鼠标中键.
MK_SHIFT:表明按下了Shift键.
WM_LBUTTONUP在客户区放开鼠标左键的时候,传递此消息.
参数:与WM_LBUTTONDOWN的参数相同.
续表名称说明WM_LBUTTONUP在客户区放开鼠标中键的时候,传递此消息.
参数:与WM_MBUTTONDOWN的参数相同.
WM_RBUTTONUP在客户区放开鼠标右键的时候,传递此消息.
参数:与WM_RBUTTONDOWN的参数相同.
WM_MOUSEWHEEL很多新式鼠标都有Z轴控制,一般表现为滚轮.
它既可以向前、向后滚动,也可以按下.
如果它被按下,一般都是发送按下鼠标中键的消息.
但是,如果它是向前或者向后滚动,那么就传递以下参数.
参数:(short)HIWORD(wParam)从上一个消息以来,滚轮转动的圈数.
正数表示滚轮向前转动(远离用户),负数表示滚轮向后转动(朝用户的方向).
(short)LOWORD(lParam)鼠标在客户区中的x坐标.
(short)HIWORD(lParam)鼠标在客户区中的y坐标.
LOWORD(wParam)键标志.
它可以帮助您了解键盘的状态,看看是否按下了什么特殊键(如Alt键和鼠标左键的组合).
键标志有如下的一些值.
MK_CONTROL:表明按下了Ctrl键.
MK_LBUTTON:表明按下了鼠标左键.
MK_MBUTTON:表明按下了鼠标中键.
MK_RBUTTON:表明按下了鼠标右键.
MK_SHIFT:表明按下了Shift键.
1.
8类的封装现在您已经学会如何创建窗口,下面将教大家设计那些隐藏在Direct3D和其他游戏代码之中的框架.
正是这个框架降低了本书中所有其他应用程序的编程复杂度.
首先,让我们先看看封装能够带来哪些好处.
如果应用程序能够实现以下这些,那将非常好(排列不分先后).
能够控制和重新实现应用程序对象的构造和析构.
能够自动创建标准的系统对象(对现在来说就是应用程序窗口,以后可能是Direct3D、DirectInput等),并且还要有创建用户对象的能力.
能够加入一些可以监听窗口消息流的对象,并能添加自定义的方式去处理那些传递给应用程序的消息.
一个简单的主循环,除非程序结束,否则它会一直运行.
要完成上述几点,需要定义两个类.
其中一个类是cWindows,它把Windows代码中需要运行的部分抽象出来.
它会被另一个被命名为cApplication的类调用.
cApplication类负责应用程序的实际运行.
我们所创建的每一个应用程序(除了一些例外)都将是cApplication类的子类.
在程序运行过程中,可能会发生导致程序崩溃的错误,所以我们设计的基本框架需要有错误传递的机制.
整个应用程序是围绕着一个try/catch模块来运行的,因此当WinMain()捕获到任何异常时,应用程序就会被关闭.
在异常的传递中包含着一个描述异常信息的文本消息,在程序退出之前,该文本信息会以消息框的形式弹出来.
如果每个函数都返回一个错误代码,并且检查它调用的函数所返回的错误码,这将会很复杂.
而我们所选择的上述方法则简单得多.
由于异常发生得很少,那么因错误代码而增加的复杂度就显得没有必要了.
通过使用异常处理,代码就会变得漂亮、整洁很多.
我们把这种抛出的错误命名为cGameError.
在本书中,几乎所有代码抛出的错误都属此类.
classcGameError{stringm_errorText;public:cGameError(char*errorText){DP1("***\n***[ERROR]cGameErrorthrown!
text:[%s]\n***\n",errorText);m_errorText=string(errorText);}constchar*GetText(){returnm_errorText.
c_str();}};enumeResult{resAllGood=0,//functionpassedwithflyingcolorsresFalse=1,//functionworkedandreturns'false'resFailed=–1,//functionfailedmiserablyresNotImpl=–2,functionhasnotbeenimplementedresForceDWord=0x7FFFFFFF};抽象窗口类cWindow的实现其实非常简单.
只需将MyRegisterClass()用cWindow::RegisterClass()来代替,将MyInitInstance()变成cWindow::InitInstance(),然后将WndProc()修改成静态函数cWindow::WndProc().
函数之所以要是静态的,是因为非静态类函数都包含一个隐含变量(即this指针),这就与WndProc()函数的定义不兼容了.
消息泵被封装为两个函数.
HasMessages()函数用来检查队列并查看是否还有待处理的消息,如果有,则返回true.
Pump()函数则用来处理单个消息,并使用TranslateMessage()/DispatchMessage()函数将消息发送给WndProc().
当Pump()收到Windows要求程序退出的WM_QUIT消息时,它就会返回resFalse.
在处理窗口程序抛出的异常时,我们需要格外注意.
因为,在执行DispatchMessage()函数和WndProc()函数之间,调用堆栈需要调用到DLL的核心函数.
如果抛出的异常影响了它们,就会发生一些糟糕的事(轻则程序崩溃,重则机器崩溃).
为了防止这种情况的发生,WndProc()无论捕获到什么异常,都需要先把它保存在一个临时变量中.
当Pump()函数完成一个消息的抽取后,它就检查这个临时变量,看看是否有异常被抛出.
如果有待处理的异常,Pump()将重新将其抛出,而该错误就会由WinMain()接收.
classcWindow{protected:intm_width,m_height;HWNDm_hWnd;std::stringm_name;boolm_bActive;staticcWindow*m_pGlobalWindow;public:cWindow(intwidth,intheight,constchar*name="Defaultwindowname");~cWindow();virtualLRESULTWndProc(HWNDhWnd,UINTuMsg,WPARAMwParam,LPARAMlParam);virtualvoidRegisterClass(WNDCLASSEX*pWc=NULL);virtualvoidInitInstance();HWNDGetHWnd();boolIsActive();boolHasMessages();eResultPump();staticcWindow*GetMainWindow();};inlinecWindow*MainWindow();参数说明如下.
m_width,m_height窗口的客户区的宽度和高度.
不同于窗口的实际宽度和高度.
m_hWnd窗口的句柄.
使用GetHWnd函数可以在类之外访问它.
m_name窗口名称,用来构造窗口类和窗口.
m_bActive布尔类型;如果窗口是活动的(如果当前的窗口是在最前面,则认为是活动的),该值为true.
m_pGlobalWindow静态变量,指向应用程序中cWindow类的一个实例.
初始值设置为NULL.
cWindow()窗口对象的构造函数.
只能创建这个对象的一个实例;通过设置m_pGlobalWindow来验证这一点.
~cWindow()析构函数,用来销毁窗口;并将全局的窗口变量设置为NULL,以防它们继续被使用.
WndProc()该类的窗口函数.
它被隐藏在Window.
cpp中的一个函数调用.
RegisterClass()用来注册窗口类的虚函数.
此函数可以在子类中被重载,并加入新的功能,如添加一个菜单或者不同的WndProc().
InitInstance()用来创建窗口类的虚函数.
此函数可以在子类中被重载,并加入新的功能,如改变窗口风格.
GetHWnd()返回本窗口的窗口句柄.
IsActive()如果应用程序是激活状态,并且在前景,那么它的返回值就为true.
HasMessages()如果在窗口的消息队列里面有待处理的消息,则返回true;并使用参数为PM_NOREMOVE的PeekMessage()函数.
Pump()从消息队列里抽取第一个信息,然后把它发送到WndProc()里.
当从消息队列中得到的消息是WM_QUIT时,该函数返回resFalse,其余情况则返回的是resAllGood.
GetMainWindow()Public函数;全局函数MainWindow()通过它来获得窗口对象的入口.
MainWindow()全局函数,返回程序中cWindow类的单个实例.
任何代码段都可以使用这个函数来询问窗口的信息.
例如,在代码中任何地方,调用MainWindow()->GetHWnd()都可以得到窗口的hWnd.
最后,让我们来学习cApplication类.
其派生类一般只需要重新实现SceneInit()和DoFrame()就行了.
然而,如果需要增加一些功能,如构造另外的系统对象,就需要重新实现相应函数.
在本书最后一章介绍的游戏,因为需要使用一些其他系统对象,所以要构造这些对象.
classcApplication{protected:stringm_title;intm_width;intm_height;boolm_bActive;staticcApplication*m_pGlobalApp;virtualvoidInitPrimaryWindow();virtualvoidInitGraphics();virtualvoidInitInput();virtualvoidInitSound();virtualvoidInitExtraSubsystems();public:cApplication();virtual~cApplication();virtualvoidInit();virtualvoidRun();virtualvoidDoFrame(floattimeDelta);virtualvoidDoIdleFrame(floattimeDelta);virtualvoidParseCmdLine(char*cmdLine);virtualvoidSceneInit();virtualvoidSceneEnd();voidPause();voidUnPause();staticcApplication*GetApplication();staticvoidKillApplication();};inlinecApplication*Application();HINSTANCEAppInstance();cApplication*CreateApplication();参数说明如下.
m_title应用程序的标题.
在构造应用程序后,将它发送给cWindow.
m_width,m_height窗口的客户区的宽度和高度.
m_bActive如果应用程序是激活的并且正在运行,则返回true.
当应用程序处于非活动状态时,用户输入就不会被接收,同时空闲处理函数被调用.
m_pGlobalApp静态指针,指向应用程序的单个全局实例.
InitPrimaryWindow()虚函数.
用来初始化应用程序的主窗口.
如果bExclusive的值是true,应用程序将创建一个全屏模式的弹出式窗口;如果是false,则创建一个常规窗口.
InitGraphics()此函数在后面的章节讨论.
InitInput()此函数在后面的章节讨论.
InitSound()此函数在后面的章节讨论.
InitExtraSubsystems()虚函数.
在初始化场景之前,用它来初始化一些应用程序所需要的额外的子系统.
cApplication()构造函数,它把成员变量赋为默认值.
~cApplication()析构函数,关闭所有的系统对象.
Init()初始化所有的系统对象(将在第3章中讲解).
Run()应用程序的主体部分.
按照最快的速度来显示画面,直到WM_QUIT消息到来.
DoFrame()显示每一帧画面时,Run()函数都会调用它.
在该函数里,子程序将执行所有的游戏逻辑,并且绘制画面.
timeDelta是浮点型数据,它表示与上一帧的时间间隔.
timeDelta使应用程序能够以一个恒定的速度来播放动画,而不受机器的帧速影响.
DoIdleFrame()当应用程序处于非活动状态时,Run()函数会调用它.
我向大家演示的绝大多数程序都不需要该函数.
但是为了完整性,还是把它放置在程序中.
ParseCmdLine()虚函数,它允许子类在运行之前就查看命令行.
SceneInit()虚函数;重载它用来初始化特殊场景.
在创建系统对象后,才会调用它.
SceneEnd()虚函数;重载它用来执行关闭特殊场景的代码.
Pause()暂停应用程序.
UnPause()继续运行应用程序.
GetApplication()public类型的存取函数,用来获得全局的应用程序指针.
KillApplication()销毁应用程序,并使全局的应用程序指针变得无效.
Application()全局的内联函数,用来它简化对全局应用程序指针的访问.
AppInstance()全局的内联函数,用来获得应用程序的HINSTANCE.
CreateApplication()没有定义的全局函数,在将来的应用程序中它必须被声明.
它创建一个供GameLib内部代码使用的应用程序对象.
如果应用程序从cApplication分出一个子类cMyApplication,CreateApplication()会返回新的cMyApplication.
应用程序的WinMain()已经从子程序中被抽象出来,并被隐藏在GameLib的代码之中.
为了使大家不至于忽略它,我把其代码列在下面.
intAPIENTRYWinMain(HINSTANCEhInstance,HINSTANCEhPrevInstance,LPSTRlpCmdLine,intnCmdShow){cApplication*pApp;g_hInstance=hInstance;try{pApp=CreateApplication();pApp->ParseCmdLine(lpCmdLine);pApp->Init();pApp->SceneInit();pApp->Run();}catch(cGameError&err){/***Knockoutthegraphicsbeforedisplayingthedialog,*justtobesafe.
*/if(Graphics()){Graphics()->DestroyAll();}MessageBox(NULL,err.
GetText(),"Error!
",MB_OK|MB_ICONEXCLAMATION);//CleaneverythingupdeletepApp;return0;}deletepApp;return0;}1.
9COM:组件对象模型基于组件的软件开发是一个很大的领域.
它不是要设计一些深度关联的软件模块(这种被称为整体式的软件开发),而是要求每个开发小组都应开发出一套可以彼此通信的小组件.
如果组件被充分模块化,那么可以毫不费力地在其他项目中使用这些组件,这就是组件化的优势.
另一个优势就是:能够彼此独立地更新和改进各个组件.
只要组件之间的通信模式不变,就不会发生问题.
为了帮助大家进行基于组件的软件设计,Microsoft建立了一种叫组件对象模型(ComponentObjectModel)的体系,简称COM.
它为对象间的相互通信,以及阐明对象自身的功能提供了标准的方法.
它是语言独立、平台独立甚至机器独立的(一个COM对象可以通过网络连接同另外一个COM对象进行对话).
在这一节,我们将学习在基于组件的软件开发中如何使用COM对象.
由于在本书中并不需要您知道如何构建自己的COM对象,因此这方面我们不涉及.
如果想了解更多的关于COM的知识,建议阅读一些专门讨论COM的书籍.
实际上,COM现在受到了相当多的抨击,并且已经被.
NET结构体系所取代了.
然而,它在DirectX封装内仍然被使用,因此,它还会在一段时间内继续出现.
从本质上说,COM组件是实现一个或多个COM接口的程序块.
(我喜欢这种循环式的定义.
)COM接口就是一组函数.
实际上,实现它的方法和绝大多数C++编译器实现虚函数表的方法一样.
在C++中,COM对象只是从一个或多个抽象基类继承而来,这些抽象基类被称为COM接口.
其他类只需通过调用函数接口来获得COM对象.
函数只能存在于接口之中.
要访问成员变量,也只有通过接口中的Get/Set函数来实现.
所有COM接口,都直接或间接地来自于一个叫IUnknown的类.
用专业术语讲,这意味着在所有COM接口的虚拟表中,最初的三项是三个同样的IUnknown函数.
此接口如下:typedefstructinterfaceinterfaceIunknown{virtualHRESULTQueryInterface(REFIIDidd,void**ppvObject)=0;virtualULONGAddRef(void)=0;virtualULONGRelease(void)=0;};AddRef()和Release()为我们实现引用计数.
COM对象的创建是不受我们控制的.
它们可能通过new、malloc()或者一个完全不同的内存管理器来创建.
因此,当您不再使用它们时,不能简单地删除该接口.
引用计数允许对象实现内存的自治.
引用计数表示在不同的代码中某个对象被引用的次数.
当您刚创建一个COM对象的时候,由于只有您使用了它,所以此时引用计数一般为1.
当其他组件中有一些代码需要接口时,我们一般要调用接口中的AddRef()函数,告诉COM对象有另外一些代码正在使用它.
当某段代码不再使用某接口的时候,它将调用Release()函数,引用计数将减1.
当引用计数达到0的时候,就意味着当前没有任何对象正在调用此COM对象,那么它就能够安全地自我销毁了.
警告:不再使用某COM对象而又没有释放它们,它们将不会自我销毁.
这会在您的应用程序中造成令人讨厌的资源泄漏.
QueryInterface()是可以让COM工作的函数.
它允许对象向其他有接口的COM对象请求另一个接口.
如果得到被请求的接口的支持,就可以传递给QueryInterface()两个参数,一个是接口ID,另一个是指向void指针类型的指针.
让我们以汽车作为例子.
我们创建一个汽车对象,并且获取了一个指向iCarIgnition的接口指针.
如果要改变广播频道,就可以请求iCarIgnition接口的所有者,问它是否也支持iCarRadio接口.
ICarRadio*pRadio=NULL;HRESULThr=pIgnition->QueryInterface(IID_ICarRadio,(VOID**)&pRadio);if(!
pRadio||FAILED(hr)){/*handleerror*/}//NowpRadioisreadytouse.
这就是COM优美的地方.
不需要重新编译,就能够改进对象.
如果决定在汽车中增加对CD播放器的支持,只需运行QueryInterface()即可.
想要COM像上面这样工作,在设计系统时就必须受到两个方面的限制.
第一个限制就是所有的接口都必须是公开的.
打开DirectX的头文件,就可以发现所有的DirectX接口的定义.
只要有接口定义和COM接口的ID,任何COM程序都可以使用所有的COM对象.
第二个限制,也是COM接口永远都无法改变的最大的限制.
一旦COM接口被公开发布,用任何形式都不能修改它们(甚至是一些无损的修改,如在接口中加函数等,也不能进行).
如果没有这点限制,每当接口被更改的时候,那些用到了COM对象的应用程序都需要被重新编译,这违背了COM的初衷.
要给COM对象添加功能,就需要增加新的接口.
例如,如果想扩展iCarRadio,增加低音和高音的控制,不能直接增加函数,需要把这些新函数加到一个新的接口中去,这个新接口可以叫做iCarRadio2.
那些不需要用到新功能或在iCarRadio2创建之前就编译了的应用程序,并没有受到任何影响,它们仍然能够用iCarRadio接口正常运行.
那些需要新功能的应用程序只需使用QueryInterface()函数来获得iCarRadio2接口.
前天,还有在"Hostodo商家提供两款大流量美国VPS主机 可选拉斯维加斯和迈阿密"文章中提到有提供两款流量较大的套餐,这里今天看到有发布四款庆祝独立日的七月份的活动,最低年付VPS主机13.99美元,如果有需要年付便宜VPS主机的可以选择商家。目前,Hostodo机房可选拉斯维加斯和迈阿密两个数据中心,且都是基于KVM虚拟+NVMe整列,年付送DirectAdmin授权,需要发工单申请。(如何...
特网云为您提供高速、稳定、安全、弹性的云计算服务计算、存储、监控、安全,完善的云产品满足您的一切所需,深耕云计算领域10余年;我们拥有前沿的核心技术,始终致力于为政府机构、企业组织和个人开发者提供稳定、安全、可靠、高性价比的云计算产品与服务。公司名:珠海市特网科技有限公司官方网站:https://www.56dr.com特网云为您提供高速、稳定、安全、弹性的云计算服务 计算、存储、监控、安全,完善...
tmhhost怎么样?tmhhost正在搞暑假大促销活动,全部是高端线路VPS,现在直接季付8折优惠,活动截止时间是8月31日。可选机房及线路有美国洛杉矶cn2 gia+200G高防、洛杉矶三网CN2 GIA、洛杉矶CERA机房CN2 GIA,日本软银(100M带宽)、香港BGP直连200M带宽、香港三网CN2 GIA、韩国双向CN2。点击进入:tmhhost官方网站地址tmhhost优惠码:Tm...