看起来我在做发明车轮的浪费时间的事-- windows已经有一套非常复杂功能完善的GUI.不幸的是windows的GUI适用于办公软件,而这通常不适合游戏软件.游戏一般需要比 windows更精确的控制(例如,使用自己的GUI实现用alpha混合创造的部分透明的窗口比较容易,而使用windows的GUI则几乎做不到).

这篇文章将讲述如何使用C++和DirectX来创建自己的GUI.文章分为几部分,每部分涉及一个特定的GUI编程主题.这并不需要按顺序阅读,所以你可以从你需要的部分开始.

这里我假设读者有以下的背景知识:事件驱动的编程原理;对PDL和C++的熟练掌握.我使用C++来建立我的GUI系统,因为我是C++爱好者俱乐部的持卡会员,并且C++的OOP很适用于windows编程和控制方式.

让我们从定义工作范围开始. 应该认识到我们不是要设计windows95,我们只是想要为一个游戏开发一套简单的GUI.所以我们不用实现每一个简单控制和GUI结构.我们只需要几 个部分而已:一个鼠标指针,一个普通窗口,一些在窗口中的对话框控制.我们也需要一个资源编辑器--一个允许我们在图形环境中使用拖动控制来设计对话框.

从基础开始:渲染循环

我将通过定义一个计算并绘制框线的函数开始.我们在RenderGUI()函数中调用这个函数.RenderGUI用PDL描述如下:

void CApplication::RenderGUI(void)
{

  
// get position and button status of mouse cursor
  
// calculate mouse cursor抯 effects on windows / send messages
  
// render all windows
  
// render mouse
  
// flip to screen
}

我们得到新的位置和鼠标光标的状态,计算由新的数据引起的任何改变,渲染所有的窗口,鼠标指针并在屏幕上绘制.

鼠标

class CMouse {
public:

 
CMouse(); // boring
 
~CMouse(); // boring

 
int Init(LPDIRECTINPUT di); // we抣l talk about this later
 
int Refresh(void); // we抣l talk about this later

 
int GetButton(int index)
 
{
   
if (index < 0 || index > NUMMOUSEBUTTONS) return(0);
   
return(m_button[index]);
 
}

 
void clear(void); // sets all vars to zero 

  // makes sure p is a valid on-screen point
 
void ConstrainPosToScreenSize(CPoint &p);
 
CPoint GetAbsPosition(void) { return(m_absposition); }
 
CPoint GetRelPosition(void) { return(m_relposition); }
 
enum { NUMMOUSEBUTTONS = 3 }; // three button mouse

private:
 
LPDIRECTINPUTDEVICE m_mousedev;
 
char m_button[NUMMOUSEBUTTONS]; // state of buttons
 
CPoint m_absposition; // actual screen position
 
CPoint m_relposition; // relative position

};

绝对位置和相对位置,DirectInput

为什么我要使用DirectInput? 这实际是一个口味问题.有两种方法可得到windows的鼠标信息,从DirectInput中或通过一条叫做GetCursorPos()的Win32 函数.主要的区别在于DirectInput提供的是相对坐标,即相对于上次位置的当前位置;而GetCursorPos()将提供屏幕坐标系的绝对坐 标.绝对坐标对GUI很有用;而相对坐标则适合于没有光标的鼠标,例如在FPS游戏中的环视(译者:即光标位置固定的情况).然而你可以由绝对坐标计算出 相对坐标,反之亦然.

由于很多原因我选择了DirectInput,所有这些原因超出了本文的范围.对你来说GetCursorPos()可能是个更好的选择?如果是这样,鼠标类可能就会非常庞大.DirectInput更加技巧化(也更有趣),所以本文将集中于DirectInput.

初始化DirectInput

在我们详述CMouse 之前,让我们看看如何初始化DirectInput.注意这些代码并不属于我们的CMouse::Init()函数;DirectInput的指针在整个 游戏中都会使用,而并不仅仅在鼠标类中,所以DirectInput的初始化应在主初始化函数中和DirectDraw,DirectSound的初始化 同时完成.DirectInput指针和DirectInput device指针不同;DirectInput指针用以得到DirectInput device指针.注意理解这一区别,下面是初始化主DirectInput接口指针的代码

LPDIRECTINPUT di = NULL;

hr = DirectInputCreate(hinst, DIRECTINPUT_VERSION, &di, NULL);

if (FAILED(hr)) {
 
// error
 
handle_error ();
 
}

// Now that we抳e got a DirectInput interface, let抯 begin
// fleshing out our CMouse by implementing CMouse::Init(). 

bool CMouse::Init(LPDIRECTINPUT di)
{

 
// Obtain an interface to the system mouse device.
 
hr = di->CreateDevice(GUID_SysMouse, (LPDIRECTINPUTDEVICE*)&m_mousedev, NULL); 

  if (FAILED(hr)) { /* handle errors! */ }
 
//  Set the data format to "mouse format".
 
hr = m_mousedev->SetDataFormat(&c_dfDIMouse);
 
if (FAILED(hr)) { /* handle errors! */ }

 
// Set the cooperativity level
 
hr = m_mousedev->SetCooperativeLevel(hwnd, DISCL_NONEXCLUSIVE | DISCL_FOREGROUND);
 
if (FAILED(hr)) { /* handle errors! */ }
}

这段代码作了三件重要工作. 第一,得到一个有效的DirectInput鼠标设备接口,并把他付给di_mouse.第二,设定数据格式和设备的协作度,仅仅让windows知道我 们想查询鼠标设备,而不是要独占该设备.(独占意味着只有我们的应用程序可以使用鼠标,通过指定DISCL_NONEXCLUSIVE,我们通知 windows我们将和其他程序共享鼠标)

通过DirectInput检测鼠标状态

现在让我们关注CMouse::Refresh().该函数负责更新CMouse的内部键状态和位置.下面是代码:

void CMouse::Refresh(void)
{

 
char done = 0;
 
int q;
 
HRESULT hr;
 
CPoint p;
 
DIMOUSESTATE dims; 

  // clear our struct ?eventually, directinput will fill this in
 
memset(&dims, 0, sizeof(DIMOUSESTATE));

 
if (!m_mousedev) return; // we don抰 have a pointer! Bail!

  while (!done)
  {
    // query DirectInput for newest mouse data
    hr = m_mousedev->GetDeviceState(sizeof(DIMOUSESTATE), &dims);
    if (FAILED(hr))
    {
      if (hr == DIERR_INPUTLOST || hr == DIERR_NOTACQUIRED)
      {
        // device lost... reacquire
        hr = m_mousedev->Acquire();
        if (FAILED(hr))
        {
          // error handling goes here
          clear();
          done=1;
        }
      }
      else
      {
          // it抯 some other error ?handle it
          clear();
          done = 1;
      }
    }
    else // read mouse successfully!
    {
      done = 1;
    }
  } // end while loop ?we抳e read DI correctly

  // squirrel away newest rel position data
  m_relposition.x = dims.lX;
  m_relposition.y = dims.lY;
  m_relposition.z = dims.lZ;

  // now calc abs position from new relative data
  m_absposition.z += dims.lZ;
  m_absposition.x += dims.lX;
  m_absposition.y += dims.lY; 

  // keep the mouse pointer on-screen...
  ConstrainPosToScreenSize(m_absposition);

  // get button data
  for (q=0; q < NUMMOUSEBUTTONS; q++)
  {
    m_button[q] = (dims.rgbButtons[q] & 0x80));
  }

}

这段代码作了许多工作. 首先,通过DirectInput得到新的绝对鼠标位置(使用while循环来自动重新检测).其次,把绝对位置数据付给m_absposition,然 后得到新的相对位置.ConstrainPosToScreenSize()函数确保鼠标所在点在屏幕范围内.最后,通过循环更新所有的键.

绘制鼠标

绘制鼠标光标有两个主要的原则. 如果你知道每一帧整个屏幕都会用新的像素数据刷新,你可以简单地blt鼠标光标到新的绝对位置,并由此完成绘制.然而,更好的方法是在你blt之前,得到 鼠标光标下的像素数据得一份拷贝,然后,当鼠标移动时,通过blt保存过的像素数据从而擦除旧的blt.我更倾向于后一种方法.

本文将不会讨论具体的blt表面及其他,你应该懂得如何实现.

线程和尾迹

如果你不介意多线程,有一种更好的方法来解决鼠标问题.这里的方法是单一线程的,每一帧都会查询鼠标.

适用于高刷新率,而不是低刷新率.鼠标光标会显得很迟缓.最好的方法是建立独立的mouse-rendering线程,它会不断地检测MOUSEMOVE消息,并当鼠标移动时刷新和blt鼠标光标.多线程的优势在于不管你的游戏刷新率

是多少,鼠标光标都会很流畅.有一个单独的鼠标线程将使游戏在任何帧频率下都会相应很快.

建立鼠标轨迹对你显然是件容易的事.在一个数组中保存最后几个鼠标光标的位置.当鼠标移动时,丢弃最旧的坐标,把所有其他的坐标值向下移动一个栏位,并把新的坐标存入顶端的栏位.然后,如果你想要额外的效果.使用alpha混合,并以比新坐标更高的透明度渲染旧的坐标.

期待...

现在我们结束了鼠标光标的工作.下一节将讲述如何创建基本的窗体,及如何移动窗体.敬请期待.

posted on 2006-04-22 23:03 我不是神 阅读(77) 评论(4)  编辑 收藏

评论

# re: 使用C++和Directx开发GUI 2006-04-22 23:04 我不是神

使用C++和Directx开发GUI(二)



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

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

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

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

计划

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

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

2.一旦我有了基础的窗体管理函数,我花了一些时间思考我的坐标系统.写了一些坐标管理函数.

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

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

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

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

实现

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

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

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

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

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

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

void setpos(coord x1, coord y1); // boring
void setsize(coord width, coord height); // boring

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

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

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

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

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

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

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

int calcall(void);

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

protected:

virtual void copy(gui_window &r); // deep copies one window to another

gui_window *m_pParent;
uti_pointerarray m_subwins;
uti_rectangle m_position;

// active and inactive colorsets
gui_wincolor m_activecolor;
gui_wincolor m_inactivecolor;

// window caption
uti_string m_caption;
};


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


窗体管理

/****************************************************************************
addwindow: adds a window to this window's subwin array
****************************************************************************/
int gui_window::addwindow(gui_window *w)
{
if (!w) return(-1);
// only add it if it isn't already in our window list.
if (m_subwins.find(w) == -1) m_subwins.add(w);
w->setparent(this);
return(0);
}

/****************************************************************************
removewindow: removes a window from this window's subwin array
****************************************************************************/
int gui_window::removewindow(gui_window *w)
{
w->setparent(NULL);
return(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 array.
****************************************************************************/
void gui_window::bringtotop(void)
{
if (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: returns true if this window is the active one (the one with input focus).
****************************************************************************/
bool gui_window::isactive(void)
{
if (!m_parent) return(1);
if (!m_parent->isactive()) return(0);
return(this == m_parent->m_subwins.getat(m_parent->m_subwins.getsize()-1));
}

这一系列函数是处理我所说的窗体管理:新建窗体,删除窗体,显示/隐藏窗体,改变它们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个窗体中将第二个窗体移到最前;我将为了移动二 号窗体而移动48个窗体.但信运的是,移动窗体到Z坐标最前不是最耗时的函数,即使是,也有很多好的快的方法可以处理,比如链表即可.

看看我在bringtotop()函数中的小技巧.因为我知道窗体不拥有指针,我就删除这个窗体又马上创建一个,非常有效率的将它重定位在数列最 前.我这样做是因为我的指针类,uti_pointerarray,已经被编写好了一旦删除一个元素,所有的更高的元素将向后移动.

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

坐标系统

/****************************************************************************
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最大的优势是独立的解决方案,我称之为"弹性对话框".基本上,我希望我的窗体和对话框根据它们运行系统的屏幕设置决定它们的大小.对 系统的更高的要求是,我希望窗体,控件等在640 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();
}

查看这些代码将会使你看到程序是如何开始在一起工作的.


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

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



# re: 使用C++和Directx开发GUI 2006-04-22 23:05 我不是神


使用C++和Directx开发GUI(三)



欢迎回到"使用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种不同的控件.你要在功能、好事、复杂性、坏事中做出平衡.

控件就到这吧.下一章也是最后一章中我们会看看资源编辑器,序列化窗口和创建对话框.祝愉快!

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



# re: 使用C++和Directx开发GUI 2006-04-22 23:06 我不是神


使用C++和DirectX开发GUI - 资源编辑器及其它



  欢迎回到“使用C++和DX开发GUI”的第四部分。接着我们的主题(我是如何为我未来的游戏开发GUI的 — Quaternion),本文将关注所有的有关游戏GUI的细节问题。

4.1、保存窗口
  窗口序列化(存储和载入窗口)对你的工程而言或许不重要。如果你的游戏GUI很简单,你可以全靠程序在游戏中实现窗口。但如果你的GUI相对 复杂,或者随着开发的过程经常会改变,那么你会想写程序以把一个窗口(和所有它的子窗口)存到文件里,然后再装载它。对初学者而言,窗口序列化代码允许你 改变游戏的GUI而不用重新编译,而且它对于多人协调工作也是有益的。
  我的计划是从主对话窗口开始,然后遍历它的所有子窗口,把每一个存到磁盘上。如果我用C语言写程序的话,我告诉自己的第一句话是“好的,如 果我必须保存这些窗口,我需要每个窗口有一个字节来告诉我它是什么样的窗口,以使我能正确的载入它。比如1是按钮,2是列表框,3是图表,等等。”
  这种问题是C++的RTTI(运行期类型识别)关心的。RTTI提供了两个东东,type_info类和typeid()函数,两者允许我 查询一个对象的所属类名称,例如gui_window,gui_button等等。我没有使用枚举和ID,而是简单地为每个将要保存的窗口调用typid (),并“写下”窗口的类名称。
  我注意到使用RTTI的对象识别函数来帮助保存窗口的两个小缺点。第一,RTTI ID是字符串而不是整型,这意味着它们将占用磁盘上的更多空间(按照Pascal方式存储字串将是前4个字节代表字串的长度,接下来是字串本身的数据)。 第二,如果你改变一个窗口类的名字,你会破坏已经存好的所有窗口文件。
  由于这些原因,你可能不会这样使用RTTI。毕竟,并不是有技术就一定要使用它。然而,我发现RTTI对我的代码而言却是救生员。要获得更多的关于RTTI和这两个函数的信息,请在你的联机帮助文档里查找。
  另外,如果你决定在VC++里使用RTTI,确保你在工程属性的C/C++栏和C++语言选项中打开它。

4.2、载入窗口
  载入窗口比存储他们要难一点儿,这主要是因为你必须新建(new)每个窗口,载入,并当它不再需要的时候删除。
  这是个递归函数,用PDL表达的话如下所示:

  void gui_window:load(int filehandle)
  {
    // read window properties (colorsets, etc.)
    // read total number of children for this window
    // for each child?
      // read window ID from disk
      // new a gui_window derivative based on that ID
      // tell the newly created window to load itself (recurse!)
    // next child
  }

  换句话说,你得像你所想得那样从磁盘载入窗口。第一,要处理基类窗口:读取他的属性。然后,读取基类窗口的所有子窗口的数目。对每个子窗口,读取ID字节,根据ID新建一个窗口,然后让新窗口载入自己(低轨到它)。当所有的字窗口载入完毕时,就结束了。
  当然,你的文件结构一致也非常重要。确保你的存储代码以你想要载入的顺序存储信息。

4.3、资源编辑器
  要想真正使你的GUI大放光彩,你必须有一个资源编辑器。当然你不需要做个象开发环境提供的资源编辑器那样的华丽和强大,但是你起码得有个基本的程序来完成加入,编辑,删除,排列,免除你为对话框的每个控件计算虚拟坐标位置的麻烦事儿。
  写一个功能完善的,所见即所得(WYSIWYG)的资源编辑器超出了本文的范围,但是我能给你一些小提示来帮助你完成这个壮举:

共享你的代码。特别地,让你的资源编辑器和你的游戏一起分享同样的渲染代码。这样,你就得到了所见即所得支持,并且免除了开发两套GUI代码的麻 烦,我向你保证,调整你的DirectX代码以使它渲染一个GDI表面而不是一个双缓冲系统将比重新开发一整套新的绘制代码简单。记住过一段时间之后你的 GUI系统可能会改变,你不会想要经常在两个不同的地方改写代码。


不要试图模仿开发环境的外表和感觉。换句话说,不要费时间模仿开发环境的细节(例如,属性页和预览窗口)。如果你的编辑器相对而言比较难看的话, 不要沮丧;的确,小组的效率是和他使用的工具的效能成直接正比关系的,但是同时,小组之外的人是不可能使用你的资源编辑器的,你也不会用它去开发一个完整 的GUI程序;你不过只是做几个对话框而已。你不需要环境文本帮助(context sensitive help)。你不需要环境文本菜单(context menus),除非你觉得这会简化一个特定的繁复的操作。如果你的资源编辑器不那么漂亮也无所谓,只要它能完成工作就行了。


强调数据完整而不是速度。资源编辑器是个数据整合者,而不是个高性能程序,没有什么比你花了一个小时来设计的东西由于程序错误而丢失而更让人恼火 的事儿了。当写你的GUI的时候,保存数据是你的最高目标。花些时间来做自动存储,释放缓冲区(autosaves, undo buffers)等等,别把优化看得那么重要。
4.4、生成子类(subclass)
  那些熟悉Win32处理窗口的人们可能已经知道“subclass”这个术语的含义了。如果不知道的话,当你“subclass”一个窗口的时候,你就“衍生(derive)”一个新的窗口类型,然后把新的窗口类型嵌入到旧窗口要使用的地方。
  让我做更详细地解释。比如我们需要一个超级列表框。我们已经有个普通的列表框类,但是因为某种原因它不适合;我们的游戏需要超级列表框。所以我们从普通的列表框类中衍生出一个超级列表框类。就是这样。
  但是我们如何在我们的游戏对话框中放置这个超级列表框呢?由于超级列表框是为我们的程序特制的,我们不能为我们的资源编辑器增加函数来支持 它。但同时,我们怎样通知GUI系统为这个特殊的实例(我们的游戏),让所有的列表框都是超级列表框呢?这就是生成子类(subclass)要做的事情。 这不是个精确的技术定义,但是表达了足够的信息。
  这里我要讲述的方法称作“载入过程生成子类(subclassing at load time)”。要理解它,让我们从上一节所述的基本的载入代码开始。我们有个载入函数,它第归地完成创建,载入,增加窗口。这里我们用PDL表述如下:

  // read total number of children for this window
  // for each child...
    // read window ID from disk
    // new a gui_window derivative based on that ID
    // ...
  // next child

  要完成生成子类,我让我的窗口载入函数“给程序一个创建这一类型窗口的机会“,像这样:

  // read total number of children for this window
  // for each child...
    // read window ID from disk
    // give application a chance to create a window of this type
    // if the application didn't create a window,
      // then new a gui_window derivative based on the ID
    // else, use the application's created window
    // ...
  // next child

  我通过一个函数指针给程序这个机会。如果程序需要为一个窗口生成子类,就在函数指针里填上自己函数的地址。当窗口载入的过程中,调用这个函 数,传入想要创建的窗口ID。如果程序想要根据ID为一个窗口生成子类,新建一个适当的对象并把新指针返回给窗口。如果程序不需要这个ID,则返回 NULL,窗口函数根据返回值创建恰当的默认对象。这种方法允许程序“预先过滤”引入的窗口ID信息,并为特定的窗口类型重载默认函数。太完美了(译者: 这一段我翻译得实在不能说是完美,相反是简直不知所云,这里把原文贴出来,请您自己斟酌吧)。
  Specifically, I give the application this chance by way of a function pointer. If the application needs to subclass a window, it fills in the function pointer with the address of its own function. When the windows are loading, they call this application function, passing in the ID of the window they want to create. If the application wants to subclass a window from this ID, it news up the appropriate object and returns the new pointer back to the window. If the app doesn't want to do anything special for this ID, it returns NULL, and the window function senses this and news up the appropriate default object. This method allows the app to "pre-filter" the incoming window ID bytes, and to override the default behavior for certain window types. Perfect!

  用这种方法在创建自定控件的时候给了我很大的自由。我为我的资源编辑器增加代码以使我能为每个存储的窗口改变ID。然后,当我需要自定控间的时候,我只需用资源编辑器改变保存这个窗口ID的字节。在磁盘上保存的是ID和所有为自定控件的其他基类属性。
  很快吧?还有其他的方法来做同样的事,这是在模仿STL需要创建对象时使用的方法。STL使用特定的“allocator(分配符)”类,这有点儿像“类厂”(factories),它们按照客户告诉他们的需要来创建类。你可以使用这种方法来创建窗口。
  这种方法的工作原理如下:创建一个类并把它叫做“gui_window_allocator”。写一个虚拟函数,叫做 CreateWindowOfType,它接受一个给定的窗口ID并传出一个新的指针给窗口。现在你就得到了一个简单的分配符类,你的窗口载入代码将使用 它来新建需要的窗口。
  现在,当你的程序需要为窗口重载“new”操作符,衍生一个新的、程序相关的gui_window_allocator类,并告诉你的窗口载入代码来使用这个分配符,而不是默认的那个。这种方法就象提供一个函数指针,只用了一点儿C++。

4.5、加速GUI渲染
  还有一个能帮助你加速GUI渲染的小提示。
  关键的概念是,就象其它绘制函数的优化一样,不要画你不需要的东西。默认方式,GUI花很多时间绘制没有变化的部分。然而,你能通过告诉 GUI绘制改变的窗口(windows that are dirty)作些优化。当窗口的外观需要改变的时候,窗口设定它们的dirty标志,在绘制的时候清除它们的dirty标志。
  由于我们的GUI控件可能是透明的,当一个控件被标记为dirty,它的父窗口必须也被标记为dirty。这样,当它被绘制的时候,背景没有改变,因为父窗口也刚刚被重绘。

4.6、总结:相逢在XGDC
  呼,好长的4篇文章,然而我们还是遗漏了很多。
  我准备在即将来临的Armageddon XGDC(eXtreme Game Developer's Conference)上讲述GUI。我将尽力让我的发言有用,现在你已经看了所有的文章,如果你有什么需要扩展的问题,或是我有什么遗漏,请给我写信让 我了解。祝各位做游戏愉快。


--------------------------------------------------------------------------------

  作者简介:
  Mason McCuskey先生是Spin Studio(一个正在制作名为Quaternion的伟大游戏的制作小组)的领导者.他盼望着你的建议和评论,他的电子邮件是(mason@spin-studios.com)。

  译者手记:
  终于结束了漫长的翻译过程,这是Rick翻译过的最长的也是难度最大的技术文章了。其中一定有不少的错误和遗漏,如果您发现了这些问题,请来信告诉我(rick_yinhua@sina.com)。谢谢您的支持。