本文共 5058 字,大约阅读时间需要 16 分钟。
现在,进入有趣的坐标系统。 坐标系统 /**************************************************************************** virtual coordinate system to graphics card resolution converters ****************************************************************************/ const double GUI_SCALEX = 10000.0; const double GUI_SCALEY = 10000.0; int gui_window::virtxtopixels(int virtx) { int width = (m_parent) ? m_parent->getpos().getwidth() : getscreendims().getwidth(); return((int)((double)virtx*(double)width/GUI_SCALEX)); } int gui_window::virtytopixels(int virty) { int height = (m_parent) ? m_parent->getpos().getheight() : getscreendims().getheight(); return((int)((double)virty*(double)height/GUI_SCALEY)); } /**************************************************************************** findchildatcoord: returns the top-most child window at coord (x,y); recursive. ****************************************************************************/ gui_window *gui_window::findchildatcoord(coord x, coord y, int flags) { for (int q = m_subwins.getsize()-1; q >= 0; q--) { gui_window *ww = (gui_window *)m_subwins.getat(q); if (ww) { gui_window *found = ww->findchildatcoord(x-m_position.getx1(), y-m_position.gety1(), flags); if (found) return(found); } } // check to see if this window itself is at the coord - this breaks the recursion if (!getinvisible() && m_position.ispointin(x,y)) return(this); return(NULL); } 我的GUI最大的优势是独立的解决方案,我称之为"弹性对话框".基本上,我希望我的窗体和对话框根据它们运行系统的屏幕设置决定它们的大小.对系统的更高的要求是,我希望窗体,控件等在*0 x 480的屏幕上扩张或缩小.同时我也希望不管它们父窗体的大小,它们都可以适合. 这就意味着我需要实现一个像微软窗体一样的虚拟坐标系统.我以一个任意的数据定义我的虚拟坐标系统--或者说,"从现在起,我将不管窗体的实际尺寸假设每一个窗体都是10000 x 10000个单元",然后我的GUI将在这套坐标下工作.对于桌面,坐标将对应显示器的物理尺寸. 我通过以下四个函数实现我的想法:virtxtopixels(),virtytopixels(), pixelstovirtx(), 和pixelstovirty(). (注意:在代码中之列出了两个;我估计你已理解这个想法了).这些函数负责把虚拟的10000 x 10000单元坐标要么转换为父窗体的真实尺寸要么转换为显示器的物理坐标.显然,显示窗体的函数将倚重它们. 函数screentoclient()负责取得屏幕的绝对位置并将它转换为相对的虚拟坐标.相对的坐标从窗体的左上角开始,这和3D空间的想法是相同的.相对坐标对对话框是必不可少的. 在GUI系统中所有的坐标都是相对于其他的某物的.唯一的一个例外就是桌面窗体,它的坐标是绝对的.相对的方法可以保证当父窗体移动时它的子窗体也跟着移动,而且可以保证当用户拖动对话框到不同位置时其结构是一致的.同时,因为我们整个虚拟坐标系统都是相对的,当用户拉伸或缩小一个对话框时其中的所有控件都会随之变化,自动的尽量适合新的尺寸.对我们这些曾在win32中试过相同特性的人来说,这是个令人惊异的特点. 最后,函数findchildatcoord()取得(虚拟)坐标确定哪个(如果有)子窗体在当前坐标--非常有用,比如,当鼠标单击时,我们需要知道哪个窗体处理鼠标单击事件.这个函数通过反向搜寻子窗体列阵(记住,最前的窗体在列真的最后面),看那个点在哪个窗体的矩形中.标志参数提供了更多的条件去判断点击是否发生;比如,当我们开始实现控制时,我们会意识到不让标示和光标控件响应单击是有用的,取而带之应给在它们下面的窗体一个机会响应--如果一个标示放在一个按钮上面,即使用户单击标示仍表示单击按钮.标志参数控制着这些特例. 现在,我们已经有了坐标,我们可以开始绘制我们的窗体了? 绘制窗体 递归是一柄双刃剑.它使得绘制窗体的代码很容易跟踪,但是它也会造成重复绘制像素,而这将严重的影响性能。(这就是说,例如你有一个存放50个相同大小相同位置的窗体,程序会一直跑完50个循环,每个像素都会被走上50遍)。这是个臭名昭著的问题。肯定有裁剪算法针对这种情况,实际上,这是个我需要花些时间的领域。在我自己的程序-Quaternion's GUI 在非游戏屏幕过程(列标题和关闭等等)中一般是激活状态的,要放在对GUI而言最精确的位置是很蠢的想法,因为根本就没有任何其他的动作在进行。 但是,我在对它进行修补。现在我试图在我的绘制方法中利用DirectDrawClipper对象。到现在为止,初始的代码看起来很有希望。下面是它的工作方式:桌面窗口“清除”裁剪对象。然后每个窗口绘制它的子窗口,先画顶端的,在画底端的。当每个窗口绘制完毕后,把它的屏幕矩形加入到裁剪器,有效地从它之下的窗口中“排除”这个区域(这假设所有的窗口都是100%不透光的).这有助于确保起码每个像素将被只绘制一次;当然,程序还是被所有的GUI渲染所需要的计算和调用搞的乱糟糟的,(并且裁剪器可能已经满负载工作了),但是起码程序不会绘制多余的像素.裁剪器对象运行的快慢与否使得这是否值得还不明了。 我也在尝试其他的几个主意-也许利用3D显卡的内建Z缓冲,或者某种复杂的矩形创建器(dirty rectangle setup).如果你有什么意见,请告诉我;或者自己尝试并告诉我你的发现。 我剪掉了大量的窗体绘制代码,因为这些代码是这对我的情况的(它调用了我自定的精灵类).一旦你知晓你要绘制窗体的确切的屏幕维数(screen dimensions)时,实际的绘制代码就能够直接被利用。基本上,我的绘制代码用了9个精灵-角落4个,边缘4个,背景1个-并用这些精灵绘制窗体. 色彩集需要一点儿解释.我决定每个窗口有两套独特的色彩集;一套当窗口激活时使用,一套不激活时使用.在绘制代码开始之前,调用getappropriatecolorset(),这个函数根据窗口的激活状态返回正确的色彩集.具有针对激活和非激活状态的不同色彩的窗口是GUI设计的基本规则;它也比较容易使用. 现在我们的窗口已经画完了,开始看看消息吧。 窗口消息 这一节是执行GUI的核心。窗口消息是当用户执行特定操作(点击鼠标,移动鼠标,击键等等)时发送给窗口的事件.某些消息(例如WM_KEYDOWN)是发给激活窗口的,一些(WM_MOUSEMOVE)是发给鼠标移动其上的窗口,还有一些(WM_UPDATE)总是发给桌面的. 微软的Windows有个消息队列.我的GUI则没有-当calcall()计算出需要给窗口送消息时,它在此停下并且发送消息-它为窗口调用适当的WM_XXXX()虚函数.我发现这种方法对于简单的GUI是合适的.除非你有很好的理由,不要使用一个太复杂的消息队列,在其中存储和使用线程获取和发送消息.对大多说的游戏GUI而言,它并不值得. 此外,注意WM_XXXX()都是虚函数.这将使C++的多态性为我们服务.需要改变某些形式的窗口(或者控件,比如按钮),处理鼠标左键刚刚被按下的事件?很简单,从基类派生出一个类并重载它的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 on windows / send messages // render all windows // render mouse // flip to screen } 最后,让我们开始加入一些PDL(页面描述语言). void CApplication::RenderGUI(void) { // get position and button status of mouse cursor m_mouse.Refresh(); // calculate mouse cursor's effects on windows / send messages GetDesktop()->calcall(); // render all windows GetDesktop()->renderall(); // render mouse m_mouse.Render(); // flip to screen GetBackBuffer()->Flip(); } 查看这些代码将会使你看到程序是如何开始在一起工作的. 在下一章,第三部分中,我们会处理对话框控件.按钮,文本框和进度条. 转载地址:http://qsdeo.baihongyu.com/