欢迎回到"使用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里滚动条就是有两边各有一个按钮的滑动条.这两个按钮(上/下或左/右箭头)会移动滑快单位距离.这种方法消除了大量的按钮类和滚动条之间的代码复制,我强烈推荐你看看做相似的事.
看完了滚动条,看看最复杂的控件吧.
列表框控件
移出精力看这个吧,列表框控件是你要花最多时间的地方.
// represents a column in our listbox
class gui_listbox_column { public: gui_listbox_column() { } virtual ~gui_listbox_column() { }
virtual void draw(uti_rectangle &where);
void setname(const char *name) { m_name = name; } uti_string getname(void) { return(m_name); }
int getwidth(void) { return(m_width); } void setwidth(int w) { m_width = w; }
private: uti_string m_name; int m_width; };
// an item in our listbox
class gui_listbox_item { public: gui_listbox_item() { m_isselected = 0; m_indent = 0; } virtual ~gui_listbox_item() { }
virtual draw(int colnum, uti_rectangle &where);
void clearallcolumns(void); // boring
void setindent(int i) { m_indent = i; } int getindent(void) { return(m_indent); }
void settext(int colnum, const char *text); // boring
uti_string gettext(int colnum = 0); // boring
void setitemdata(unsigned long itemdata) { m_itemdata = itemdata; } unsigned long getitemdata(void) { return(m_itemdata); }
void setselected(int s = 1) { m_isselected = s; } int getselected(void) { return(m_isselected); }
private: int m_isselected; int m_indent; // # of pixels to indent this item
unsigned long m_itemdata; uti_pointerarray m_coltext; };
// the listbox itself
class gui_fancylistbox : public gui_window { public: gui_fancylistbox() { m_multiselect = 0; } virtual ~gui_fancylistbox() { clear(); }
int getselected(int iter = 0);
virtual int wm_command(gui_window *win, int cmd, int param); virtual int wm_paint(coord x, coord y); virtual int wm_lbuttondown(coord x, coord y);
gui_scrollbar_horz &gethscroll(void) { return(m_hscroll); } gui_scrollbar_vert &getvscroll(void) { return(m_vscroll); }
virtual int wm_sizechanged(void); // the window's size has changed somehow
gui_listbox_item *getitemat(int index); // boring
gui_listbox_item *additem(const char *text); // boring
int delitem(int index); // boring
int delallitems(void); // boring
gui_listbox_column *getcolumn(int index); // boring
int addcolumn(const char *name, int width); // boring
gui_listbox_column *getcolumnat(int index); // boring
int delcolumn(int index); // boring
int delallcolumns(void); // boring
int clear(void); // delete columns & items
int getnumitems(void); int getnumcols(void);
void deselectall(void); void selectitem(int item); void selecttoggleitem(int item);
void deselitem(int item);
private: int m_numdispobjsy; int m_vertgutterwidth; // # of pixels between items vertically
gui_scrollbar_horz m_hscroll; gui_scrollbar_vert m_vscroll;
bool m_multiselect; // is this multi-selectable?
uti_pointerarray m_items; // array of gui_listbox_items
uti_pointerarray m_columns; // array of gui_listbox_columns
};
|
列表框是到现在为止你做的最难的控件吧?但这仅仅是因为它是最通用的.一个能够处理多列、缩进、多重选择列表框控件将在实践中证明他对你的游戏是不可或缺的.停下来并想想在大多数游戏里用到列表框的地方,你就会很快发现这一点.
我把我的列表框控件分成两部分:一个多列的"报表风格"的列表控件和一个图标列表控件,它创建一个类似于当你在windows"我的电脑"里选择大图标察看方式的显示.
图表列表控件比较容易建立.它使用了一列静态图标(在一次代码重用),所有的具有相同的大小.我使用图标的宽度除列表框的宽,这让我知道有几列可用.(如果证明我的列表框比大图表小,我假设我只有一列,并让绘制系统剪裁图标以使他们不会超出我的绘制区域).一旦我有了列数,我通过图标的总数除以它计算出我所需要的行数.这样我就知道我该怎样设定要包括的滚动条.
注意当控件改变大小时必须重新计算这些值.为此我设定了一个wm_sizechanged()消息,calcall()将会在窗口绘制区域被改变的时候调用它.
报表风格列表控件要复杂一些.我先写了两个辅助类,gui_listbox_column和gui_listbox_item,它们包含了所有的关于列表中给定物件和列的信息.
gui_listbox_column是两者中较简单的.主要的列表框类有一个成员变量身份的gui_listbox_column的动态数组,这代表了目前列表框中的列.gui_listbox_column包含了在列表框中所需要的列的所有信息,包括列的名字,列的对齐,显示或隐藏,大小等等.
主要的列表框类也有一个gui_listbox_item的动态数.gui_listbox_item类包含了与我们的报表风格列表框中特定行(或物件)相关的所有信息.目前这个类最重要的数据成员是代表每列数据的字串数组.我也让每个物件通过m_itemdata成员存储一个附加的32位数据.这个技术类似于windows允许你通过位你的列表物件调用SetItemData()和GetItemData()来存储32位数据.这个细节很重要,因为它允许列表框的用户为每个物件存储一个指针-通常一个与该物件有关的特定类,以使它以后可用.
怎么绘制列和物件呢?我倾向于在要绘制的列表框中在每个单独的物件/列上有个绝对的控件.到最后,我决定让列表控件通过不断调用两个虚函数,gui_listbox_item::draw()和gui_listbox_column::draw()来绘制他的物件和列.每个函数使用一个代表列或者物件在屏幕上位置的矩形.默认的对这些draw()函数的开发仅仅分划出与矩形中特定列和子物件相关的文本;然而,我先在可以简单的为需要独特外观的物件或列派生和重载draw().这种技术目前工作的很好,但是我还不足以宣称这是最好的方法.
然而,绘制物件比行需要更多的工作.物件需要用高光绘制,这决定于他们是否被选择.这并不很难,但一定不能忘记.
然后就是滚动条的问题了.我的列表框包含两个成员,m_horzscrollbar和m_vertscrollbar,他们都是GUI滚动条.当列表框的大小被改变时(wm_sizechanged()),他会看看数据的宽度和高度并决定是否显示滚动条.
总结
真是绕了一大圈子,但是幸运的是你对为GUI创建控件有了个大致的想法.这里我想强调的是"风格就是乐趣".在做你的GUI时不要害怕创新-做做你曾经梦想过的东西,和使你的游戏最酷的东西.如果你的游戏很依赖你的GUI的效能这一点尤其重要,比如你在作即时战略游戏.
还要记住当创建控件的时候,你需要为你的游戏的其他部分考虑平衡问题-细节表现力和开发时间是成正比的.尽量给你的玩家最易于上手的GUI,但同时不要花时间做50种不同的控件.你要在功能、好事、复杂性、坏事中做出平衡.
控件就到这吧.下一章也是最后一章中我们会看看资源编辑器,序列化窗口和创建对话框.祝愉快!
|