图形界面 XI: 重构函数库代码 (集成编译 14.1)

Anatoli Kazharski | 21 八月, 2017


内容

概述

首篇文章 图形界面 I: 函数库结构的准备 (第 1 章) 详细研究了这个函数库可以做什么。每部分的文章末尾都提供了章节链接的列表。您也可以从那里下载最新、最完整版本的函数库。文件必须位于存档中所在的目录下。

函数库的最后更新旨在优化代码, 减少其大小, 令其实现更加面向对象。所有这一切将令代码更容易学习。所有变更的详细描述将允许读者独立地修改函数库用于自己的任务, 尽可能少地花费时间。 

由于发布量很大, 最新函数库变更的描述已切分为两篇文章。第一部分呈现于此。

控件的常用属性

首先, 这些变更涉及函数库控件的常用属性。以前, 某些属性 (背景, 边框, 文本等的颜色) 的字段和方法存储在每个独立控件的派生类中。在面向对象编程方面, 这是重载。如今, 所有必要的图形界面控件已经实现了, 很容易确定频繁重复地负责设置常规属性的字段和方法, 并将它们移动到基类。 

我们来定义属性的完整列表, 它们是函数库内所有控件所固有的, 可以放在基类中。

  • 从文件中读取图像 (图标) 或以编程方式渲染。
  • 沿 X 和 Y 轴的缩进形成图像。
  • 背景颜色。
  • 边框颜色。
  • 文本颜色。
  • 描述文本。
  • 描述文本的缩进。

现在有必要决定哪个类将包含用于设置和获取这些属性的字段和方法。 

每个控件的类层次结构中有两个类: CElementBaseCElement。以下属性的字段和方法将被放置在 CElementBase 基类中: 坐标, 大小, 标识符, 索引, 以及来自列表的每个控件特有的模式。用于管理控件外观相关属性的字段和方法将被放置在派生类 CElement 中。

另外, 用来创建控件的图形对象名称的方法被添加到 CElementBase 类中。以前, 该名称是在创建控件的方法中生成的。现在, 每个控件均绘制在一个单独的对象上, 可作为通用方法置于基类中。 

CElementBase::NamePart() 方法设计用来获取和设置名称部分, 示意控件的类型。

//+------------------------------------------------------------------+
//| 控件基类                                                           |
//+------------------------------------------------------------------+
class CElementBase
  {
protected:
   //--- 名称部分 (控件的类型)
   string            m_name_part;
   //---
public:
   //--- (1) 保存, 并 (2) 返回控件名称部分
   void              NamePart(const string name_part)                { m_name_part=name_part;                }
   string            NamePart(void)                            const { return(m_name_part);                  }
  };

CElementBase::ElementName() 方法用于生成图形对象的全名。必须将名称部分传递给该方法, 以便示意控件类型。如果事实证明, 名称部分早前已设置, 则不会使用所传递的值。由于函数库最近的变化 (见下文), 这种方法用于一个控件衍生自另一个控件的情况, 而名称部分需要重新定义。

class CElementBase
  {
protected:
   //--- 控件的名称
   string            m_element_name;
   //---
public:
   //--- 形成对象名
   string            ElementName(const string name_part="");
  };
//+------------------------------------------------------------------+
//| 返回生成的控件名称                                                   |
//+------------------------------------------------------------------+
string CElementBase::ElementName(const string name_part="")
  {
   m_name_part=(m_name_part!="")? m_name_part : name_part;
//--- 形成对象名
   string name="";
   if(m_index==WRONG_VALUE)
      name=m_program_name+"_"+m_name_part+"_"+(string)CElementBase::Id();
   else
      name=m_program_name+"_"+m_name_part+"_"+(string)CElementBase::Index()+"__"+(string)CElementBase::Id();
//---
   return(name);
  }

当处理某个控件上的鼠标点击时, 需要检查所点击图形对象的名称。许多控件经常重复这个检查, 因此, 一个特殊的 CElementBase::CheckElementName() 方法已经被放进基类中:

class CElementBase
  {
public:
   //--- 检查该行是否包含控制名称的重要部分
   bool              CheckElementName(const string object_name);
  };
//+------------------------------------------------------------------+
//| 返回生成的控件名称                                                  |
//+------------------------------------------------------------------+
bool CElementBase::CheckElementName(const string object_name)
  {
//--- 如果在这个控件上按下
   if(::StringFind(object_name,m_program_name+"_"+m_name_part+"_")<0)
      return(false);
//---
   return(true);
  }

对于其它属性, 仅介绍处理图像的方法的描述是有意义的。


处理图像数据的类

若要处理图像, 已实现了 CImage 类, 可以存储图像数据:

  • 图像像素数组;
  • 图像维度 (宽度和高度);
  • 文件路径。

为了获取这些属性的值, 需要适当的方法:

//+------------------------------------------------------------------+
//| 存储图像数据的类                                                    |
//+------------------------------------------------------------------+
class CImage
  {
protected:
   uint              m_image_data[]; // 图像像素的数组
   uint              m_image_width;  // 图像宽度
   uint              m_image_height; // 图像高度
   string            m_bmp_path;     // 图像文件的路径
   //---
public:
                     CImage(void);
                    ~CImage(void);
   //--- (1) 数据数组大小, (2) 设置/返回数据 (像素颜色)
   uint              DataTotal(void)                             { return(::ArraySize(m_image_data)); }
   uint              Data(const uint data_index)                 { return(m_image_data[data_index]);  }
   void              Data(const uint data_index,const uint data) { m_image_data[data_index]=data;     }
   //--- 设置/返回图像宽度
   void              Width(const uint width)                     { m_image_width=width;               }
   uint              Width(void)                                 { return(m_image_width);             }
   //--- 设置/返回图像高度
   void              Height(const uint height)                   { m_image_height=height;             }
   uint              Height(void)                                { return(m_image_height);            }
   //--- 设置/返回图像路径
   void              BmpPath(const string bmp_file_path)         { m_bmp_path=bmp_file_path;          }
   string            BmpPath(void)                               { return(m_bmp_path);                }
  };

CImage::ReadImageData() 方法用于读取图像并存储其数据。此方法应该将路径传递给图像文件:

class CImage
  {
public:
   //--- 读取并存储所传递图像的数据
   bool              ReadImageData(const string bmp_file_path);
  };
//+------------------------------------------------------------------+
//| 将传递的图像存储到数组                                               |
//+------------------------------------------------------------------+
bool CImage::ReadImageData(const string bmp_file_path)
  {
//--- 如果是空字符串, 离开
   if(bmp_file_path=="")
      return(false);
//--- 存储到图像的路径
   m_bmp_path=bmp_file_path;
//--- 重置最后错误
   ::ResetLastError();
//--- 读取并存储图像数据
   if(!::ResourceReadImage("::"+m_bmp_path,m_image_data,m_image_width,m_image_height))
     {
      ::Print(__FUNCTION__," > 读取图像时出错 ("+m_bmp_path+"): ",::GetLastError());
      return(false);
     }
//---
   return(true);
  }

有时需要复制所传递图像的数据。这可通过 CImage::CopyImageData() 方法来完成。一个 CImage 类型的对象通过引用传递 到此方法, 以便复制此对象的数据。此处, 首先获得源数组的大小, 并且为接收数组设置相同的大小。之后, CImage::Data() 方法用于循环检索所传递数组的数据并将其存储在接收数组中

class CImage
  {
public:
   //--- 复制所传递图像的数据
   void              CopyImageData(CImage &array_source);
  };
//+------------------------------------------------------------------+
//| 复制所传递图像的数据                                                 |
//+------------------------------------------------------------------+
void CImage::CopyImageData(CImage &array_source)
  {
//--- 获取源数组的大小
   uint source_data_total =array_source.DataTotal();
//--- 调整接收数组的大小
   ::ArrayResize(m_image_data,source_data_total);
//--- 复制数据
   for(uint i=0; i<source_data_total; i++)
      m_image_data[i]=array_source.Data(i);
  }


CImage::DeleteImageData() 方法用于删除图像数据:

class CImage
  {
public:
   //--- 删除图像数据
   void              DeleteImageData(void);
  };
//+------------------------------------------------------------------+
//| 删除图像数据                                                        |
//+------------------------------------------------------------------+
void CImage::DeleteImageData(void)
  {
   ::ArrayFree(m_image_data);
   m_image_width  =0;
   m_image_height =0;
   m_bmp_path     ="";
  }

CImage 类包含在 Objects.mqh 文件里。现在所有的控件都将被渲染, 因此不再需要用于创建图形基元的类。它们已从 Objects.mqh 文件中删除。仅针对子图表进行异常处理, 这样能够创建类似于品种主图的图表。所有类型的 MQL 应用程序位于其窗口中, 相应地, 创建了图形界面。

//+------------------------------------------------------------------+
//|                                                      Objects.mqh |
//|                           版权所有 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#include "Enums.mqh"
#include "Defines.mqh"
#include "Fonts.mqh"
#include "Canvas\Charts\LineChart.mqh"
#include <ChartObjects\ChartObjectSubChart.mqh>
//--- 文件中的类清单, 以便快速导航 (Alt+G)
class CImage;
class CRectCanvas;
class CLineChartObject;
class CSubChart;
...


处理图像的方法

处理图像的方法已经实现了若干种。所有这些都被放置在 CElement 类中。现在可以设置特定控件所需的图像组数 (数组)。这令控件的外观更具信息化。控件中显示的图标数量由 MQL 应用程序的开发者定义。 

为此目的, 在 CElement 文件当中创建了 EImagesGroup 结构, 并声明了一个 其实例的动态数组。它将包含图像组 (缩排和选择要显示的图像) 的属性, 以及存储在 CImage 类型 动态数组中的图像本身。 

//+------------------------------------------------------------------+
//| 控件的派生类                                                       |
//+------------------------------------------------------------------+
class CElement : public CElementBase
  {
protected:
   //--- 图像组
   struct EImagesGroup
     {
      //--- 图像数组
      CImage            m_image[];
      //--- 图标边距
      int               m_x_gap;
      int               m_y_gap;
      //--- 从组中选择要显示的图像
      int               m_selected_image;
     };
   EImagesGroup      m_images_group[];
  };

为了将图像添加到控件中, 必须首先添加组。这可以在 CElement::AddImageGroup() 方法的帮助下完成。必须将该组中的图像距控件左上角的缩进作为参数传递。省缺时, 组中的第一个图像 将选为显示。

class CElement : public CElementBase
  {
public:
   //--- 添加一个图像组
   void              AddImagesGroup(const int x_gap,const int y_gap);
  };
//+------------------------------------------------------------------+
//| 添加一个图像组                                                      |
//+------------------------------------------------------------------+
void CElement::AddImagesGroup(const int x_gap,const int y_gap)
  {
//--- 获取图像组数组的大小
   uint images_group_total=::ArraySize(m_images_group);
//--- 添加一个组
   ::ArrayResize(m_images_group,images_group_total+1);
//--- 设置图像的缩进
   m_images_group[images_group_total].m_x_gap=x_gap;
   m_images_group[images_group_total].m_y_gap=y_gap;
//--- 省缺图像
   m_images_group[images_group_total].m_selected_image=0;
  }

CElement::AddImage() 方法设计用于将图像添加到组中。必须将组索引和图像文件的路径指定为其参数。如果没有组, 则不会添加图像。此处还为防止超出数组范围而进行调整。超出范围时, 图像将被添加到最后一个组。

class CElement : public CElementBase
  {
public:
   //--- 将图像添加到指定的组
   void              AddImage(const uint group_index,const string file_path);
  };
//+------------------------------------------------------------------+
//| 将图像添加到指定的组                                                 |
//+------------------------------------------------------------------+
void CElement::AddImage(const uint group_index,const string file_path)
  {
//--- 获取图像组数组的大小
   uint images_group_total=::ArraySize(m_images_group);
//--- 如果没有组, 离开
   if(images_group_total<1)
     {
      Print(__FUNCTION__,
      " > 可以使用 CElement::AddImagesGroup() 方法添加一组图像");
      return;
     }
//--- 预防超出范围
   uint check_group_index=(group_index<images_group_total)? group_index : images_group_total-1;
//--- 获取图像数组的大小
   uint images_total=::ArraySize(m_images_group[check_group_index].m_image);
//--- 将数组大小增加一个元素
   ::ArrayResize(m_images_group[check_group_index].m_image,images_total+1);
//--- 添加一张图像
   m_images_group[check_group_index].m_image[images_total].ReadImageData(file_path);
  }


现在可以使用第二个版本的 CElement::AddImagesGroup() 方法来添加一组含有图像数组。在此, 除了缩进之外, 还需要传递一个文件路径的数组作为参数。将图像组数组的大小增加一个元素之后, 程序使用 CElement::AddImage() 方法 (参见以上) 在循环中添加所传递的整个数组

class CElement : public CElementBase
  {
public:
   //--- 添加含有图像数组的一个图像组
   void              AddImagesGroup(const int x_gap,const int y_gap,const string &file_pathways[]);
  };
//+------------------------------------------------------------------+
//| 添加含有图像数组的一个图像组                                          |
//+------------------------------------------------------------------+
void CElement::AddImagesGroup(const int x_gap,const int y_gap,const string &file_pathways[])
  {
//--- 获取图像组数组的大小
   uint images_group_total=::ArraySize(m_images_group);
//--- 添加一个组
   ::ArrayResize(m_images_group,images_group_total+1);
//--- 设置图像的缩进
   m_images_group[images_group_total].m_x_gap =x_gap;
   m_images_group[images_group_total].m_y_gap =y_gap;
//--- 省缺图像
   m_images_group[images_group_total].m_selected_image=0;
//--- 获取所添加的图像数组的大小
   uint images_total=::ArraySize(file_pathways);
//--- 如果传递了非空数组, 则将图像添加到新组
   for(uint i=0; i<images_total; i++)
      AddImage(images_group_total,file_pathways[i]);
  }

某个组中的图像可以在程序运行时被设置或替换。为此, 请使用 CElement::SetImage() 方法, 传递 (1) 组的索引, (2) 图像的索引, 和 (3) 文件的路径作为参数。 

class CElement : public CElementBase
  {
public:
   //--- 设置/替换图像
   void              SetImage(const uint group_index,const uint image_index,const string file_path);
  };
//+------------------------------------------------------------------+
//| 设置/替换图像                                                      |
//+------------------------------------------------------------------+
void CElement::SetImage(const uint group_index,const uint image_index,const string file_path)
  {
//--- 检查超出数组范围
   if(!CheckOutOfRange(group_index,image_index))
      return;
//--- 删除图像
   m_images_group[group_index].m_image[image_index].DeleteImageData();
//--- 添加一张图像
   m_images_group[group_index].m_image[image_index].ReadImageData(file_path);
  }

但是, 如果在创建控件时最初设置了所有必要的图像, 最好使用 CElement::ChangeImage() 方法简单地切换它们:

class CElement : public CElementBase
  {
public:
   //--- 切换图像
   void              ChangeImage(const uint group_index,const uint image_index);
  };
//+------------------------------------------------------------------+
//| 切换图像                                                           |
//+------------------------------------------------------------------+
void CElement::ChangeImage(const uint group_index,const uint image_index)
  {
//--- 检查超出数组范围
   if(!CheckOutOfRange(group_index,image_index))
      return;
//--- 存储要显示的图像索引
   m_images_group[group_index].m_selected_image=(int)image_index;
  }

要查找特定组中当前选定的图像, 请使用 CElement::SelectedImage() 方法。如果没有组或指定组中没有图像, 则方法返回负值

class CElement : public CElementBase
  {
public:
   //--- 返回在指定组中所选的显示图像
   int               SelectedImage(const uint group_index=0);
  };
//+------------------------------------------------------------------+
//| 返回在指定组中所选的显示图像                                          |
//+------------------------------------------------------------------+
int CElement::SelectedImage(const uint group_index=0)
  {
//--- 如果没有组, 离开
   uint images_group_total=::ArraySize(m_images_group);
   if(images_group_total<1 || group_index>=images_group_total)
      return(WRONG_VALUE);
//--- 如果指定组中没有图像, 离开
   uint images_total=::ArraySize(m_images_group[group_index].m_image);
   if(images_total<1)
      return(WRONG_VALUE);
//--- 返回所选的显示图像
   return(m_images_group[group_index].m_selected_image);
  }


以前, 用户可能需要显示图标, 所有控件类都具有设置图像的方法。例如, 可以为按钮的释放和按压状态以及锁定状态分配图标。这个功能将会保留下来, 因为它是一个清晰易懂的选项。如前所述, 图标缩进可以使用 CElement::IconXGap() 和 CElement::IconYGap() 方法设置。

class CElement : public CElementBase
  {
protected:
   //--- 图标边距
   int               m_icon_x_gap;
   int               m_icon_y_gap;
   //---
public:
   //--- 图标边距
   void              IconXGap(const int x_gap)                       { m_icon_x_gap=x_gap;              }
   int               IconXGap(void)                            const { return(m_icon_x_gap);            }
   void              IconYGap(const int y_gap)                       { m_icon_y_gap=y_gap;              }
   int               IconYGap(void)                            const { return(m_icon_y_gap);            }
   //--- 设置活跃和锁定状态的图标
   void              IconFile(const string file_path);
   string            IconFile(void);
   void              IconFileLocked(const string file_path);
   string            IconFileLocked(void);
   //--- 设置控件按下状态的图标 (可用/锁定)
   void              IconFilePressed(const string file_path);
   string            IconFilePressed(void);
   void              IconFilePressedLocked(const string file_path);
   string            IconFilePressedLocked(void);
  };

CElement::IconFile() 方法的代码作为示例提供。在此, 如果控件没有任何图像组, 则先添加一个组。如果在调用方法之前未指定任何缩进, 则为它们设置零值。添加组之后, 添加参数中所传递的图像, 并为 控件的锁定状态图像保留空格

//+------------------------------------------------------------------+
//| 设置活跃状态的图标                                                  |
//+------------------------------------------------------------------+
void CElement::IconFile(const string file_path)
  {
//--- 如果尚无图像组
   if(ImagesGroupTotal()<1)
     {
      m_icon_x_gap =(m_icon_x_gap!=WRONG_VALUE)? m_icon_x_gap : 0;
      m_icon_y_gap =(m_icon_y_gap!=WRONG_VALUE)? m_icon_y_gap : 0;
      //--- 添加组和图像
      AddImagesGroup(m_icon_x_gap,m_icon_y_gap);
      AddImage(0,file_path);
      AddImage(1,"");
      //--- 省缺图像
      m_images_group[0].m_selected_image=0;
      return;
     }
//--- 将图像设置为第一个组作为第一个元素
   SetImage(0,0,file_path);
  }

为了找出 图像组的数量特定组中的图像数量, 需要使用相应的方法 (参见下面的代码清单):

class CElement : public CElementBase
  {
public:
   //--- 返回图像组的数量
   uint              ImagesGroupTotal(void) const { return(::ArraySize(m_images_group)); }
   //--- 返回指定组中的图像数量
   int               ImagesTotal(const uint group_index);
  };
//+------------------------------------------------------------------+
//| 返回指定组中的图像数量                                               |
//+------------------------------------------------------------------+
int CElement::ImagesTotal(const uint group_index)
  {
//--- 检查组索引
   uint images_group_total=::ArraySize(m_images_group);
   if(images_group_total<1 || group_index>=images_group_total)
      return(WRONG_VALUE);
//--- 图像数量
   return(::ArraySize(m_images_group[group_index].m_image));
  }

合并控件作为函数库代码优化的一部分

直至目前, 许多控件实质上已重复, 只有少数方法是特有的。这令代码急剧膨胀。因此, 函数库已进行了变更, 减少了代码并简化理解。

1. 早前, 已经开发了两个类来创建按钮。

  • CSimpleButton — 简单按钮。
  • CIconButton — 图标按钮。

但现在, 可以使用基本工具在任何控件中创建图标。因此, 要创建具有不同属性的按钮, 不再需要两个或更多的类。只留下一个修订的类 - CButton。要将文本在按钮中居中, 只需使用 CElement::IsCenterText() 方法启用相应的模式, 该方法可以应用于任何控件。

class CElement : public CElementBase
  {
protected:
   //--- 文本对齐模式
   bool              m_is_center_text;
   //--- 
public:

   //--- 文本对齐到中心
   void              IsCenterText(const bool state)                  { m_is_center_text=state;          }
   bool              IsCenterText(void)                        const { return(m_is_center_text);        }
  };


2. 这同样适用于创建按钮组。在之前版本的函数库中, 创建了三个类来创建具有不同属性的按钮组:

  • CButtonsGroup — 简单按钮组。
  • CRadioButtons — 单选按钮组。
  • CIconButtonsGroup — 图标按钮组。

在所有这些类中, 按钮是从标准图形基元创建的。现在仅保留了 CButtonsGroup 类。它创建按钮作为现成的 CButton 类型的控件。 

可以使用 CButtonsGroup::RadioButtonsMode() 方法启用单选按钮模式 (当组中的一个按钮始终被选择时)。可以使用 CButtonsGroup::RadioButtonsStyle() 方法启用与单选按钮类似的外观。

class CButtonsGroup : public CElement
  {
protected:
   //    单选按钮模式
   bool              m_radio_buttons_mode;
   //--- 单选按钮显示风格
   bool              m_radio_buttons_style;
   //--- 
public:
   //--- (1) 设置模式, 并 (2) 显示单选按钮的样式
   void              RadioButtonsMode(const bool flag)              { m_radio_buttons_mode=flag;       }
   void              RadioButtonsStyle(const bool flag)             { m_radio_buttons_style=flag;      }
  };

3. 以下三个类用于创建带有编辑框的控件:

  • CSpinEdit — 轮转编辑框。
  • CCheckBoxEdit — 带有复选框的轮转编辑框。
  • CTextEdit — 文本编辑框。

上述类的所有属性都可以紧凑地放置在一个类中, 这就是 CTextEdit 类。如果需要创建带有增量和减量开关的轮转编辑框, 请使用 CTextEdit::SpinEditMode() 方法启用相应模式。如果控件需要复选框, 请使用 CTextEdit::CheckBoxMode() 方法启用该模式。 

class CTextEdit : public CElement
  {
protected:
   //--- 带复选框的控件模式
   bool              m_checkbox_mode;
   //--- 带按钮的轮转编辑框模式
   bool              m_spin_edit_mode;
   //--- 
public:
   //--- (1) 复选框, 和 (2) 轮转编辑框模式
   void              CheckBoxMode(const bool state)          { m_checkbox_mode=state;              }
   void              SpinEditMode(const bool state)          { m_spin_edit_mode=state;             }
  };

4. 这同样适用于创建组合框元素。以前有两个类:

  • CComboBox — 带有一个下拉列表的按钮。
  • CCheckComboBox — 带有下拉列表和复选框的按钮。

保留两个几乎相同的类是多余的, 因此只留下一个类 — CComboBox。可以使用 CComboBox::CheckBoxMode() 方法启用带有复选框的控件模式。

class CComboBox : public CElement
  {
protected:
   //--- 带复选框的控件模式
   bool              m_checkbox_mode;
   //--- 
public:
   //--- 设置带复选框的控件模式
   void              CheckBoxMode(const bool state)         { m_checkbox_mode=state;                  }
  };

5. 以前有两个类用于创建滑块:

  • CSlider — 简单的数字滑块。
  • CDualSlider — 双滑块用于指定数值范围。

只保留其中一个 — CSlider 类。使用 CSlider::DualSliderMode() 方法启用双滑块模式。

class CSlider : public CElement
  {
protected:
   //--- 双滑块模式
   bool              m_dual_slider_mode;
   //--- 
public:
   //--- 双滑块模式
   void              DualSliderMode(const bool state)           { m_dual_slider_mode=state;           }
   bool              DualSliderMode(void)                 const { return(m_dual_slider_mode);         }
  };

6. 早前, 函数库包含两个用于创建带滚动条的列表视图的类, 其中之一允许使用复选框创建列表视图。

  • CListView — 简单的列表视图, 能够选择一个项目。
  • CCheckBoxList — 带复选框的列表视图。

其中只剩下 CListView。若要创建带复选框的列表, 只需使用 CListView::CheckBoxMode() 方法启用相应的模式。

class CListView : public CElement
  {
protected:
   //--- 带有复选框的列表视图模式
   bool              m_checkbox_mode;
   //--- 
public:
   //--- 设置带复选框的控件模式
   void              CheckBoxMode(const bool state)         { m_checkbox_mode=state;                  }
  };

7. 函数库的以前版本包含多达三种表格类:

  • CTable — 编辑框表格。
  • CLabelsTable — 文本标签表格。
  • CCanvasTable — 渲染表格。

在函数库改进过程中, CCanvasTable 类被证明是最先进的。因此, 其它类已被删除, 并且 CCanvasTable 类已重命名为 CTable

8. 有两个类用于创建选项卡:

  • CTabs — 简单的选项卡。
  • CIconTabs — 图标选项卡。

没有必要在函数库中保留两个类。以前是由图形基元对象创建图标选项卡的数组。现在使用 CButton 类型的按钮用于此目的, 可以使用上述基本方法定义图标。结果就是, 我们只保留 CTabs 类。

控件层次

图形界面的控件层次也发生了变化。迄今为止, 所有控件都附加到窗体 (CWindow)。控件在窗体中相对其左上角对齐。在创建图形界面时, 需要指定每个控件的坐标。并且如果元素的位置在最终确定期间被修改, 则需要将位于区域内的所有控件重新手工定义相对于特定区域的所有坐标。例如, 位于 "选项卡" 控件区域内的其它控件组。如果我们需要将选项卡控件移动到窗体的任何其它部分, 则在每个单独选项卡上的所有控件都将保留在以前的位置。这是非常不方便的, 但现在问题解决了。

早前, 在自定义 MQL 应用程序类中创建某个控件之前, 必须使用 CElement::WindowPointer() 方法将窗体对象指针传递到窗体中。现在使用 CElement::MainPointer() 方法。作为参数, 它传递控件的对象, 其为将要附加的已创建控件。 

class CElement : public CElementBase
  {
protected:
   //--- 指向主控件的指针
   CElement         *m_main;
   //--- 
public:
   //--- 存储并返回指向主控件的指针
   CElement         *MainPointer(void)                               { return(::GetPointer(m_main));    }
   void              MainPointer(CElement &object)                   { m_main=::GetPointer(object);     }
  };

如前所述, 除非连接到主控件, 否则不能创建控件。创建控件 (在所有类中) 的主要方法是检查指向主控件的指针。这种检查的方法已改名为 CElement::CheckMainPointer()。在此存储 窗体指针指向鼠标光标对象的指针。它还检测并存储控件的标识符, 存储加载了 MQL 应用程序及其图形界面的图表标识符和子窗口编号。以前, 这段代码从一个类到另一个类一直重复。

class CElement : public CElementBase
  {
protected:
   //--- 检查指向主控件的指针是否存在
   bool              CheckMainPointer(void);
  };
//+------------------------------------------------------------------+
//| 检查指向主控件的指针是否存在                                          |
//+------------------------------------------------------------------+
bool CElement::CheckMainPointer(void)
  {
//--- 如果没有指针
   if(::CheckPointer(m_main)==POINTER_INVALID)
     {
      //--- 将消息输出到终端日志
      ::Print(__FUNCTION__,
              " > 在创建控件之前... \n ...必须将指针传递给主控件: "+
              ClassName()+"::MainPointer(CElementBase &object)");
      //--- 终止构建应用程序的图形界面
      return(false);
     }
//--- 存储窗体指针
   m_wnd=m_main.WindowPointer();
//--- 存放鼠标光标指针
   m_mouse=m_main.MousePointer();
//--- 存储属性
   m_id       =m_wnd.LastId()+1;
   m_chart_id =m_wnd.ChartId();
   m_subwin   =m_wnd.SubwindowNumber();
//--- 发送指针存在的标志
   return(true);
  }


加载控件到主控件的方法交错分布在所有类中。复杂的复合控件是由其它若干汇集而成的, 它们都按照一定的顺序相依加载。异常仅在三个类中产生:

  • CTable — 创建表格的类。
  • CListView — 创建列表视图的类。
  • CTextBox — 创建多行文本框的类。

它们由多个 OBJ_BITMAP_LABEL 类型的图形对象汇集而成, 其内容也被渲染。测试各种方法表明, 使用多个对象是函数库开发阶段的最佳方式。

CButton 类已经成为一块通用的 "基石", 实际上它用于函数库的所有其它控件。另外, CButton (按钮) 类型的控件现在是某些其它控件类的基础, 例如: 

  • CMenuItem — 菜单项。
  • CTreeItem — 树形视图项。

作为结果, 类之间关系的一般规划图按照 "基类派生" ("亲子") 格式现在看起来像这样:

图例. 1. 类之间关系的规划图, 按照基类派生 (亲子)。

图例. 1. 类之间关系的规划图, 按照基类派生 (亲子)。

截至目前, 函数库内已实现了 33 (三十三) 个不同的控件。以下是一系列规划图, 以字母顺序显示函数库内的所有控件。根控件以绿色标记并编号。接下来是整个嵌套深度的嵌套控件。每一列是特定控件的下一层嵌套控件。'[]' 符号标记的控件, 表示多个实例。这样的视图可以帮助读者更快更好地了解函数库的面向对象编程规划。

以下控件的规划如下图所示:

1. CButton — 按钮。
2. CButtonsGroup — 按钮组。
3. CCalendar — 日历。
4. CCheckBox — 复选框。
5. CColorButton — 颜色拾取器按钮。
6. CColorPicker — 颜色拾取器。
7. CComboBox — 调用下拉列表 (组合框) 的按钮。
8. CContextMenu — 上下文菜单。

图例. 2. 控件的规划示意图 (第 1 部分)。 

图例. 2. 控件的规划示意图 (第 1 部分)。


9. CDropCalendar — 调用下拉式日历的按钮。
10. CFileNavigator — 文件导航器。
11. CLineGraph — 线形图表。
12. CListView — 列表视图。

图例. 3. 控件的规划示意图 (第 2 部分)。

图例. 3. 控件的规划示意图 (第 2 部分)。

13. CMenuBar — 主菜单。
14. CMenuItem — 菜单项。
15. CPicture — 图片。
16. CPicturesSlider — 图片滑块。
17. CProgressBar — 进度条。
18. CScroll — 滚动条。
19. CSeparateLine — 分隔线。
20. CSlider — 数字滑块。
21. CSplitButton — 切分按钮。
22. CStandartChart — 标准图表。
23. CStatusBar — 状态栏。
24. CTable — 表格。
 


图例. 4. 控件的规划示意图 (第 3 部分)。

图例. 4. 控件的规划示意图 (第 3 部分)。

25. CTabs — 选项卡。
26. CTextBox — 文本编辑框, 带有启用多行模式的选项。
27. CTextEdit — 编辑框。
28. CTextLabel — 文本标签。
29. CTimeEdit — 输入时间的控件。
30. CTooltip — 工具提示。
31. CTreeItem — 树形视图项。
32. CTreeView — 树形视图。
33. CWindow — 容纳控件的窗体 (窗口)。
 

图例. 5. 控件的规划示意图 (第 4 部分)。

图例. 5. 控件的规划示意图 (第 4 部分)。


嵌套控件的数组

在以前版本的函数库中创建控件时, 它们的基类将指针存储到图形基元的对象中。现在, 它们将指针保存到图形界面的某个控件组件的控件当中。 

使用 protected CElement::AddToArray() 方法, 将控件指针添加到 CElement 类型的 一个动态数组中。此方法专为内部使用而设计, 仅用于控件类。 

class CElement : public CElementBase
  {
protected:
   //--- 嵌套控件的指针
   CElement         *m_elements[];
   //---
protected:
   //--- 将指针添加到嵌套控件的方法
   void              AddToArray(CElement &object);
  };
//+------------------------------------------------------------------+
//| 将指针添加到嵌套控件的方法                                            |
//+------------------------------------------------------------------+
void CElement::AddToArray(CElement &object)
  {
   int size=ElementsTotal();
   ::ArrayResize(m_elements,size+1);
   m_elements[size]=::GetPointer(object);
  }

可以从数组的指定索引处恢复控件的指针。这可使用 public CElement::Element() 方法完成。此外, 也提供了恢复嵌套控件数量和释放数组的方法。

class CElement : public CElementBase
  {
public:
   //--- (1) 获取嵌套控件的数量, (2) 释放嵌套控件数组
   int               ElementsTotal(void)                       const { return(::ArraySize(m_elements)); }
   void              FreeElementsArray(void)                         { ::ArrayFree(m_elements);         }
   //--- 返回指定索引处嵌套控件的指针
   CElement         *Element(const uint index);
  };
//+------------------------------------------------------------------+
//| 返回指定索引处嵌套控件的指针                                          |
//+------------------------------------------------------------------+
CElement *CElement::Element(const uint index)
  {
   uint array_size=::ArraySize(m_elements);
//--- 验证对象数组的大小
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > 这个控件 ("+m_class_name+") 没有嵌套控件!");
      return(NULL);
     }
//--- 超出范围时进行调整
   uint i=(index>=array_size)? array_size-1 : index;
//--- 返回对象指针
   return(::GetPointer(m_elements[i]));
  }

虚拟方法的基本代码

已定义了用于管理控件的虚拟方法的基本代码。这些方法包括:

  • Moving() — 移动。
  • Show() — 显示。
  • Hide() — 隐藏。
  • Delete() — 删除。
  • SetZorders() — 设置优先级。
  • ResetZorders() — 重置优先级。

现在每个控件均绘制在一个单独的图形对象上。所以, 上述方法可以被通用化, 从而消除了所有控件类中的重复。有些情况下, 一些方法将被某些控件类所覆盖。这就是这些方法被声明为虚拟的原因。例如, 这些控件包括 CListView, CTableCTextBox。之前已经提到, 这些是当前版本的函数库中由多个渲染图形对象汇集而成的仅有控件。 

考察 CElement::Moving() 方法为例。它现在没有参数。以前, 它要传递坐标和操作模式。但现在无需如此 — 一切都变得更加简单。这是由于函数库的核心 (CWndEvents 类) 经过实质性的修改, 但这会在本文的以下章节之中更详细地讨论。 

控件相对于其所挂载的主控件的当前位置进行移动。一旦图形对象被移动, 其嵌套控件 (如果有的话) 就会在一个循环中移动。几乎每个控件都调用相同的 CElement::Moving() 方法的副本。在嵌套的各个层次均会如此。也就是说, 如果需要移动某个控件, 那么调用该方法一次就足够了, 其它控件的类似方法将被自动调用 (按照它们被创建的顺序)。 

下表展示了 CElement::Moving() 虚方法的代码:

class CElement : public CElementBase
  {
public:
   //--- 移动控件
   virtual void      Moving(void);
  };
//+------------------------------------------------------------------+
//| 移动控件                                                          |
//+------------------------------------------------------------------+
void CElement::Moving(void)
  {
//--- 如果控件被隐藏, 离开
   if(!CElementBase::IsVisible())
      return;
//--- 如果锚定在右边
   if(m_anchor_right_window_side)
     {
      //--- 在控件字段中存储坐标
      CElementBase::X(m_main.X2()-XGap());
      //--- 在对象的字段中存储坐标
      m_canvas.X(m_main.X2()-m_canvas.XGap());
     }
   else
     {
      CElementBase::X(m_main.X()+XGap());
      m_canvas.X(m_main.X()+m_canvas.XGap());
     }
//--- 如果锚定到底部
   if(m_anchor_bottom_window_side)
     {
      CElementBase::Y(m_main.Y2()-YGap());
      m_canvas.Y(m_main.Y2()-m_canvas.YGap());
     }
   else
     {
      CElementBase::Y(m_main.Y()+YGap());
      m_canvas.Y(m_main.Y()+m_canvas.YGap());
     }
//--- 更新图形对象的坐标
   m_canvas.X_Distance(m_canvas.X());
   m_canvas.Y_Distance(m_canvas.Y());
//--- 移动嵌套控件
   int elements_total=ElementsTotal();
   for(int i=0; i<elements_total; i++)
      m_elements[i].Moving();
  }

自动检测鼠标左键的优先级

在以前版本的函数库中, 鼠标单击的优先级在每个控件类中都被硬编码。现在, 每个控件都是一个单一的图形对象, 可以实现检测优先级的自动模式。每个高于其它对象之上的图形对象都会获得更高的优先级:

图例. 6. 鼠标左键优先级的检测的直观表现。 

图例. 6. 鼠标左键优先级的检测的直观表现。

但有些控件根本没有图形对象。如前所述, 有些控件是由多个渲染对象汇集而成。在这两种情况下, 每个特定控件的类都会调整优先级。首先, 检测需要进行这种调整的控件。

没有图形对象的控件:

  • CButtonsGroup — 按钮组。
  • CDropCalendar — 调用下拉式日历的按钮。
  • CSplitButton — 切分按钮。
  • CStandardChart — 标准图表。

带有多个图形对象的控件:

  • CListView — 列表视图。
  • CTable — 表格。
  • CTextBox — 文本编辑框。

以下方法已添加到 CElement 类中, 以便设置和获取优先级:

class CElement : public CElementBase
  {
protected:
   //--- 点击鼠标左键的优先级
   long              m_zorder;
   //---
public:
   //--- 鼠标左键优先级
   long              Z_Order(void)                             const { return(m_zorder);                }
   void              Z_Order(const long z_order);
  };
//+------------------------------------------------------------------+
//| 点击鼠标左键的优先级                                                 |
//+------------------------------------------------------------------+
void CElement::Z_Order(const long z_order)
  {
   m_zorder=z_order;
   SetZorders();
  }

没有图形对象的控件是其它嵌套控件的一种控制模块。在这种控件中无需设置优先级。但是嵌套控件的优先级是相对于主控件的优先级来计算的。所以, 需要手工设置所有值才能正常工作。

无对象控件的优先级应设置为与其主控件相同:

...
//--- 优先级如同主控件, 由于控件没有自己的区域进行点击
   CElement::Z_Order(m_main.Z_Order());
...

对于所有其它控件, 创建 canvas 对象后设置优先级。窗体的值等于零, 相对于主控件, 每个按顺序创建的控件递增 1:

...
//--- 除窗体外, 所有控件的优先级高于主控件
   Z_Order((dynamic_cast<CWindow*>(&this)!=NULL)? 0 : m_main.Z_Order()+1);
...

例如, 再考察一种情况。想象一下由以下控件组成的图形界面 (按创建顺序列出):

  • 容纳控件的窗体 (CWindow)。窗体有按钮 (CButton) 用来 (1) 关闭程序, (2) 最小化/最大化窗体, 以及 (3) 工具提示, 等鞥。其它按钮可能会在将来更新的函数库中添加, 从而扩展该控件的功能。
  • 选项卡 (CTabs)。除了控件组所在的工作区域之外, 控件包含一组代表选项卡的按钮 (CButton)。
  • 带滚动条 (CScrollV) 的列表视图 (CListView), 而递增和递减按钮 (CButton) 将置于选项卡之一。
  • 另一个选项卡将包含带有水平 (CScrollH) 和垂直 (CScrollV) 的滚动条, 和多行文本框 (CTextBox)。

不需要任何设置图形界面对象优先级的操作。一切均将根据规划自动设定:

图例. 7. 定义鼠标左键优先级的示例。 

图例. 7. 定义鼠标左键优先级的示例。

窗体接收的优先级最低, 值为 0 (零)。窗体上层按钮的优先级为 1。 

每个 CTabs 类型 (选项卡) 控件的组件收到的优先级为 1, 选项卡的工作区域 — 优先级也为 1。但 CButtonsGroup 控件的值为 0, 因为它没有自己的图形对象, 它只是 CButton 类型按钮的控制模块。在 MQL 应用程序的自定义类中, 使用 CElement::MainPointer() 方法来指定主控件 (参见以下代码)。在此, 窗体 (CWindow) 将是主控件, 而 CTabs 控件只是挂载到它上面。在调用创建控件的方法之前, 应该保存指针。

...
//--- 保存指向主控件的指针
   m_tabs1.MainPointer(m_window);
...

列表视图接收的优先级为 2, 因其主控件在此处是 CTabs。必须在创建控件之前指定:

...
//--- 保存指向主控件的指针
   m_listview1.MainPointer(m_tabs1);
...

不必为滚动条指定主控件, 因为它已经在列表视图类 (CListView) 中实现。这同样适用于所有函数库内作为其它控件组件的控件。如果滚动条的主控件列表视图 (CListView) 优先级为 2, 则优先级将增加 1 (3)。而对于滚动条上的按钮, 滚动条是按钮的主控件, 则优先级将是 4

在列表视图 (CListView) 中工作的所有内容也可以在 CTextBox 控件中使用。

测试控件的应用

已实现了一款 MQL 应用程序用于测试目的。其图形界面包含函数库的所有控件, 以便您可以看到所有这些控件的工作原理。这是它看上去的样子: 

图例. 12. 测试 MQL 应用程序的图形界面。

图例. 12. 测试 MQL 应用程序的图形界面。

这款测试应用程序已附加在文章末尾, 以便更详细的研究。


结束语

这个版本的函数库与文章 图形界面 X: 多行文本框 (集成编译 13) 中的表述有显著的区别。完成了很多工作, 几乎影响到函数库的所有文件。现在函数库中的所有控件都是在单独的对象上绘制的。代码可读性有所改善, 其体积已经减少了大约 30%, 其性能已得到改进。用户报告的一些错误和缺陷已被修复。

如果您已经开始使用以前版本的函数库创建 MQL 应用程序, 建议您首先下载新版本并安装到单独的 MetaTrader 5 终端副本, 以便学习和彻底测试函数库。

用于创建图形界面的函数库, 其当前开发阶段如下图所示。这并非最终版本; 函数库将会持续发展和改善。

图例. 13. 当前开发阶段的函数库结构

图例. 13. 当前开发阶段的函数库结构

如果您在使用这些文件中提供的素材时有任何疑问, 可以参考函数库开发系列文章之一的详细描述, 或在本文的评论中提出您的问题。