在 GUI 控件中使用布局和容器: CBox 类

Enrico Lambino | 13 十一月, 2015

目录


1. 介绍

在大多应用里, 在对话框窗口里使用控件绝对定位的直接方式来创建图形用户界面。然而, 在某些情况下, 这种 图形用户界面 (GUI) 设计的方式很不方便, 甚或很不实际。本文介绍一种基于布局和容器来创建 GUI (图形用户界面) 的替代方法, 使用一个布局管理器 — CBox 类。

在本文中实现并使用的布局管理器大致等同于一些可在主流编程语言找到的诸如 BoxLayout (Java) 和 几何管理器包 (Python/Tkinter)。


2. 目标

查看 MetaTrader 5 里提供的 SimplePanel 和控件例程, 我们可以看到在这些面板里的控件都按照像素定位 (绝对定位)。创建的每个控件都在客户区域分配一个确定的位置, 且每个控件都依赖于在其之前创建的控件, 并附加一些偏移。虽然这是很自然的方式, 尽管在大多情况下不要求很高精度, 使用这种方法在许多方面都很不利。

任何有经验的程序员在设计图形用户界面是都可采用图形控件的精确像素定位。不过, 这有以下不足:

这促使我们创建一个布局系统, 其目标如下:

在这篇文章里介绍了一种此类系统的实现, 使用容器 — CBox 类。


3. 类 CBox

类 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 有两种布局样式: 垂直样式和水平样式。

水平样式有以下基本布局:

CBox 的水平样式

图利 2. 水平样式 (居中)

垂直样式有以下基本布局:

CBox 的垂直样式

图例 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 包含的所有控件的大小总和超过可用空间时, 不会调整容器大小。在此情况下, 或是主对话框窗口的大小 (CDialogCAppDialog), 或是单独的控件将需要手工调整。


3.6. 递归渲染

对于 CBox 的简单用法, 单词调用 Pack() 方法就足够了。然而, 对于嵌套容器, 需要调用同样的方法, 以便所有的容器能够为自身的单个控件或容器定位。我们可以通过在函数里添加方法来预防这种情况, 当且仅当问题中的控件是 CBox 类或任何布局类的实例时, 可为其自身控件实现相同的方法。为此, 首先我们定义一个 并为之分配独一的数值:

#define CLASS_LAYOUT 999

之后, 我们重写 CObject 类的 Type() 方法, 以便它返回我们刚准备的宏数值:

virtual int       Type() const {return CLASS_LAYOUT;}

最后, 在 CBox 类的 Pack() 方法里, 我们将为其子容器执行渲染方法, 而这些子容器均是布局类的实例:

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);

      //如果它是布局类, 调用控件 Pack() 方法
      if(control.Type()==CLASS_LAYOUT)
        {
         CBox *container=control;
         container.Pack();
        }     
   
      if (j<ControlsTotal()-1)
         Shift(GetPointer(control),x,y,x_space,y_space);      
     }

渲染方法在开始阶段计算容器内的控件可用空间。这些数值分别存储在 m_total_x 和 m_total_y。下一步任务是基于布局样式和对齐计算控件间的间隔。最后一步是在容器内实现真实的控件重定位。

CBox 维护一个控件的记录用于重定位, 因为容器内的一些对象无需重定位, 譬如 CWndClient 本地背景对象, 或是其他一些可能的 CBox 扩展控件。

CBox 也保留容器内控件的最小尺寸 (背景除外), 由 m_min_size (CSize 结构) 定义。其目的是保持控件在容器内统一叠放, 无论是水平或垂直。它的定义是相当反直觉的, 因为这实际上是最大的控件尺寸。然而, 在这里我们将其定义为最小, 因为 CBox 会假定这个尺寸是最小尺寸, 并且将基于该尺寸计算可用空间。

注意, Shift() 方法遵循一个途径, 类似于通常的控件定位实现 (绝对定位)。渲染方法保留 x- 和 y- 坐标的引用, 记住并在更新 CBox 时为每个控件重定位。不过, 对于 CBox, 这可自动完成, 留给面板开发者的只是简单地为用到的每个控件设置实际大小。


4. 在对话框窗口里的实现

当使用 CBox 时, 我们几乎取代了 CDialogCAppDialog 本地客户区域的功能, m_client_area, 则是 CWndClient 的实例。所以, 我们此刻至少有三个选项:

  1. 扩展/重写 CAppDialogCDialog 以 CBox 取代客户区域。
  2. 使用容器并将它们加入客户区域。
  3. 使用一个主体 CBox 容器来容纳其余较小的容器。

使用第一种选择可能需要很多的工作量, 我们将需要重写对话框对象, 令其使用新的客户区域对象。代之, 可扩展对话框对象来使用定制的容器类, 但留给我们一个无用的 CWndClient (m_client_area) 实例, 占用不必要的内存空间。

第二个选项也是可行的。我们简单地将控件置于 CBox 容器内, 之后使用像素定位将它们添加到客户区域。但这个选项并未充分利用 CBox 类的潜能, 即设计面板时不必为独立控件和容器的定位所困扰。

推荐第三种选项。即是, 我们创建一个主体 CBox 容器, 以便容纳所有其它较小的容器和控件。主体容器将占据整个本地客户区域, 并作为其唯一添加的子容器。这将使得本地客户区域有点冗余, 但至少, 它仍可使用。此外, 我们可以避免为使用此选项而大量的编码/重编码。


5. 例程


5.1. 例程 #1: 简单点值计算

现在, 我们使用 CBox 类来实现一个简单的面板: 点值计算器。点值计算器对话框将包含sange CEdit 类型的字段, 称为:

这给我们带来总共 7 个不同的控件, 包括每个字段的标签 (CLabel), 和执行计算的按钮 (CButton)。计算器的屏幕截图如下图所示:

点值计算器 - 屏幕截图

图例 8. 点值计算器

通过观察计算器面板, 我们可以使用 CBox 将之降低到 5 种不同的容器。每个字段的水平容器共有 3 个, 另一个水平容器居右, 作为按钮。所有这些容器将在一个垂直样式的主体容器内封装。最后, 这个主体容器将被附加至 CAppDialog 实例的客户区域。下图示意容器的布局。紫罗兰色的盒代表水平行。白色盒代表基本控件, 包含所有较小盒子的最大灰色盒是主体盒子窗口。

点值计算器 - 对话框布局

图例 9. 点值计算器布局

注意, 使用 CBox 容器时, 我们不能为缺口和缩进声明任何宏。我们只需简单声明控件尺寸的宏, 配置每个 CBox 实例, 并让它们分配给控件。

为了构建这个面板, 首先我们在开始阶段创建一个头文件, 'PipValueCalculator.mqh', 它应与稍后我们要准备的源代码主体文件放在同一文件夹 (PipValueCalculator.mq5)。我们在此文件里包含 CBox 类的头文件, 以及其它我们面板所需的包含文件。我们也需要 CSymbolInfo 类, 我们将用其实际计算所有给定品种的点值:

#include <Trade\SymbolInfo.mqh>
#include <Layouts\Box.mqh>
#include <Controls\Dialog.mqh>
#include <Controls\Label.mqh>
#include <Controls\Button.mqh>

最后一步是为我们将要使用的控件指定宽度和高度。。可以为每个控件指定确定的大小, 但对于这个面板, 我们将使用一个通用的空间大小。也就是说, 所有基本控件将具有相同的宽度和高度:

#define CONTROL_WIDTH   (100)
#define CONTROL_HEIGHT  (20)

现在, 我们继续创建真实的面板类对象。这可按照通常方式从 CAppDialog 类继承一个新类来完成:

class CPipValueCalculatorDialog : public CAppDialog

类的初始结构看上去如下相仿:

class CPipValueCalculatorDialog : public CAppDialog
  {
protected:
//此处是类的保护成员
public:
                     CPipValueCalculatorDialog();
                    ~CPipValueCalculatorDialog();

protected:
//此处是类的保护方法
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CPipValueCalculatorDialog::CPipValueCalculatorDialog(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CPipValueCalculatorDialog::~CPipValueCalculatorDialog(void)
  {
  }

来自以上代码片断, 我们现在有了一个点值计算器面板类的起始模板 (实际上, 在创建类似面板时它可以重复使用)。现在, 我们继续创建主体容器类的成员, 它将作为可在面板上出现的所有其它 CBox 容器的父容器:

class CPipValueCalculatorDialog : public CAppDialog
  {
protected:
   CBox              m_main;
//此处更多代码...

我们已经为面板定义了主体 CBox 容器, 但还没为它创建实际函数。为此, 我们将其它类方法添加到面板类, 如下所示:

// 开始类定义
// ...
public:
                     CPipValueCalculatorDialog();
                    ~CPipValueCalculatorDialog();
protected:
   virtual bool      CreateMain(const long chart,const string name,const int subwin);
// 其余的定义
// ...

之后, 在类之外, 我们定义类方法的实体 (类似于 如何定义类的构造函数和析构函数的实体):

bool CPipValueCalculatorDialog::CreateMain(const long chart,const string name,const int subwin)
  {   
   //创建主体 CBox 容器
   if(!m_main.Create(chart,name+"main",subwin,0,0,CDialog::m_client_area.Width(),CDialog::m_client_area.Height()))
      return(false);   

   //应用垂直布局
   m_main.LayoutStyle(LAYOUT_STYLE_VERTICAL);
   
   //设置所有边框衬垫 10 个像素
   m_main.Padding(10);
   return(true);
  }

我们使用 CDialog::m_client_area.Width() 和 CDialog::m_client_area.Height() 来指定容器的宽度和高度。即为, 它占据面板客户区的整个空间。我们也对容器进行一些修饰: 应用垂直样式, 并设置所有边框的衬垫为 10 个像素。这些函数已由 CBox 类提供。

现在我们已经定义了主体容器类成员, 且它应该被创建, 然后我们为图例 9 中的每行创建成员。对于最上面一排, 这行是品种, 我们首先创建容器来声明它们, 然后是包含它的基本控件, 仅仅低于上面的代码片段中所示的主体容器类成员:

CBox              m_main;
CBox              m_symbol_row;   //行容器
CLabel            m_symbol_label; //标签控件
CEdit             m_symbol_edit;  //编辑控件

类似于主体容器, 我们也定义一个函数来创建实际的行容器:

bool CPipValueCalculatorDialog::CreateSymbolRow(const long chart,const string name,const int subwin)
  {
   //为行创建 CBox 容器 (品种行)
   if(!m_symbol_row.Create(chart,name+"symbol_row",subwin,0,0,CDialog::m_client_area.Width(),CONTROL_HEIGHT*1.5))
      return(false);

   //创建标签控件
   if(!m_symbol_label.Create(chart,name+"symbol_label",subwin,0,0,CONTROL_WIDTH,CONTROL_HEIGHT))
      return(false);
   m_symbol_label.Text("品种");
   
   //创建编辑控件
   if(!m_symbol_edit.Create(chart,name+"symbol_edit",subwin,0,0,CONTROL_WIDTH,CONTROL_HEIGHT))
      return(false);
   m_symbol_edit.Text(m_symbol.Name());

   //添加基本控件至它们的父控件 (行)
   if(!m_symbol_row.Add(m_symbol_label))
      return(false);
   if(!m_symbol_row.Add(m_symbol_edit))
      return(false);
   return(true);
  }

在此函数里, 我们首先创建品种行容器。注意, 我们使用客户区域的整个宽度作为宽度, 同时令其高度大于我们先前定义的控件高度的 50%。

创建行之后, 我们再创建单独的控件。这次, 它们使用我们先前定义的控件宽度和高度宏。也要注意我们是如何创建这些控件的:

Create(chart,name+"symbol_edit",subwin,0,0,CONTROL_WIDTH,CONTROL_HEIGHT))

红色数值是 x1 和 y1 坐标。这意味着在创建时, 所有控件被置于图表的左上方。在我们调用 CBox 类的 Pack() 方法之后这些会立刻重新安排。

我们已经创建了行容器。我们也已经在该容器内创建了基本控件。下一步骤是添加我们刚刚创建的控件到行容器, 在函数的最后几行表示:

if(!m_symbol_row.Add(m_symbol_label))
   return(false);
if(!m_symbol_row.Add(m_symbol_edit))
   return(false);

对于其它行 (点大小, 点值, 和按钮行), 我们的实现大致如同在品种行做到的那样。

当使用 CBox 类时, 需要创建主体容器和其它子行。现在, 我们进入比较熟悉的局面, 即是创建面板对象本身。这可以通过覆盖 CAppDialog 类的方法 Create() 来达成 (无论是否使用 CBox)。我们之前定义的两个方法最终有了意义, 因为我们将在此调用这些方法:

bool CPipValueCalculatorDialog::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2)
  {
   //创建 CAppDialog 面板
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      return(false);
   
   //使用我们之前定义的函数创建主体 CBox 容器  
   if(!CreateMain(chart,name,subwin))
      return(false);  

   //使用我们之前定义的函数创建品种行 CBox 容器  
   if(!CreateSymbolRow(chart,name,subwin))
      return(false);

   //添加品种行作为主体 CBox 容器的子 CBox 容器
   if(!m_main.Add(m_symbol_row))
      return(false);

   //渲染主体 CBox 容器以及它的所有子容器 (递归)
   if (!m_main.Pack())
      return(false);
   
   //添加主体 CBox 容器作为面板客户区域的仅有子容器
   if (!Add(m_main))
      return(false);
   return(true);
  }

不要忘记在 CPipValueCalculatorDialog 类中声明重写的 Create() 方法, 如下:

public:
                     CPipValueCalculatorDialog();
                    ~CPipValueCalculatorDialog();
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);

如上所示代码, 它应该是一个类的公共方法, 因为我们将从类外部调用它。在主要源代码文件里将需要更多具体内容: PipValueCalculator.mq5:

#include "PipValueCalculator.mqh"
CPipValueCalculatorDialog ExtDialog;
//+------------------------------------------------------------------+
//| 初始函数                                                          |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
//--- 创建应用对话框
   if(!ExtDialog.Create(0,"点值计算器",0,50,50,279,250))
      return(INIT_FAILED);
//--- 运行应用
   if(!ExtDialog.Run())
      return(INIT_FAILED);
//--- ok
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| 逆初函数                                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   ExtDialog.Destroy(reason);
  }
//+------------------------------------------------------------------+
//| 报价处理函数                                                      |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
  }
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
    //评论至此, 将在章节稍后继续讨论  
    //ExtDialog.ChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+

这段代码与我们在面板主体源代码文件里经常看到的十分相似, 除了以下三处:

  1. 我们加入了 'PipValueCalculator.mqh' 头文件, 而不是从 CAppDalog 来包含文件。'PipValueCalculator.mqh' 已经包含在头文件里了, 所以在主体源代码里不再需要包含它。'PipValueCalculator.mqh' 也负责包含 CBox 类的头文件。
  2. 我们声明 ExtDialog 作为我们在 'PipValueCalculator.mqh' (PipValueCalculator 类) 之中定义的类的实例。
  3. 我们为面板指定一个更适合它的自定义尺寸, 定义在 ExtDialog.Create()。

当仅仅编译品种行时, 面板看上去与如下截图类似:

仅有一行的点值计算器面板

图例 10. 仅有一行的点值计算器面板

主体容器有垂直布局并居中对齐, 此时品种行显示为水平布局 (同样是水平和垂直居中对齐)。为了将面板组装成图例. 8 所示之一, 我们需要添加其它三行, 基本上是采用我们创建品种行时的同样方法。一个例外是按钮行, 它只包含一个单一基本控件(按钮), 且是右对其:

m_button_row.HorizontalAlign(HORIZONTAL_ALIGN_RIGHT);

事件的处理超出了本文的范畴, 但为了这个例程的完整性, 我们将其简要介绍一下。我们首先为 PipValueCalculator 类声明一个新的类成员, m_symbol。我们还包括了两个附加成员, m_digits_adjust 和 m_points_adjust, 它们将在稍后用于转换点数至点值。

CSymbolInfo      *m_symbol;
int               m_digits_adjust;
double            m_points_adjust;

初始化 m_symbol, 既可在类的构造器里, 也可在 Create() 方法里, 使用如下代码:

if (m_symbol==NULL)
      m_symbol=new CSymbolInfo();
if(m_symbol!=NULL)
{
   if (!m_symbol.Name(_Symbol))
      return(false);
}   

如果品种点数为空, 我们创建一个新的 CSymbolInfo 实例。如果它不为空, 我们为其分配的品名等于图表的品名。

下一步骤是定义按钮的点击事件处理器。这可通过 OnClickButton() 类方法来实现。我们如下定义它的实体:

void CPipValueCalculatorDialog::OnClickButton()
  {
   string symbol=m_symbol_edit.Text();
   StringToUpper(symbol);
   if(m_symbol.Name(symbol))
     {
      m_symbol.RefreshRates();
      m_digits_adjust=(m_symbol.Digits()==3 || m_symbol.Digits()==5)?10:1;
      m_points_adjust=m_symbol.Point()*m_digits_adjust;
      m_pip_size_edit.Text((string)m_points_adjust);      
      m_pip_value_edit.Text(DoubleToString(m_symbol.TickValue()*(StringToDouble(m_pip_size_edit.Text()))/m_symbol.TickSize(),2));
     }
   else Print("无效输入");
  }

类方法首先获取 m_symbol_edit 控件的数值来进行计算点值。之后传递品名到 CSymbolInfo 类实例。所述类获取所选择品种的报价值, 然后再乘以一定的乘数进行调整计算出 1 点值。

最后一个步骤是启用类中定义的事件处理器 (也在 PipValueCalculator 类中)。在类的公有方法之下插入这样几行代码:

virtual bool      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);

然后, 我们在类的外部定义类的方法实体, 如下片断:

EVENT_MAP_BEGIN(CPipValueCalculatorDialog)
   ON_EVENT(ON_CLICK,m_button,OnClickButton)
EVENT_MAP_END(CAppDialog)


5.2. 例程 #2: 控件重构实例

在全新安装 MetaTrader 之后 Controls 面板的例程会自动安装。在导航窗口, 可在 Expert Advisors\Examples\Controls 之下找到它。此面板的屏幕截图如下图所示:

控件 - 对话框

图例 11. 控件对话框 (原始)

以上显示的对话框窗口的布局, 在下图中详述。为了使用 CBox 实例重构面板, 我们看到 4 个主要的水平行 (紫罗兰色) 组成以下控件集合:

  1. Edit 控件;
  2. 三个 Button 控件;
  3. SpinEdit 和 DatePicker;
  4. ComboBox, RadioGroupCheckGroup (列 1) 以及 ListView (列 2)。

最后的水平容器是一种特殊情况, 因为它是一个嵌套水平容器持有其它两个容器 (绿色的列 1 和 2)。这些容器应以垂直布局。

图例 12. 控件对话框布局

图例 12. 控件对话框布局

当重新构造对话框控件时, 任何调用 Add() 方法的代码都应被删除, 除了主体容器, 它将作为客户区域的唯一子角色。同时, 其它控件和容器应添加到它们的设计好的父容器里, 从最深层直到主体容器, 最终抵达本地客户区域。

一旦安装完成, 编译并执行。除了日期选择器的递增、递减和列表按钮会拒绝工作, 其它应当一切正常。这是出于这样的事实, 即 CDatePicker 类的下拉列表被设置在其它容器的背景当中。要解决此问题, 搜寻位于%数据文件夹%\MQL5\Include\Controls\DatePicker.mqh 的 CDatePicker 类文件。找到 ListShow() 方法并在函数开始处, 插入这几行代码:

BringToTop();

重新编译并测试。这将令日期选择器的下拉列表放置在前景, 且赋予当它显示时的点击事件优先权。此处是完整的函数片断:

bool CDatePicker::ListShow(void)
  {
   BringToTop();
//--- 设置数值   
   m_list.Value(m_value);
//--- 显示列表
   return(m_list.Show());
  }

控件对话框重构的屏幕截图如下图所示:

控件 - 对话框重构

图例 13. 控件对话框 (使用 CBox)

细观大图, 它与原图近乎一致。但此处也有一个显著的区别, 而这只是顺便做到的 — 列 1 与列 2 完美地对齐。在原图里, 我们可以看到 CheckGroupListView 在底部均匀堆叠。不过, 在顶部, ComboBox 并没有与 ListView 对齐。当然, 在原始面板里坐标可以重新定位, 但这不仅将需要调整 ComboBox 的像素, 而且还需调整 RadioGroup 以及三个控件之间的间隔。使用一个 CBox 容器, 另一方面, 仅需设置顶底衬垫为零, 并使用正确的对齐。

然而, 这并不意味着使用 CBox 或布局在精细之处更胜一筹。虽然不可否认精度低于为控件坐标强制编码, 使用容器和布局仍然能够提供合适的精度级别, 而在同时令 GUI 设计更容易一点。


6. 优缺点

优点:

缺点:


7. 结论

在这篇文章中, 我们已经研究了在设计图形面板时使用布局和容器的可能性。这种方法使我们能够利用布局和对齐样式自动处理不同控件的定位。它可令图形面板设计更加简便, 并且在某些场合, 减少编写代码时间。

类 CBox class 是一个辅助控件, 在 GUI 面板里充当一个基本控件的容器。在这篇文章中, 我们已经演示了它的操作以及如何在实际应用中使用它。虽然精度低于绝对定位, 但其精度级别依然可以在许多应用里提供便利。