directx开发:使用C++和Directx开发GUI( 2)



欢迎您继续阅读\"使用C和Directx开发GUI\"第 2部分.这里是第部分.接着我们主题(讲解在我未来游戏如何使用GUI(图形用户界面)),本文将解释窗体许多神秘的处.我们将关注窗体树如何工作,为我们使用GUI制订计划,以及创建窗体类细节,包括绘制,消息机制,坐标系统和其他所有麻烦事儿. 在此我们将着重使用C.如果你对纯虚,dynamic_cast\'ing等等已经生疏了,那么赶快翻翻C书再继续吧.

不开玩笑了,让我们开始.

在涉及代码的前,明确我们目标是很重要.

在我们游戏已完成GUI里,我们将使用个树来跟踪显示在屏幕上每个窗体.窗体树是个简单N节点树.树根部是视窗桌面(windows desktop).桌面窗体(Desktop window)子窗体通常是应用主窗体;主窗体子窗体是对话框,对话框子窗体是独立对话Control控件(按钮,文本框等).重要区别在于--窗体外观并不取决于它在树中位置.例如,许多游戏把按钮直接放在他们桌面窗体上,就如同对话框样. 是,按钮也是窗体.意识到这点是很重要.个按钮只是个有着有趣外观窗体.实际上,所有GUIControl控件都是有着区别外观简单窗体.这体现了C能力.如果我们创建个继承窗体类,给它几条虚,我们就能通过重载基类轻易地创建我们Control控件.如此应用多态性简直称得上优雅;实际上,许多C书将它作为范例(在第 3部分我将详述此点). 这是我们基本设计,下面让我们想想应用思路方法.

计划

当我应用我GUI时,我做了如下几步:

1.首先我写了些基本窗体管理代码.这些代码负责窗体树,增加/删除窗体,显示/隐藏窗体,把它们移动到Z坐标顶端(即在最前显示),等等.我通过在窗体应处位置绘制矩形完成了窗体绘制过程,然后根据窗体Z坐标在左上角绘制个数字. 如果你购买或编写个优秀可靠指针阵列模版类,那你生活将会变得非常轻松.STL(标准模版库Standard Template Library)得到许多C版本支持,它有很多好模板性指针阵列类,但是如果你想使用你自己模板类,在你应用于你窗体管理的前要进行完整彻底测试.现在你要注意问题是由阵列类所引起不易察觉内存泄漏或空指针引用.

2.旦我有了基础窗体管理,我花了些时间研究我坐标系统.写了些坐标管理.

3.下步,我处理窗体绘制代码.我继承个\"奇异窗体\"类,并显示它如何使用套 9个精灵绘制自身--其中 4个精灵绘制角落, 4个绘边,个绘制背景. 使用这 9个窗体精灵,使创建既有独特艺术外观又可动态改变大小(ala StarDock\'s WindowBlinds)窗体成为可能.这样做基础是你需要有个相当智能绘图库,个能处理封存精灵,弹性精灵以及集中精灵库,并且它是个非常复杂窗体生成(些艺术家可以用以创建他们窗体代码),这使这种思路方法可以实际实现.当然,你也要注意窗体绘制速度.

4.旦普通窗体绘制代码完成,我开始实现控制部分.代码控制是简单,但还是需要非常彻底测试.我由简单控制:静态,图标等开始像在前面解释那样来回反复我工作.

5.最后,完成我控制部分后,我开始编写个简单资源编辑器,个允许用户可视放置Control控件,布局对话框.这个资源编辑器用了我整整个月时间,但我强烈建议这样做(而不是用文本文件去决定位置)--图形化对话框建立非常容易,并且这也是个好练习:在完善中我在我控制部分代码中没有发现几个bug,在实际中被证明是很难解决.

我被编写个可以转换MSVC资源(.RC)文件为我GUI可使用资源文件这个想法困扰了好久.最后,我发现这样远比它价值麻烦.我写这个GUI就是要摆脱Windows限制,为了正真做到这点,我要由自己编辑器,使用我自己资源文件格式,按自己形式做事情.我决定用MFC由底层实现个所见即所得(WYSIWYG)资源编辑器.我需求,我决定;你需求也许区别.如果某人想要写个转化器,我将很乐于听到这样消息. 现在到哪了?这篇文章剩下部分将探究开始两步.这系列第 3部分将进入令人麻木控制代码细节.第 4部分将讨论点资源编辑器实现和序列化窗体. 因此...让我们来开始第步:基本窗体管理代码.

实现

我们开始.这是为我们基本窗体类定义开始:

gui_window
{
public:
gui_window; // boring
~gui_window; // boring
virtual void init(void); // boring
gui_window *getparent(void) { (m_pParent); }

/////////////
// section I: window management controls
/////////////

addwindow(gui_window *w);
removewindow(gui_window *w);

void show(void) { m_bIsShown = true; }
void hide(void) { m_bIsShown = false; }
bool isshown(void) { (m_bIsShown); }
void bringtotop(void);
bool isactive(void);

/////////////
// Section II: coordinates
/////////////

void pos(coord x1, coord y1); // boring
void size(coord width, coord height); // boring

void screentoclient(coord &x, coord &y);

virtxtopixels(coord virtx); // convert GUI units to actual pixels
virtytopixels(coord virty); // ditto

virtual gui_window *findchildatcoord(coord x, coord y, flags = 0);

/////////////
// Section III: Drawing Code
/////////////

// renders this window + all children recursively
renderall(coord x, coord y, drawme = 1);

gui_wincolor &getcurrentcolor(void)
{ (isactive ? m_activecolors : m_inactivecolors); }

/////////////
// Messaging stuff to be discussed in later Parts
/////////////

calcall(void);

virtual wm_pa(coord x, coord y);
virtual wm_rendermouse(coord x, coord y);
virtual wm_lbuttondown(coord x, coord y);
virtual wm_lbuttonup(coord x, coord y);
virtual wm_ldrag(coord x, coord y);
virtual wm_lclick(coord x, coord y);
virtual wm_keydown( key);
virtual wm_command(gui_window *win, cmd, param) { (0); };
virtual wm_cansize(coord x, coord y);
virtual wm_size(coord x, coord y, cansize);
virtual wm_sizechanged(void) { (0); }
virtual wm_update( msdelta) { (0); }



protected:

virtual void copy(gui_window &r); // deep copies _disibledevent=>

当你细读我们讨论,你将会发现递归到处可见.比如,我们将通过个源窗体思路方法renderall来绘制整个GUI系统,这个思路方法又将回调它子窗体renderall思路方法,这些子窗体renderall思路方法还要调它们子窗体renderall思路方法,以此类推.大部分都遵循这种递归模式. 整个GUI系统有个全局静态变量--源窗体.出于安全性考虑,我把它封装在个全局GetDesktop中.
现在,我们开始,我们来完成,由窗体管理代码开始,如何?


窗体管理

/****************************************************************************
addwindow: adds a window to this window\'s subwin .gif' />
****************************************************************************/
gui_window::addwindow(gui_window *w)
{
(!w) (-1);
// _disibledevent=>w->parent(this);
(0);
}

/****************************************************************************
removewindow: removes a window from this window\'s subwin .gif' />
****************************************************************************/
gui_window::removewindow(gui_window *w)
{
w->parent(NULL);
(m_subwins.findandremove(w));
}

/****************************************************************************
bringtotop: bring this window to the top of the z-order. the top of the
z-order is the HIGHEST index in the subwin .gif' />.
****************************************************************************/
void gui_window::bringtotop(void)
{
(m_parent) {
// we gotta save the old parent so we know who to add back to
gui_window *p = m_parent;
p->removewindow(this);
p->addwindow(this);
}
}
/****************************************************************************

isactive: s true this window is the active _disibledevent=>}

系列是处理我所说窗体管理:新建窗体,删除窗体,显示/隐藏窗体,改变它们Z坐标.所有这些都是完全列阵操作:在这里你列阵类得到测试. 在增加/删除窗体中唯感兴趣问题是:\"谁来对窗体指针负责?\"在C中,这总是个问自己得很好问题.Addwindow和removewindow都要获得窗体类指针.这就意味这创建个新窗体你代码新建个指针并通过addwindow把指针传到父(桌面)窗体.那么,谁来负责删除你新建指针呢?

回答是\"GUI不拥有窗体指针;游戏本身负责增加指针\".这和C笨拙规则\"谁创建谁删除\"是.

我选择可行思路方法是\"父窗体为它所有子窗体指针负责\".这就意味着为了防治内存泄漏,每个窗体必须在它(虚拟)析构(记住,有继承类)中搜寻它子窗体列阵并且删除所有包括在其中窗体.

如果你决定实现个拥有指针系统GUI,注意个重要原则--所有窗体必须动态分配.这样系统崩溃最快思路方法是把个变量地址传到堆栈中,如\"addwindow(&mywindow)\",其中mywindow被定义为堆栈中局部变量.系统将好好工作直到mywindow超出它有效区,或其父窗体析构,此时系统将试图删除给地址,这样系统即崩溃.所以说\"对待指针定要特别小心\".

这就是为什么我GUI不拥有窗体指针主要原因.如果你在你GUI中处理大量复杂窗体指针(也就是说,比如你要处理属性表),你将更想要这样个系统,它不必跟踪每个指针比且删除只意味着\"这个指针现在为我所控制:只从你列阵中移走它但并不删除它\".这样只要你能保证在指针超出有效区前removewindow,你也可以使用(小心)在堆栈中局部变量地址.

继续?显示和隐藏窗体通过个布尔型变量来完成.Showwindow和hindewindow只是简单设置或清除这个变量:窗体绘制和消息处理在它们处理任何的前先检查这个\"窗体可见\"标志位.非常简单吧!

Z坐标顺序也是相当简单.不熟悉这种说法,可把z坐标顺序比为窗体\"堆栈\"个重叠个.开始,你也许想像DirectDraw处理覆盖那样实现z坐标顺序,你也许决定给每个窗体个整数来描述它在z坐标绝对位置,也就是说,可能0表示屏幕顶端,则-1000代表最后.我想了下这种Z坐标顺序实现思路方法,但我不赞成--Z坐标绝对位置不是我所关心;我更关心是他们相对位置.也就是说,我不需要准确知道个窗体在另多后,我只要简单知道这个给定窗体在另后面还是前面.

所以,我决定实现Z坐标顺序如下:在列阵中有最大索引值,m_subwins,窗体在\"最前\".拥有[size-1]窗体紧跟其后,紧接着是[size-2],依次类推.位置为[0]窗体将在最底.用这种思路方法Z坐标顺序实现变得非常容易.而且,举两得,我将把最前窗体视为活动窗体,或更技术说法,它将被视为拥有输入焦点窗体.尽管我GUI使用这种\"始终最前\"窗体是有限制(比如,在Windows NT中任务管理器不管输入焦点始终在所有窗体的前),我觉得这样有利于使代码尽可能简单.



当然,我用数列表示Z坐标顺序在我移动窗体到最前时处理数列付出了些小代价.比如,我要在50个窗体中将第 2个窗体移到最前;我将为了移动 2号窗体而移动48个窗体.但信运是,移动窗体到Z坐标最前不是最耗时,即使是,也有很多好思路方法可以处理,比如链表即可.

看看我在bringtotop小窍门技巧.我知道窗体不拥有指针,我就删除这个窗体又马上创建个,非常有效率将它重定位在数列最前.我这样做是指针类,uti_poer.gif' />,已经被编写好了旦删除个元素,所有更高元素将向后移动.

这就是窗体管理了.现在,进入有趣坐标系统?

坐标系统

/****************************************************************************
virtual coordinate system to graphics card resolution converters
****************************************************************************/
const double GUI_SCALEX = 10000.0;
const double GUI_SCALEY = 10000.0;

gui_window::virtxtopixels( virtx)
{
width = (m_parent) ? m_parent->getpos.getwidth : getscreendims.getwidth;
(()((double)virtx*(double)width/GUI_SCALEX));
}

gui_window::virtytopixels( virty)
{
height = (m_parent) ? m_parent->getpos.getheight : getscreendims.getheight;
(()((double)virty*(double)height/GUI_SCALEY));
}

/****************************************************************************
findchildatcoord: s the top-most child window at coord (x,y);
recursive.
****************************************************************************/
gui_window *gui_window::findchildatcoord(coord x, coord y, flags)
{
for ( q = m_subwins.getsize-1; q >= 0; q--)
{
gui_window *ww = (gui_window *)m_subwins.getat(q);
(ww)
{
gui_window *found = ww->findchildatcoord(x-m_position.getx1, y-m_position.gety1, flags);
(found) (found);
}
}

// check to see this window itself is at the coord - this s the recursion
(!getinvisible && m_position.ispoin(x,y))
(this);
(NULL);
}

GUI最大优势是独立解决方案,我称的为\"弹性对话框\".基本上,我希望我窗体和对话框根据它们运行系统屏幕设置决定它们大小.对系统更高要求是,我希望窗体,Control控件等在640 x 480屏幕上扩张或缩小.同时我也希望不管它们父窗体大小,它们都可以适合.

这就意味着我需要实现个像微软窗体虚拟坐标系统.我以个任意数据定义我虚拟坐标系统--或者说,\"从现在起,我将不管窗体实际尺寸假设每个窗体都是10000 x 10000个单元\",然后我GUI将在这套坐标下工作.对于桌面,坐标将对应显示器物理尺寸.

我通过以下 4个实现我想法:virtxtopixels,virtytopixels, pixelstovirtx, 和pixelstovirty. (注意:在代码中的列出了两个;我估计你已理解这个想法了).这些负责把虚拟10000 x 10000单元坐标要么转换为父窗体真实尺寸要么转换为显示器物理坐标.显然,显示窗体将倚重它们.

screentoclient负责取得屏幕绝对位置并将它转换为相对虚拟坐标.相对坐标从窗体左上角开始,这和3D空间想法是相同.相对坐标对对话框是必不可少.

在GUI系统中所有坐标都是相对于其他某物.唯个例外就是桌面窗体坐标是绝对.相对思路方法可以保证当父窗体移动时它子窗体也跟着移动而且可以保证当用户拖动对话框到区别位置时其结构是.同时我们整个虚拟坐标系统都是相对当用户拉伸或缩小个对话框时其中所有Control控件都会随的变化自动尽量适合新尺寸.对我们这些曾在win32中试过相同特性人来说这是个令人惊异特点.

最后findchildatcoord取得(虚拟)坐标确定哪个(如果有)子窗体在当前坐标--非常有用,比如,当鼠标单击时,我们需要知道哪个窗体处理鼠标单击事件.这个通过反向搜寻子窗体列阵(记住,最前窗体在列真最后面),看那个点在哪个窗体矩形中.标志参数提供了更多条件去判断点击是否发生;比如,当我们开始实现控制时,我们会意识到不让标示和光标Control控件响应单击是有用,取而带的应给在它们下面窗体个机会响应--如果个标示放在个按钮上面,即使用户单击标示仍表示单击按钮.标志参数控制着这些特例.



现在,我们已经有了坐标,我们可以开始绘制我们窗体了?

绘制窗体

递归是柄双刃剑.它使得绘制窗体代码很容易跟踪但是它也会造成重复绘制像素而这将严重影响性能(这就是说例如你有个存放50个相同大小相同位置窗体直跑完50个循环每个像素都会被走上50遍)这是个臭名昭著问题肯定有裁剪算法针对这种情况实际上这是个我需要花些时间领域在我自己-Quaternion\'s GUI 在非游戏屏幕过程(列标题和关闭等等)中般是激活状态要放在对GUI而言最精确位置是很蠢想法根本就没有任何其他动作在进行

但是我在对它进行修补现在我试图在我绘制思路方法中利用DirectDrawClipper对象到现在为止代码看起来很有希望下面是它工作方式:桌面窗口“清除”裁剪对象然后每个窗口绘制它子窗口先画顶端在画底端当每个窗口绘制完毕后把它屏幕矩形加入到裁剪器有效地从它的下窗口中“排除”这个区域(这假设所有窗口都是100%不透光).这有助于确保起码每个像素将被只绘制次;当然还是被所有GUI渲染所需要计算和乱糟糟(并且裁剪器可能已经满负载工作了),但是起码不会绘制多余像素.裁剪器对象运行快慢和否使得这是否值得还不明了

我也在尝试其他几个主意-也许利用3D显卡内建Z缓冲,或者某种复杂矩形创建器(dirty rectangle up).如果你有什么意见请告诉我;或者自己尝试并告诉我你发现



我剪掉了大量窗体绘制代码这些代码是这对我情况(它了我自定精灵类).旦你知晓你要绘制窗体确切屏幕维数(screen dimensions)时,实际绘制代码就能够直接被利用基本上绘制代码用了9个精灵-角落4个边缘4个背景1个-并用这些精灵绘制窗体.

色彩集需要点儿解释.我决定每个窗口有两套独特色彩集;套当窗口激活时使用,套不激活时使用.在绘制代码开始的前getappropriatecolor,这个根据窗口激活状态返回正确色彩集.具有针对激活和非激活状态区别色彩窗口是GUI设计基本规则;它也比较容易使用.


现在我们窗口已经画完了开始看看消息吧

窗口消息

节是执行GUI核心窗口消息是当用户执行特定操作(点击鼠标移动鼠标击键等等)时发送给窗口事件.某些消息(例如WM_KEYDOWN)是发给激活窗口些(WM_MOUSEMOVE)是发给鼠标移动其上窗口,还有些(WM_UPDATE)总是发给桌面.

微软Windows有个消息队列.我GUI则没有-当calcall计算出需要给窗口送消息时,它在此停下并且发送消息-它为窗口适当WM_XXXX.我发现这种思路方法对于简单GUI是合适.除非你有很好理由不要使用个太复杂消息队列在其中存储和使用线程获取和发送消息.对大多说游戏GUI而言它并不值得.

此外注意WM_XXXX都是虚.这将使C多态性为我们服务.需要改变某些形式窗口(或者Control控件,比如按钮),处理鼠标左键刚刚被按下事件?很简单,从基类派生出个类并重载它wm_lbuttondown思路方法.系统会在恰当时候自动派生类思路方法;这体现了C力量.

就我自己意愿我不能太深入calcall细节,这个得到所有输入设备并发出消息.它做很多事,并有很多对我GUI而言特定行为.例如,你或许想让你GUI像X-Window样运行,在鼠标活动范围的内窗口总是处于激活状态窗口.或者,你想要使得激活窗口成为系统模态窗口(指不可能发生其他事直到用户关闭它),就像许多基于苹果平台(Mac)那样.你会想要在窗口内任何位置点击来关闭窗口,而不是仅仅在标题栏,像winamp那样.calcall执行结果根据你想要GUI完成什么样功能而会有很大区别.

我会给你个提示,虽然-calcall不是没有状态,实际上,你calcall可能会变成个很复杂状态机(state machine).有关这例子是拖放物体.为了恰当计算普通\"鼠标键释放\"事件和相似但完全区别\"用户在拖动物体刚刚放下\"事件的间区别,calcall必须有个状态参数.如果你对有限状态机已经生疏了,那么在你执行calcall的前复习复习将会使你不那么头痛.

在窗口头文件中包括wm_xxxx是我感觉代表了个GUI要计算和发送信息最小集合.你需要可能会区别,你也不必拘泥于微软视窗消息集合;如果自定消息对你很合适,那么就自己做个.



窗口消息

在文章部分我提到了个叫做CApplication::RenderGUI,它是在计算的后绘制我们GUI:

void CApplication::RenderGUI(void)
{
// get position and button status of mouse cursor
// calculate mouse cursor\'s effects _disibledevent=>

最后,让我们开始加入些PDL(页面描述语言).

void CApplication::RenderGUI(void)
{
// get position and button status of mouse cursor
m_mouse.Refresh;

// calculate mouse cursor\'s effects _disibledevent=>
查看这些代码将会使你看到是如何开始在起工作.


在下章,第 3部分中,我们会处理对话框Control控件.按钮,文本框和进度条.

参看使用C和Directx开发GUI()


Tags:  directx使用 directx如何使用 directx怎么使用 directx开发

延伸阅读

最新评论

发表评论