《DirectX9 User Interfaces Design and Implementation》第七章的译文

7.1 什么是用户界面库(UI LIB)?

程序员总是喜欢捷径,没有人希望做重新发明车轮的事。在开发程序的时候,我们总是想法设法的包含各式各样的库,通过那些事先写好的函数来完成我们的工作。例如,文件读写函数或者printf和scanf例程允许我们完成不同的任务而不需要学习硬件的细节。因此,使用库可以节省我们的开发时间并且使我们的软件兼容性更好。我们在第二部分开发的UI LIB同样会为界面开发人员提供这样的好处。最终,它将包含一组类和函数来帮助我们在短时间内开发出一流的界面。对于那些使用我们库的开发者来说,他们只需要简单的在工程中添加#include <UILIB.h>和适当的lib文件就可以获得全部的功能。下面让我仔细看看UI LIB由什么组成。

7.2 像类一样的控件

在第一章中我们曾解释过如何用一组控件制作界面,像按钮、列表框、文本框和复选按钮等,以及用户和程序是如何通过控件通信的。因此,UI LIB也将会是一组控件的集合。

7.3 控件——类的层次和基础控件

开发UI LIB从哪里开始最好呢?我们应该从开发一个按钮、文本框或者一个下拉列表开始吗?或者有什么基本的开发结构是我们必须遵守的?实际上,最好的开始是问问自己什么是控件。只有这么做,我们才能知道什么是所有控件的共同属性。实际上,只要它包含这些属性它就可以被认为是一个控件了。随着章节的进程我们将逐个检测这些属性。根据类的特点,阶段性的开发这些控件意义重大,我们将从一个基础类或者基础控件开始。它仅包含了所有控件的基本属性,什么也不多。其他的控件,诸如按钮和标签,都将从它派生出来。这样我们就不必为每一个控件单独编写相同的功能了。我们把这个基础类命名为CXControl,我们将用两章讲解它。图7.1展示了UI LIB的层次结构。

图7.1

注意
我用了两章讲解CXControl,因为它是一个那样庞大并且重要的概念。我为这个类添加了大量的功能,以便定制那些派生类的工作简单并且迅速。

7.4 CXControl——旅行的开始

在UI LIB中CXControl作为一个基类出现,其他的类皆从CXControl派生而来。作为其他类的祖先,CXControl为它们提供了一组共有的特征。本章致力于CXControl的开发,后面的部分研究了什么才是所有控件的共有属性以及如何在CXControl中实现它们。开发将从一个空白的类的声明开始,随着研究的深入,我们将会慢慢地为它填充内容。

7.5 定义CXControl——控件和画布

图7.2

图7.2 一张空白的画布

不同的控件之间有着明显的区别,列表框是一个外观,按钮则是另外一个样子的。但是所有的控件都表现为其父控件边界内的一块矩形区域,控件在这个区域内绘制自己。在术语中,这块绘制图像的矩形区域被称为画布(canvas)。实际上,它就是像表面(surface)或纹理(texture)那样的一组像素。它的大小用宽(width)和高(height)表示,显示状态分为可见和不可见。下面的代码展示了canvas是如何实现的。

class CXControl
...{
protected:
    DWORD m_Width;        //画布的宽
    DWORD m_Height;       //画布的高
bool m_Visible;       //画布是否可视
    CXTexture * m_Canvas; //指向画布的指针
    CXPen * m_Pen;        //一些要画在画布上的东西
public:
    CXTexture * GetCanvas(void) ...{return m_Canvas;}
void SetCanvas(CXTexture * Texture) ...{m_Canvas = Texture;}
bool GetVisible(void) ...{return m_Visible;}
void SetVisible(bool Visible) ...{m_Visible = Visible;}
    CXPen * GetPen(void) ...{return m_Pen;}
void SetPen(CXPen * Pen) ...{m_Pen = Pen;}
    DWORD GetWidth(void) ...{return m_Width;}
    DWORD GetHeight(void) ...{return m_Height;}
void SetWidth(DWORD Width) ...{m_Width = Width;}
void SetHeight(DWORD Height) ...{m_Height = Height;}
};

注:绘制的细节将在下一章研究消息和事件响应的时候介绍。另外,像media player使用的那种非矩形控件不在本书的讨论范围内。

7.6 父控件、兄弟控件、子控件

第一章曾简要地提到过界面中各种控件的层次关系。因此,控件之间是密切相关的。例如,除了桌面之外这些界面中顶层的控件是没有父控件的。通常,这类控件用作应用程序的主窗口,它包含着按钮、复选按钮之类的其他控件。这些控件是一个窗口的孩子,是彼此的兄弟,而这个窗口就是它们的父亲。实际上,这种层次关系对控件来说是最重要的影响之一,在应用程序创建和销毁它们的时候就决定了。看图7.3来想象一下这种层次关系。

图7.3

图7.3

在先前的章节中,我们研究过如何使用链表来有效地管理鼠标指针列表。现在我们将使用一个改进了的方法来处理控件之间的关系。还记得吗,链表就是一个线性的项的列表,其中每一项都有一个指针指向它的下一项,但最后一项是个例外,它的指向为空(NULL)。这是存储像控件的孩子那样的项的理想方式,但缺点是你只能在链表上沿着一个方向移动。虽然不是什么大问题,但这是多么的不方便和不切实际啊。解决这个问题的办法就是使用双向链表。这样,每个控件都维持了指向前后兄弟的指针,换句话说就是用两个指针分别指向链表中此控件的前一控件和后一控件。对于开发者来说,这样的安排有几个好处:一、你可以在此列表上双向移动,从任意一点开始到任意一点结束;二、你可以删除任意的项,然后将缺口修补好;三、你可以完成所有形式的排序以及重新整理项的操作。看图7.4理解双向链表的概念。

图7.4

图7.4

因此,使用双向链表来实现控件之间的关系是不错的选择。要为CXControl添加这样一个列表来操纵它的孩子只需要简单地添加几个不同的指针:一个指向父控件,一个指向前后兄弟控件(译者:其实就是两个),一个指向第一个子控件。为了管理这些指针,我们还得添加几个函数,这包括添加子控件的函数、通过兄弟列表操纵的函数和删除子控件的函数。这些在后面的小节中都有讲解。先看一下修改过的CXControl类的声明。

class CXControl
...{
protected:
    DWORD m_Width;
    DWORD m_Height;
bool m_Visible;
    CXTexture * m_Canvas;
    CXPen * m_Pen;
    CXControl * m_ChildControls;
    CXControl * m_NextSibling;
    CXControl * m_PreviousSibling;
    CXControl * m_Parent;
public:
// Accessors
    CXTexture * GetCanvas(void) ...{return m_Canvas;}
void SetCanvas(CXTexture * Texture) ...{m_Canvas = Texture;}
bool GetVisible(void) ...{return m_Visible;}
void SetVisible(bool Visible) ...{m_Visible = Visible;}
    CXPen * GetPen(void) ...{return m_Pen;}
void SetPen(CXPen * Pen) ...{m_Pen = Pen;}
    DWORD GetWidth(void) ...{return m_Width;}
    DWORD GetHeight(void) ...{return m_Height;}
void SetWidth(DWORD Width) ...{m_Width = Width;}
void SetHeight(DWORD Height) ...{m_Height = Height;}
    CXControl * GetParentControl(void) ...{return m_Parent;}
void SetParentControl(CXControl * Control) ...{m_Parent = Control;}
    CXControl * GetNextSibling(void) ...{return m_NextSibling;}
void SetNextSibling(CXControl * Control) ...{m_NextSibling = Control;}
    CXControl * GetPreviousSibling(void) ...{return m_PreviousSibling;}
void SetPreviousSibling(CXControl * Control) ...{m_PreviousSibling = Control;}
    CXControl * GetFirstChild(void) ...{return m_ChildControls;}
void SetFirstChild(CXControl * Control) ...{m_ChildControls = Control;}

    CXControl * AddChildControl(CXControl * Control);
    CXControl * RemoveChildControl(CXControl * Control);
void RemoveAllChildren();
int GetChildCount();
};

7.6.1 添加子控件

控件通过m_ChildControls指针存储其子控件的信息。如果要把一个已存在的控件变成另外一个控件的孩子,你需要调用CXControl的AddChildControl方法。看看这个函数的定义,是不是有点眼熟?

注意,这个函数与前一章把光标添加到链表的函数稍有不同。这里我们创建的是一个双向链表,因此,除了后一个兄弟控件之外,前一个兄弟控件也需要设置。这样我们才能双向的操纵这个列表。

CXControl * CSControl::AddChildControl(CXControl * Control)
...{
    Control->SetParentControl(this);
    CXPen * Pen = Control->GetPen();
    SAFE_DELETE(Pen);
    Control->SetPen(this->GetPen());

if(!m_ChildControls)
        m_ChildControls = Control;
else
...{
        CXControl * Temp = this->GetFirstChild();

while(Temp->GetNextSibling())
            Temp = Temp->GetNextSibling();

        Temp->SetNextSibling(Control);
        Control->SetPreviousSibling(Temp);
    }

return Control;
}

7.6.2 清除子控件

清除子控件就是将它们全部删除的过程。要达到这个目的,调用CXControl的RemoveAllChildren方法就可以了。在前一章,我们展示过一个类似的过程,请看下面的函数定义。

void CXControl::RemoveAllChildren()
...{
    CXControl * Temp = this->GetFirstChild();

while(Temp)
...{
        CXControl * Next = Temp->GetNextSibling();
        SAFE_DELETE(Temp);
        Temp = Next;
    }
}

7.6.3 删除指定的子控件

图7.5

图7.5

在前一章我们没有见到过如何删除列表中任意位置的项,而双向链表使这个过程变得简单了。例如,我们想要删除项目I,只要完成一下步骤:用N指向I的后一个兄弟控件,P指向I的前一个兄弟控件,然后删除I,最后将P的后一个兄弟指向N。看图和下面的定义你可以很快理解它。

CXControl * CXControl::RemoveChildControl(CXControl * Control)
...{
    CXControl * Next = Control->GetNextSibling();
    CXControl * Previous = Control->GetPreviousSibling();
    SAFE_DELETE(Control);
    Next->SetPreviousSibling(Previous);
    Control = Next;
return Control;
}

7.6.4 统计子控件的数量

有时候如果能知道指定的控件有多少子控件会很有帮助。计算它们很简单。只要遍历它的子控件并增加计数器就可以了。你可以调用CXControl的GetChildCount来完成此功能,函数定义如下:

int CXControl::GetChildCount()
...{
int Count = 0;
    CXControl * Temp = this->GetFirstChild();

while(Temp)
...{
        CXControl * Next = Temp->GetNextSibling();
        Count++;
        Temp = Next;
    }
return Count;
}

7.7 绝对坐标和相对坐标

图7.6

图7.6
注意,这个按钮的绝对坐标和相对坐标是不一样的。一个表示的是它在屏幕上的位置,而另一个表示的是它在它的父控件中的位置。

第7.5小节解释过什么是画布以及任何可视的东西本质上都是控件。它示范了如何用宽和高描述一个控件的大小,如何用可见和不可见表示控件的显示状态。但是我们忽略了另外一个属性——坐标。很显然,每一个控件都有X和Y两个坐标。坐标又分绝对坐标和相对坐标两种。绝对坐标是人们想起坐标时立即跳进人们脑子的想法,它是从屏幕的左上角开始计算的。相对坐标是相对于它的父控件来说的,换句话说,它是从其父控件的左上角开始计算的。有些人可能想问这个区别是否真的重要。我可以十分肯定的回答你,是的。为什么?看看图7.6你就明白了。在UI LIB中,所有的控件都使用的是相对坐标,因为它比绝对坐标更简单更直观。虽然有时候我们也不得不计算它的绝对坐标,你会在下一小节看到如何实现它。

class CXControl 
...{
public:
    CXControl();
virtual ~CXControl();

protected:
    D3DXVECTOR2 m_Position;
    DWORD m_Width;
    DWORD m_Height;
bool m_Visible;
    CXTexture * m_Canvas;
    CXPen * m_Pen;
    CXControl * m_ChildControls;
    CXControl * m_NextSibling;
    CXControl * m_PreviousSibling;
    CXControl * m_Parent;
public:
// Accessors
    CXTexture * GetCanvas(void) ...{return m_Canvas;}
void SetCanvas(CXTexture * Texture) ...{m_Canvas = Texture;}
bool GetVisible(void) ...{return m_Visible;}
void SetVisible(bool Visible) ...{m_Visible = Visible;}
    CXPen * GetPen(void) ...{return m_Pen;}
void SetPen(CXPen * Pen) ...{m_Pen = Pen;}
    DWORD GetWidth(void) ...{return m_Width;}
    DWORD GetHeight(void) ...{return m_Height;}
void SetWidth(DWORD Width) ...{m_Width = Width;}
void SetHeight(DWORD Height) ...{m_Height = Height;}
    CXControl * GetParentControl(void) ...{return m_Parent;}
void SetParentControl(CXControl * Control) ...{m_Parent = Control;}
    CXControl * GetNextSibling(void) ...{return m_NextSibling;}
void SetNextSibling(CXControl * Control) ...{m_NextSibling = Control;}
    CXControl * GetPreviousSibling(void) ...{return m_PreviousSibling;}
void SetPreviousSibling(CXControl * Control) ...{m_PreviousSibling = Control;}
    CXControl * GetFirstChild(void) ...{return m_ChildControls;}
void SetFirstChild(CXControl * Control) ...{m_ChildControls = Control;}
    D3DXVECTOR2 * GetPosition(void) ...{return &m_Position;}
    FLOAT GetXPos(void) ...{return m_Position.x;}
    FLOAT GetYPos(void) ...{return m_Position.y;}
void SetXPos(FLOAT X) ...{m_Position.x = X;}
void SetYPos(FLOAT Y) ...{m_Position.y = Y;}
void SetXYPos(FLOAT X, FLOAT Y);

    CXControl * AddChildControl(CXControl * Control);
    CXControl * RemoveChildControl(CXControl * Control);
void RemoveAllChildren();
int GetChildCount();
void GetAbsolutePosition(D3DXVECTOR2 * Position);
};

注意
在层次结构中,像应用程序主窗口这样的顶层控件的绝对坐标和相对坐标是一样的。这是因为除了桌面以外,顶层控件没有父控件,而桌面覆盖了整个屏幕。(译者:我一直都把桌面作为顶层控件的父控件,而桌面的ParentControl为NULL)

7.7.1 计算坐标

图7.7

图7.7

通过一个控件的相对坐标可以轻松地计算出它的绝对坐标。这使得拖动窗口在屏幕上移动时控件正确的重绘变得简单,因为控件与控件之间的相对坐标是不变的。

CXControl的GetAbsolutePosition方法可以返回一个控件的绝对坐标,也就是控件在整个屏幕上的坐标。为了正确的绘制控件,我们会经常用到绝对坐标。计算控件的绝对坐标是一个简单的过程:你可以简单地用控件的相对坐标加上其父控件的绝对坐标。实际上,这是从一个控件到它的顶层父控件的相对坐标的累积。函数定义如下。

void CXControl::GetAbsolutePosition(D3DXVECTOR2 * Position)
...{
    Position->x+=this->GetXPos();
    Position->y+=this->GetYPos();

if(this->m_Parent)
this->m_Parent->GetAbsolutePosition(Position);
}

7.8 类CXControl目前的声明

代码同7.7,略

7.9 总结

本章以CXControl的形式初步介绍了UI LIB。这个类包含了所有控件都必须拥有的一般属性。下一章我们将继续深入这个主题。但在继续之前,我们复习一下所学内容。

■库是能完成某一任务的函数、结构和类的集合。库主要是通过提供完成任务的工具来减轻程序开发者的负担。像DirectX就是一个例子。

■UI LIB是User Interface Library的缩写。它由像按钮、列表框、复选框之类的一组控件组成。开发者可以用它为自己的软件创建用户界面。

■即使控件千差万别,但它们都继承了一个共有的属性集。这就是为什么开发CXControl。虽然它本身不能独立实例化,但它作为一个基类出现,为其他控件提供基础特征集。

■在几何学上,控件是一个典型的被称为画布的矩形区域。它可以用宽和高,可见和不可见表示。控件在画布区域内绘制自己。因此,按钮有一个外观,而列表是另一个。

■界面中的每一个控件都存在在一个层次结构中。顶层的控件被认为是最终的祖先或根控件,通常用作程序的主窗口。其他的控件则是它的子孙,它们同样也可以有兄弟和孩子。

■控件有两个坐标,一个绝对坐标,一个相对坐标。前者表示的是控件从屏幕左上角开始计算的真实坐标,后者表示的是从其父控件左上角开始计算的坐标。无论何时在屏幕上移动一个窗口,这个程序对重新调整控件都很有用处。

声明:本书的英文版权归原作者所有,我翻译的这些版权自然归我。你可以下载到本地保存留念,但在未取得本人书面许可时,谢绝任何形式的转载。你如果将其用于商业目的,请先与原英文版版权所有者联系,以免引起不必要的麻烦。

posted @ 2007-10-30 21:03  至尊王者  阅读(1006)  评论(0编辑  收藏  举报