《DirectX9 User Interfaces Design and Implementation》第八章的译文
第8章 Continuing CXControl
译者:leexuany(小宝)
介绍:这就是《DirectX9 User Interfaces Design and Implementation》第8章的译文,让大家等了一个月,不好意思。这次小宝偷懒了,代码都没打全,想看的到我的资源(http://download.csdn.net/source/222788)里下电子书吧,需要0个积分。
正文:
图8.1
本章将继续UI LIB的开发,这是一个控件的集合,它包括按钮、列表框、复选框,甚至是窗口。下面我们要继续前一章剩余的工作,完成CXControl这个基类。我们将涉及到以下内容:
■事件处理
■窗口消息和自定义消息
■消息的传递和处理
■绘制
■深度优先遍历
■焦点
8.1 消息
为了随意的操纵控件,我们需要清楚地知道事件何时发生。例如,文本框需要对按键做出反应,按钮必须响应鼠标事件,等等。应用程序通过WndProc函数接收事件,这是消息循环的一部分。这里,事件以消息的形式被接收。实际上,当事件发生时它们作为参数传递给WndProc函数,它们是描述事件信息的数据包,比如键盘的按键情况和鼠标的新位置。下面定义的是windows消息的结构,表8.1列出了常见的消息和它们的描述。
struct Message
{
UINT Message; // 消息的类型,如鼠标消息等
LPARAM Parameterl; // 描述事件的附加信息
WPARAM Parameterw; // 描述事件的附加信息(注意,书上的这个变量名印错了)
};
表8.1 常用的消息
略,请参看原版电子书,我的资源中有(http://download.csdn.net/source/222788)
8.1.1 传递消息
一旦WndProc接收到任何消息,它们就必须传递给各个控件。最特别的是,它们要传递给结构中的顶层控件。这样的控件一般是应用程序的主窗口。传递消息的过程被称为消息的分发,为了接收这些消息我们需要为CXControl添加PostMessage函数。它的声明和定义稍后分析。眼下我们知道一旦有消息到达这里,它们就必须向下分发给子孙控件,这是下一小节要讨论的话题。看看下面的WndProc函数来了解如何为控件选择并传递消息。
LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch(message)
{
case WM_MOUSEMOVE:
case WM_KEYDOWN:
// More messages here, you get the idea...
Window->PostMessage(message, wParam, lParam, NULL);
break;
}
return 0;
}
8.1.2 消息细节(Message Sepcifics)
消息用上面这个PostMessage方法传递给结构中的顶层控件。接着,它们以事件的形式传递给各个子控件。消息如何准确的向下传递以及谁接收它们主要取决于消息本身,这将在后面的小节中讨论。
8.2 处理鼠标消息
当鼠标状态改变的时候,结构中顶层的控件一般会接收到如下消息:
WM_LBUTTONDBLCLK
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_MBUTTONDBLCLK
WM_MBUTTONDOWN
WM_MBUTTONUP
WM_MOUSEMOVE
WM_RBUTTONDBLCLK
WM_RBUTTONDOWN
WM_RBUTTONUP
注意,这并不是一个完整的列表,而且大多数的程序不处理鼠标中键的消息。随着消息的不同,这可能表示一个按钮按下、弹起,或者鼠标位置的改变。后面的小节演示了如何确认这些鼠标消息,以及如何将它们以事件的形式向下传递。下面的CXControl类新增了鼠标事件、PostMessage方法和其他几个函数。
class CXControl
{
protected:
// ...
CXControl * PostToAll(UINT msg, WPARAM wParam, LPARAM lParam, void * Data);
}
8.2.1 光标插入点(Cursor Intersection)
一旦PostMessage方法收到鼠标消息,就有必要确认它们。由于鼠标行为的几何本质,鼠标的点击和移动只会影响到鼠标指针下面的控件并且不会超出它的边界(即,不会影响到此控件边界以外的其它控件)。因此最终只会有一个控件被告知此鼠标事件。为了确定一个控件是否可以接收某个鼠标事件,你必须检测鼠标是否在控件的矩形区域内。因此CXControl中添加了一个CursorIntersect函数用来判断输入的位置是否符合这个标准(就是判断鼠标是否在控件区域内)。
bool CXControl::CursorIntersect(FLOAT X, FLOAT Y)
{
D3DXVECTOR2 ControlAbsolutePos;
ControlAbsolutePos.x = 0;
ControlAbsolutePos.y = 0;
// ...
}
注意
这与第11章处理文本框中的插入符时对鼠标位置的解释有所不同。(译者注:在DOS的时代我们习惯将那个一闪一闪的竖线叫做“光标”,现在为了与鼠标指针相区别,已经改称为“插入符”了)
8.2.2 分级发送(Hierarchical Posting)
前面已经提到过,在某一时刻内只能有一个控件受鼠标的影响。换句话说就是,一次只能有一个控件位于鼠标指针的下方。因此,这就是那个能接受到鼠标事件的控件,它也被称作是目标控件。既然这样,目标控件就是画布与光标相交并且没有子孙控件或者没有与光标相交的子孙控件的控件。通常情况下,目标控件是结构中的最底层的控件。因此,确定目标控件就是一个排除的过程。这个过程从结构中的顶部开始并递归地向下移动,直到用相交测试找到目标控件为止。图8.2到8.5简要地示范了这个过程。
图8.2到8.5
下面我们为CXControl添加PostToAll成员函数。这是一个递归函数,作用是向结构中的子控件传递消息。在下一节中,我们将探寻如何将此运用于处理鼠标消息。
CXControl * CXControl::PostToAll(UINT msg, WPARAM wParam, LPARAM lParam, void * Data)
{
CXControl * Temp = GetFirstChild();
while(Temp)
{
CXControl * Next = Temp->GetNextSibling();
if(Temp->PostMessage(msg, wParam, lParam, Data))
return Temp;
Temp = Next;
}
return NULL;
}
8.2.3 触发鼠标事件
我们的辛苦劳动就要得到成果了,这里我们要把鼠标消息转化成鼠标事件。在前面的章节中,我们研究过如何利用PostMessage把消息从WndProc传递到结构中的顶层控件,以及此函数如何触发派生类的适当事件,从而通过处理鼠标消息来实现此函数了。下面的代码演示了如何将从PostMessage接收的鼠标消息变得可用并分发给目标控件。
bool CXControl::PostMessage(UINT msg, WPARAM wParam, LPARAM lParam, void * Data)
{
switch(msg)
{
case WM_LBUTTONDOWN:
if(CursorIntersect(LOWORD(lParam), HIWORD(lParam)))
{
CXControl * Control = PostToAll(msg, wParam, lParam, Data);
if(!Control)
OnMouseDown(msg, LOWORD(lParam), HIWORD(lParam));
return true;
}
else
return false;
break;
}
}
8.3 处理键盘消息
当用户操作键盘的时候下面的键盘消息就会发生:
WM_KEYDOWN
WM_KEYUP
WM_CHAR
一个按键既可以按下也可以不按下。结果,处理键盘消息要比处理鼠标消息简单。击键不提供几何上可测量的数据。按键一般不像鼠标那样定义一个屏幕上的位置。因此,它们不能准确地告诉程序输入是为哪个控件准备的。这就是为什么要将焦点的概念应用于键盘消息的处理。基本上,拥有焦点的控件接受键盘消息。此外,在任何时刻,只能有结构中的一个控件获得焦点。后面的章节讨论了更多的关于焦点的细节。下面的代码展示了修正后的CXControl,增加了焦点的实现部分。
protected:
bool m_Focus;
// Focus control
CXControl * m_Focus;
public:
// Called when the user presses a key
virtual void OnKeyDown(WPARAM Key, LPARAM Extended) = NULL;
// Called when the user releases a key
virtual void OnKeyUp(WPARAM Key, LPARAM Extended) = NULL;
// Get focus control
CXControl * GetFocus() {return m_Focus;}
// Set focus control
void SetFocus(CXControl * Control);
};
8.3.1 焦点
图8.6
拥有焦点的控件接收键盘消息。在任何时刻只能有一个控件拥有焦点。通过选择一个控件来把焦点转向它,换句话说就是,通过点击控件或者其他类似的过程来让控件获得焦点。从本质上来说就是,有一个控件获得焦点,就有一个控件失去它。下面的CXControl的成员方法展示了在层次中一直维持焦点是多么的奇妙以及焦点是如何从一个控件跳到另一个控件上去的。
void CXControl::SetFocus(CXControl * Control)
{
if(!m_Focus)
{
if(GetParentControl())
GetParentControl->SetFocus(Control);
else
{
if(GetFocus())
GetFocus()->m_Focus = false;
m_FocusControl = Control;
m_Focus = true;
}
}
}
注意:
实际上,当一个控件的孩子控件获得焦点的时候你不需要向父控件(指它本身)通报。但是这里已经这么做了。这样,当有消息传递给结构中的顶层控件时,拥有焦点的控件就可以直接接收输入的信息。下一小节中会谈论到这些。
8.3.2 处理事件
在使用了焦点的概念之后,把键盘消息转化为键盘事件就成了一个十分简单的过程。你可以简单地调用拥有焦点的控件的键盘事件就可以了。现在PostMessage成员方法添加进了处理键盘消息的代码。
CXControl * CXControl::PostToAll(UINT msg, WPARAM wParam, LPARAM lParam, void * Data)
{
switch(msg)
{
// Handle other messages here...
case WM_KEYUP:
case WM_KEYDOWN:
if(GetFocus())
{
if(msg == WM_KEYUP)
GetFocus()->OnKeyUp(wParam, lParam);
if(msg == WM_KEYDOWN)
GetFocus()->OnKeyDown(wParam, lParam);
}
break;
}
return NULL;
}
8.4 处理控件绘制
最后需要考虑的,同时也是最重要的事件之一就是绘制。每当结构接收到WM_RENDER消息时,所有的控件都应接收到OnRender事件。这是一个自定义的消息用来代替WM_PAINT。它看起来和下面的句子类似:
#define WM_RENDER WM_USER+1
这个消息表示这个控件期待重绘自己,这个过程被称作是重绘。每个控件的外观都不一样,按钮一个样,而标签就是另外一个样子,等等。不同的控件如何绘制它们自己将在后面的章节中进行讨论。下面的代码是CXControl处理OnRender事件的一个实例。他演示了一个典型的控件如何使用CXTexture和CXPen类来绘制自己。后面的例子展示了绘制消息是如何通过结构传递给控件的。
bool CXTest::OnRender()
{
D3DXVECTOR2 ControlAbsolutePos;
ControlAbsolutePos.x = 0;
ControlAbsolutePos.y = 0;
GetAbsolutePosition(&ControlAbsolutePos);
GetCanvas()->SetTranslation(&ControlAbsolutePos);
GetPen()->DrawTexture(GetCanvas());
GetCanvas()->SetTranslation(NULL);
}
提示
当窗口需要重绘的时候WndProc都会接收到WM_PAINT消息,尽管Direct3D在它自己的渲染程序中重绘窗口,但那是因为标准的窗口绘制太慢了。因此,WM_PAINT消息被一个自定义的WM_RENDER消息取代了,并且在Direct3D应用程序的渲染循环中手工传递到结构中去。如果你喜欢,你可以只是传递WM_PAINT消息,但是WM_RENDER更清楚、透明。
8.5 反过来传递
当WM_RENDER消息传递到结构中的时候,每一个控件都会接收到OnRender事件。绘制从结构的顶层开始,并逐层向下传递。这给人的感觉就好像是父控件要先于它的子控件绘制到屏幕上。换句话说就是,子控件晚于它们的父控件,或者说是绘制在父控件之上的。子控件接收到这个事件的顺序是重要的,因为它们要根据它们的z序列来绘制。需要特别指出的是,这里提到的序列是安排在三维空间中的。这个Z字序的方向是从屏幕后面指向你的,如图8.7。层次、控件还有它们的Z字序看起来就好像图8.8一样。
图8.7
图8.8
正如图8.8所示,右边的兄弟都有一个比左边的大的Z值。这意味着,右边的紧贴在它左边的兄弟后面。因此,结构中的兄弟需要从右往左绘制,而不是习惯上的从左向右。这确保了最左边的控件总是绘制在最前面。下面的代码是PostToAll函数的改写版本,叫做PostAllReverse。与从左向右传递消息不同的是,这个是从右向左传递的。
CXControl * CXControl::PostToAllReverse(CXControl * Control, UINT msg, WPARAM wParam, LPARAM lParam, void * Data)
{
CXControl * Next = Control->GetNextSibling();
if(Next)
Next->PostToAllReverse(Next, msg, wParam, lParam, Data);
Control->PostMessage(msg, wParam, lParam, Data);
return NULL;
}
8.6 深度搜索(深度优先遍历?)
如果你的桌面包含三个不同的窗口,你可以通过在它们上点击一下来从它们中间进行选取。当你这样做的时候,被点击的窗口被前置,其它的被放到后面去。效果就是它们的Z字序改变了。下面的几幅图阐明了这些。
为了在结构中实现当前活动的窗口总是在它众多兄弟的前面,你必须确保活动的窗口总是最左边的窗口。如果不是这样,你必须重新调整这些控件以保持这个顺序。添加下面这个名叫MoveToFront的函数。它接受一个控件作为参数,并将其设为最左边的节点(控件)。
代码略。
8.7 触发绘制事件
结构中的所有的控件都从右到左接受一个OnRender事件。这是通过PostAllReverse方法完成从而确保控件按照正确的Z字序绘制。因此,PostMessage函数修改为下面的样子来处理WM_RENDER消息。请注意其中对鼠标点击处理做的细小变动。它已经可以为一个控件设置焦点并使用了MoveToFront方法。这确保当人们点击时控件被激活,换句话说就是,将其移动到最左边的节点,然后以最低的Z字序绘制。
代码略。
8.8 CXContrl最终的声明
代码略。
8.9 总结
这一章完成了基类CXControl。现在它已经包含了其它控件需要从它继承的所有东西,这些将在下一章编写CXWindow的时候用到。在继续之前,我们来回顾一下本章内容。
■消息是用来描述用户电脑上事件的数据结构。这包括绘制消息、鼠标消息以及键盘消息等。
■消息传递到结构的顶层控件,并以事件的形式向下传递给子控件。
■鼠标消息描述了与鼠标相关的消息,这包括按钮的按下、释放和光波的移动。
■一次只能有一个控件相应鼠标事件,并且以OnMouseMove、OnButtonDown、OnButtonUp等形式接收通告消息。
■键盘消息描述了与键盘相关的消息,这包括按键的按下与释放等。
■因为键盘不能像鼠标那样定义一个屏幕上的位置,所以它们不知道哪个控件接收输入。正因如此,我们引入了焦点的概念。
■拥有焦点的控件接收键盘输入,并且在任一时刻只能有一个控件拥有焦点。
■WM_PAINT消息表示控件需要绘制它们自己。
■控件应该按照它们的Z字序绘制自己。Z字序在三维空间上定义了控件与它的兄弟控件的层次关系。