量化交易软件:在 GUI 控件中使用布局和容器CBox 类
1. 介绍
在大多应用里, 在对话框窗口里使用控件绝对定位的直接方式来创建图形用户界面。然而, 在某些情况下, 这种 图形用户界面 (GUI) 设计的方式很不方便, 甚或很不实际。本文介绍一种基于布局和容器来创建 GUI (图形用户界面) 的替代方法, 使用一个布局管理器 — CBox 类。
在本文中实现并使用的布局管理器大致等同于一些可在主流编程语言找到的诸如 BoxLayout (Java) 和 几何管理器包 (Python/Tkinter)。

编辑切换为居中
2. 目标
查看 赫兹量化里提供的 SimplePanel 和控件例程, 赫兹量化可以看到在这些面板里的控件都按照像素定位 (绝对定位)。创建的每个控件都在客户区域分配一个确定的位置, 且每个控件都依赖于在其之前创建的控件, 并附加一些偏移。虽然这是很自然的方式, 尽管在大多情况下不要求很高精度, 使用这种方法在许多方面都很不利。
任何有经验的程序员在设计图形用户界面是都可采用图形控件的精确像素定位。不过, 这有以下不足:
通常, 当一个部件的大小或位置被修改时, 它不能防止其它部件免受影响。
大多数代码不可重用 — 界面上的微小改变可能导致代码的极大修改。
它可能很耗时间, 尤其在设计更复杂界面时。
这促使赫兹量化创建一个布局系统, 其目标如下:
代码应可重用。
修改界面的一部分应对其它部件的影响最小化。
界面中的部件定位应自动计算。
在这篇文章里介绍了一种此类系统的实现, 使用容器 — CBox 类。
3. 类 CBox
类 CBox 的一个实例作为一个容器或盒子 — 可将控件添加到该盒子中, 且 CBox 可自动计算控件在其所分配的空间里的定位。一个典型的 CBox 类实例应有以下布局:

编辑
图例 1. CBox 布局
外层盒子表示容器的整个大小, 而内里的虚线盒子表示衬垫边界。蓝色区域表示衬垫空间。剩余白色空间则是控件在容器内可用于定位的空间。
依据面板的复杂度, CBox 类可按不同方式使用。例如, 它可作为容器 (CBox) 来保存其它存有一套控件的容器。或是容器内含有一个控件和其它容器。不过, 极力推荐在给定的父容器里使用同辈份容器。
赫兹量化通过扩展 CWndClient (不含滚动条) 来构建 CBox, 如以下片段所示:
#include <Controls\WndClient.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CBox : public CWndClient { public: CBox(); ~CBox(); virtual bool Create(const long chart,const string name,const int subwin, const int x1,const int y1,const int x2,const int y2); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CBox::CBox() { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CBox::~CBox() { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool CBox::Create(const long chart,const string name,const int subwin, const int x1,const int y1,const int x2,const int y2) { if(!CWndContainer::Create(chart,name,subwin,x1,y1,x2,y2)) return(false); if(!CreateBack()) return(false); if(!ColorBackground(CONTROLS_DIALOG_COLOR_CLIENT_BG)) return(false); if(!ColorBorder(clrNONE)) return(false); return(true); } //+------------------------------------------------------------------+
CBox 类也可以直接从 CWndContainer 继承。但是, 这样做会丧失一些这个类里的有用功能, 如背景和边框。代之, 一个更简单的版本可以直接从扩展 CWndObj来实现, 但您将需要加入一个 CArrayObj 的实例作为其 私有或保护成员 并重新创建所涉及对象的类方法来保存该实例。
3.1. 布局样式
CBox 有两种布局样式: 垂直样式和水平样式。
水平样式有以下基本布局:

编辑
图利 2. 水平样式 (居中)
垂直样式有以下基本布局:
编辑
图例 3. 垂直样式 (居中)
CBox 省缺使用水平样式。
使用这两种布局的组合 (也许使用多容器), 这可以重建几乎任何类型的 GUI 面板设计。此外, 在容器内布置控件也允许分段设计。即, 它允许在给定容器里自定义控件的大小和位置, 且不影响其它容器的所在。
为了在 CBox 内实现水平和垂直样式, 赫兹量化需要声明一个枚举, 然后我们在所述类中将其作为一个成员保存:
enum LAYOUT_STYLE { LAYOUT_STYLE_VERTICAL, LAYOUT_STYLE_HORIZONTAL };
3.2. 计算控件之间的间隔
CBox 在其所分配的可用空间里最大化, 并为其所含控件均匀定位, 如前图所示。
综观上图, 赫兹量化可以推导出计算给定 CBox 容器内控件之间间隔的公式, 使用以下的伪代码:
对于水平布局: x 间隔 = ((可用空间 x)-(所有控件的总计 x 大小))/(控件总数 + 1) y 间隔 = ((可用空间 y)-(y 控件大小))/2 对于垂直布局: x 间隔 = ((可用空间 x)-(x 控件大小))/2 y 间隔 = ((可用空间 y)-(所有控件的总计 y 大小))/(控件总数 + 1)
3.3. 对齐
如上一章节所述, 控件间的间隔计算仅用于居中对齐。我们希望 CBox 类能容纳更多对齐方式, 所以我们需要在计算中进行一些小修改。
对于水平对齐, 可用的选项, 除了容器居中, 是靠左, 靠右, 和居中 (无边), 如下图所示:

编辑
图例 4. 水平样式 (左对齐)

编辑
图例 5. 水平样式 (右对齐)

编辑
图例 6. 水平样式 (居中, 无边)
对于水平对齐, 可用的选项, 除了容器居中, 是靠顶, 靠底, 居中, 和居中 (无边), 如下图所示:

编辑

编辑

编辑
图例 7. 垂直对齐样式: (左) 居顶, (中) 居中 - 无边, (右) 居底
需要注意的是的 CBox 类应基于这些对齐设置来自动计算控件间的 x- 和 y-间隔。所以, 最好使用除数
(控件总数 + 1)
来获取控件间隔, 我们赫兹量化控件总数作为除数, 以及 (控件总数 - 1) 作为边界无余量的居中控件。
类似于布局样式, 实现 CBox 类的对齐特性将需要枚举。我们要为每种对齐样式声明一个枚举, 如下:
enum VERTICAL_ALIGN { VERTICAL_ALIGN_CENTER, VERTICAL_ALIGN_CENTER_NOSIDES, VERTICAL_ALIGN_TOP, VERTICAL_ALIGN_BOTTOM }; enum HORIZONTAL_ALIGN { HORIZONTAL_ALIGN_CENTER, HORIZONTAL_ALIGN_CENTER_NOSIDES, HORIZONTAL_ALIGN_LEFT, HORIZONTAL_ALIGN_RIGHT };
3.4. 部件渲染
一般地, 赫兹量化通过指定 x1, y1, x2, 和 y2 参数来创建控件, 譬如以下创建一个 按钮 的片段:
CButton m_button; int x1 = currentX; int y1 = currentY; int x2 = currentX+BUTTON_WIDTH; int y2 = currentY+BUTTON_HEIGHT if(!m_button.Create(m_chart_id,m_name+"Button",m_subwin,x1,y1,x2,y2)) return(false);
此处 x2 减去 x1 以及 y2 减去 y1 分别等于控件的宽度和高度。若不用这种方法, 我们可以利用 CBox, 采用更简单的方法来创建同样的按钮, 如以下片段所示:
if(!m_button.Create(m_chart_id,m_name+"Button",m_subwin,0,0,BUTTON_WIDTH,BUTTON_HEIGHT)) return(false);
类 CBox 将会在之后创建的面板窗口里自动重定位部件。调用方法 Pack() 用于控件和容器的重定位, 它会再调用 Render() 方法, :
bool CBox::Pack(void) { GetTotalControlsSize(); return(Render()); }
方法 Pack() 简单地获取容器的组合大小, 之后调用 Render() 方法, 在此处会有更多动作。以下片段示意通过 Render() 方法对真实容器内的控件渲染:
bool CBox::Render(void) { int x_space=0,y_space=0; if(!GetSpace(x_space,y_space)) return(false); int x=Left()+m_padding_left+ ((m_horizontal_align==HORIZONTAL_ALIGN_LEFT||m_horizontal_align==HORIZONTAL_ALIGN_CENTER_NOSIDES)?0:x_space); int y=Top()+m_padding_top+ ((m_vertical_align==VERTICAL_ALIGN_TOP||m_vertical_align==VERTICAL_ALIGN_CENTER_NOSIDES)?0:y_space); for(int j=0;j<ControlsTotal();j++) { CWnd *control=Control(j); if(control==NULL) continue; if(control==GetPointer(m_background)) continue; control.Move(x,y); if (j<ControlsTotal()-1) Shift(GetPointer(control),x,y,x_space,y_space); } return(true); }
3.5. 部件大小调整
当控件尺寸大于其容器的可用空间, 应调整控件大小以便适应可用空间。否则, 控件将溢出容器, 致使整个面板的外观问题。当您希望某个控件最大化, 占据整个客户或其容器空间, 这种方法也一样便利。如果给定控件的宽度或高度超出容器的宽度或高度减去衬垫 (双层边), 将调整控件大小至最大可用宽度或高度。
需要注意的是, CBox 包含的所有控件的大小总和超过可用空间时, 不会调整容器大小。在此情况下, 或是主对话框窗口的大小 (CDialog 或 CAppDialog), 或是单独的控件将需要手工调整。