欢迎回到"使用C++和DX开发GUI"的第三部分。接着我们的主题(描述我如何为我未来的游戏构建GUI),本文将探讨建造GUI所需的一些通用控件.我们将详细描述几种不同的控件形式,包括按钮,列表框,文本框等等.
这一节并不像其他章节那样有很多的代码--这主要是因为我们程序员对于GUI的外观是很挑剔的.我们喜欢把我们的按钮,文本框和GUI做的看起来独一无二,并且符合我们自己的审美标准.这样的结果是,每个人的控件代码都很不同,而且不会想要我的特殊的绘制代码.此外,写绘制GUI元素的代码是很有趣的,事实上,以我来看,这是在写GUI过程中最有趣的部分了.现在继续.
一个很重要的问题是,在我们开始之前-把你的gui_window析构函数做成虚函数.在第二部分里我没有提到这一点,因为我们没有从gui_window中派生出任何子类,但是现在我提出这一点-把你的gui_window和所有它的派生类的析构函数做成虚函数是很明智的做法,因为这将确保没有内存泄漏--由于派生的析构函数没有被调用.小心C++的陷阱.
说完这点之后,让我们首先判断我们需要什么样的GUI控件.
我们需要的GUI控件
我不想花太多的时间来为我的GUI开发控件;我只会专注于最简单的控件集.所以,我先列出我认为是最小控件集的控件:
1.静态文本,图标和组合框(最重要).这些空间将对话框中的其他控件标志或分组.静态控件很重要;我们可能不需要帧控件,但它非常简单,并且在某些情况下能够使对话框易于导航,所以我会包括它.图标控件也很简单,但是应该能够表现动画-为我们的对话框和菜单提供很酷的背景动画(神偷:黑暗计划).
2.按钮和选择框(最重要).特殊形式的按钮不是必需的,然而大多数的游戏不能没有基本的按钮和选择框.
3.列表框(重要).我发现列表框,特别是多列列表控件,在创建游戏GUI时是不可或缺的.他们的应用无所不在.你需要一个智能的,重量级的列表控件,和windows的列表控件一样好或者更为出色.对我而言,列表控件是最难开发的控件了.
4.滑动条和滚动条(重要).最常见于音量控制.坏消息是我们可能需要水平和垂直滚动条,好消息是他们很相似所以开发很简单.
5.文本框(最重要).你必须能够键入你的mega-3133t,super-kewl玩家标志,对吧?
6.进度条-对显示生命值是必需的,"我快要装载好了!"等等情况也是如此.
这里缺少的是纺锤状按钮(spin button),单选框(我们可以用一个单选列表框代替),下拉组合框(同样我们可以用列表框代替)以及树状控件.通过设计巧妙的列表控件来缩进特定物体,我们能够实现树状列表德功能.
由于我的游戏并没有足够的GUI来保证表状控件,所以在此没有包含它,虽然你可能会需要. 即使有这些遗漏,上述"最小"列表可能看上去还是很繁杂,但是我们能够简化一点儿.
把它打破:组合简单空间来实现复杂控件
如果我们意识到复杂控件仅仅是简单控件的巧妙组合,列表就会更易于控制.例如,一个滚动条基本上只是两个按钮和一个滑动条.一个选择框是一个静态文本和两个按钮(一个"打开"按钮,一个"关闭"按钮).一个平面按钮能够使用三个图标控件来实现(仅仅显示/隐藏适当的图标来使按钮显得被按下),这样你能够重用你的绘制代码.如果你的确没有时间,你甚至可以把一个进度条当作滑动条来用,虽然我更倾向于是用一个独立的控件.
然而,这样做是有缺陷的,名义上你的GUI控件会比他们实际需要的占用更多的系统资源.仔细考虑它-每个控件是一个窗体.让我们说你使用了重用法则创建了一个实际上是三个静态控件的按钮控件.那么每个按钮就是三个窗体.现在你使用两个按钮控件创建一个滚动条,那就是每个滚动条6个窗体.使用水平和垂直滚动条创建一个列表控件,那么每个列表就是12个窗体.它增加得很快.
所以这就是另一个经典的关于"我能多快的开发"和"我会使用多少资源"的矛盾的例子.如果你需要一个高性能,没有浪费的GUI,从基础来开发每一个控件.如果你想要快速开发,那就不要介意性能损失,你或许会选择开发控件以使实际上绘制到屏幕上的是静态控件,所有其他控件都是由静态控件组合而成的.
我开发GUI的时候,我尽力在两个极端之间取得良好的平衡.
现在,让我们开始关注每个控件的实际开发,从每个人最喜欢的静态标志开始吧.
我们需要关注三种静态控件:静态文本控件,静态图标控件和框架控件.这三种控件都很简单,因为他们不接收消息-他们所作的只是在某个位置绘制本身而已.
静态文本控件是你将开发的最简单的控件了-仅仅在窗口的左上角绘制窗口的标题,就行了.如果你想增加代码来以某种方式调整你的文本-比如,在绘制框中居中你的文本,你可能会使用经典的居中算法.-用窗体的宽度减去要绘制的文本的宽度,然后除以2,告诉你从距离窗体左边多少像素开始绘制.
静态图标控件稍微难一点儿.实际上,"静态图标控件"这个术语有些歧义,假定我们想要我们的图标控件可以表现动画的话.即使如此,开发这些图标控件也不难,假设你已经有了丰富的精灵库来处理所有开发动画的细节:检测两帧之间的时间差,使用这个差值来判断你的精灵将要走多少帧,等等.
图标控件只有当你在每一帧并不绘制整个GUI系统的时候才变得麻烦.这种情况下,你多少要处理一些图标控件的裁剪工作,这样即使每帧都绘制,也不会覆盖属于在其上的窗口的像素(但是没有改变,所以没有绘制).我没有开发这个-我的GUI每一帧都重画-但是如果你面临这个问题,你可能会想试试为每个图标设立裁剪列表,用它来绘制图标,当有任何一个窗体移动、关闭或者打开时重新计算它.这或许是个可行的方法-我只是如此构想-但是这起码是一个好的切入点.
框架控件也很简单.我开发我的框架控件时只是围绕m_position绘制边框,然后在大约绘制坐标(5,5)点附近(大约从框架控件的左上角向右向下5个像素)绘制窗口标题,你可以依照自己的想象自己决定.
你在开发静态控件中可能碰到的麻烦事是稍微改变findwindow函数的功能以使它跳过所有的静态控件窗口.这样,如果一个静态文本控件是在一个按钮之上的,用户可以透过静态控件来按这个按钮.当开发"简易移动"窗口(即你可以通过按住窗口的任何部位来移动窗口,而不仅仅是标题栏,就象winamp)的时候,这很有用.
现在让我们来看看如何开发按钮.
按钮控件
按钮只比静态控件难一点儿.你的按钮控件需要不断跟踪是否它被按下或松开.它通过两个虚函数来实现,wm_mousedown()和wm_mouseup(),你的calcall()函数需要在适当的时候调用它们.
基本上,在wm_mousedown()里,你要设定一个布尔变量,我把它叫做"depressed flag"(按下标志)为真,而在wm_mouseup()里,把它设为假.然后再你的绘制代码里如果按下标志为真,绘制按钮的按下状态,否则,绘制松开状态.
然后,增加一个附加状态-即"只有当按下标志为真和鼠标指针在绘制区域之中时绘制按钮的按下状态,否则把按下标志设为假."如果你把鼠标移出按钮这将使你的按钮弹起,并且对于精确判断一个按钮何时被按下非常重要.
对于普通的GUI,当一个按钮被点击,将为他的父窗体引发一个事件,窗体会做按钮所代表的任何事-例如,点击关闭按钮将关闭窗口,点击存储按钮将存储文件,等等.我的GUI在且仅在wm_mouseup()中判断按钮是否被点击,按下标志是否为真.按下标志在mouseup()中还为真的唯一情况是用户在鼠标在按钮之内按下和松开鼠标键.这允许用户在最后放弃选择-通过保持鼠标键按下并把鼠标指针拖到按钮之外松开,就象其他的GUI一样.
这就是按钮了.现在来看看文本框吧.
插入符和文本控件
我选择的是非常简单的文本控件.它仅仅捕捉击键,而且还不卷屏-但是你可能会要更加复杂的,也就是一个可以精确处理跳到开始(home)、跳到末尾(end)、插入和删除字符,或者可能还要通过windows剪贴板支持剪切、拷贝、粘贴.
但是在我们做文本框之前,我们需要一个插入符.如果你对这个术语不熟悉,这里解释一下.插入符是光标的另一种说法-是的,就是那个小小的闪动的竖线.插入符告诉用户他们的击键将会在哪里出现文字.
从我的GUI考虑,我很简单的处理这些事-我指定活动窗口是具有插入符和句号(这里rick不是很明白)的窗口.大多数GUI都是这样的,好像也是最好的解决办法.而且我的GUI象windows那样把文本框的标题(caption)当作文本框里的文字来处理.
那么你怎么开发插入符呢?好的,我想因为我们知道插入符总是在活动窗口里被绘制,并且插入符只有在活动窗口是文本框的时候出现,很容易联想到插入符绘制代码是文本框的一部分并且在文本框的绘制函数里完成.这就使它很易于开发-只要用一个整形变量来代表窗口标题字符数组的索引,你的文本框就有要绘制插入符的所有信息了.
这就基本上表示,如果是个文本框的话,你要做的所有绘制工作就是围绕绘制区域画边线,在边线之内绘制窗口标题,然后如果是活动窗口,在正确的位置画出插入符.在我的GUI里,文本框中字符的最大长度是由文本框窗口的大小来决定的,也就是说我不用处理在文本框之内滚动文字.然而你或许会想要用户可以在很小的文本框里输入很长的字串并可以滚动查看文本框中的内容.
现在来看看关于文本框的最难的东西-键盘处理.一旦会有击键发生,很容易建立一个wm_keypressed()虚函数并且调用它,同样很容易为wm_keypressed开发文本框处理器,然后要么把字符放到窗口标题的末尾,要么处理特殊击键(backspace键,等等-这是你的字串类要关注的东西),然后移动插入符.
难的地方在于在第一位置得到击键.windows提供了至少三种完全不同的方法来查询键盘-WM_KEYDOWN事件,GetKeyboardState()和GetAsyncKeyState()函数,当然还有DirectInput.我使用了DirectInput方法,这是因为我在开发鼠标光标的时候就已经作了大量的和DirectInput相关的工作,另外通过DirectInput来获取键盘状态对我也是最简洁和优雅的方法.
要使用DirectInput的键盘函数,你要做的第一件事是建立键盘设备.这和我们在第一章中建立DirectInput的鼠标设备的方法令人难以相信的相似.基本上,唯一的差别在于不是告诉DirectInput把我们的新设备当作鼠标来处理,而是当作键盘.如果你已经了解DirectInput处理鼠标的方法,那么再把同样的事情为键盘再做一遍.
一旦获取了键盘设备我们就可以查询它.
要实际判断一个键是否被按下需要多一点工作.基本上,要判断哪个键被按下,你需要对所有101个键的状态的两个快照-一个来自上一帧另一个当前帧.当前帧中被按下的而上一帧没有按下的键是被"点击"的,你要为他们发送wm_keypressed()消息.
来看看进度条。
进度条
进度条如同静态控件一样易于开发,因为他们只接收很少几个消息.
基本上,你需要为进度条做两件事-你要告诉它最大/最小范围和步长.例如,我要创建一个载入进度条,由于我要载入100个不同的游戏资源.我会创建一个范围为0到100的进度条.我会把进度条初始为0,然后,当我载入一个资源的时候我会用单位长度来让进度条前进一个步长.当进度条前进时,它都会重画自身,图形上用一个和绘制区成比例的长条来表示出它有多长.
进度条很象滚动条;实际上,可以用滚动条的方法来开发进度条.我把进度条和滚动条分开开发是因为我想要他们有非常不同的外观和细微差别的行为-你的需要可能会不同.
滑动条和滚动条
绘制滑动条或者滚动条和绘制进度条很相似,这表现在你需要用滑动条的绘制矩形的百分比,它提供了绘制滑快的位置信息,来表现它的当前位置.你要为垂直和水平控件作些细微的修改-我先做了个基类,gui_slider,其中包含了所有的公用代码和所有的成员变量,然后开发两个不同的派生类,gui_slider_horz和gui_slider_vert,它们处理绘制和点击逻辑的不同.
就象处理鼠标点击一样,我为滑动条选择了简便的方法.如果鼠标点击在滚动条绘制区内发生,直接自动地滚动到那个位置.在我的滑动条里,你不能同时在轴上点击和移动位置-直接跳到你点击的地方.我这么做主要是因为这样会很简单,而且我不喜欢windows默认的方法.
关于滚动条/滑动条的逻辑,你知道和进度条的基本设定是一样的-最小、最大、当前位置.然而不象进度条,用户可以通过在控件上点击改变当前位置.
现在看看滚动条.我的GUI里滚动条就是有两边各有一个按钮的滑动条.这两个按钮(上/下或左/右箭头)会移动滑快单位距离.这种方法消除了大量的按钮类和滚动条之间的代码复制,我强烈推荐你看看做相似的事.