图形界面 X: 时间控件, 复选框列表控件和表格排序 (集成编译 6)

Anatoli Kazharski | 13 二月, 2017

内容


概论

为了更好地理解这个函数库的目的, 请阅读首篇文章: 图形界面 I: 函数库结构的准备 (第 1 章)。您可在每章结尾处找到包含文章列表的链接。从那里, 您还可以下载当前最新开发阶段的完整版库文件。这些文件必须按照归档结构置于相同的目录中。

函数库继续增长。将要讨论的是时间和复选框列表控件。此外, CTable 类现已提供按照升序或降序对数据排序的能力。这些和其它更新将会在本文中描述。

 

时间控件

有时, 为指标或智能系统创建图形界面时也许有必要指定时间范围。有时这种必要性出现在日内交易中。日历和下拉日历 已经演示过了, 它们可用于设置日期, 但不是时间 (小时和分钟)。

我们来列举时间控件的所有组件:

  • 背景
  • 图标
  • 描述
  • 两个编辑框

图例. 1. 时间控件组件

图例. 1. 时间控件组件

 

我们来仔细观察这个控件的类。

 

创建时间控件的类

文件 TimeEdit.mqh 在创建时利用了 CTimeEdit 类, 它拥有函数库引擎中包含的所有控件的标准方法 (WndContainer.mqh 文件)。以下是可用于用户自定义的控件属性。

  • 控件背景的颜色
  • 控件激活和阻塞状态的图标
  • 图标沿两个轴 (x, y) 的边距
  • 控件的说明文本
  • 标签文本沿两个轴 (x, y) 的边距
  • 控件文本在不同状态下的颜色
  • 编辑框的宽度
  • 编辑框沿两个轴 (x, y) 的边距
  • 控件状态 (可用/阻塞)
  • 在编辑框中数值重置的模式
//+------------------------------------------------------------------+
//| 创建时间控件的类                              |
//+------------------------------------------------------------------+
class CTimeEdit : public CElement
  {
private:
   //--- 控件背景颜色
   color             m_area_color;
   //--- 控件激活和阻塞状态的图标
   string            m_icon_file_on;
   string            m_icon_file_off;
   //--- 图表空余
   int               m_icon_x_gap;
   int               m_icon_y_gap;
   //--- 控件的说明文本
   string            m_label_text;
   //--- 文本标签空余
   int               m_label_x_gap;
   int               m_label_y_gap;
   //--- 文本在不同状态下的颜色
   color             m_label_color;
   color             m_label_color_hover;
   color             m_label_color_locked;
   color             m_label_color_array[];
   //--- Size of the Edit box
   int               m_edit_x_size;
   //--- 编辑框空余
   int               m_edit_x_gap;
   int               m_edit_y_gap;
   //--- 控件状态 (可用/阻塞)
   bool              m_time_edit_state;
   //--- 数值重置的模式
   bool              m_reset_mode;
   //---
public:
   //--- (1) 背景颜色, (2) 图表空余
   void              AreaColor(const color clr)                     { m_area_color=clr;                  }
   void              IconXGap(const int x_gap)                      { m_icon_x_gap=x_gap;                }
   void              IconYGap(const int y_gap)                      { m_icon_y_gap=y_gap;                }
   //--- (1) 控件的说明文本, (2) 文本标签空余
   string            LabelText(void)                          const { return(m_label.Description());     }
   void              LabelText(const string text)                   { m_label.Description(text);         }
   void              LabelXGap(const int x_gap)                     { m_label_x_gap=x_gap;               }
   void              LabelYGap(const int y_gap)                     { m_label_y_gap=y_gap;               }
   //--- 文本标签在不同状态下的颜色
   void              LabelColor(const color clr)                    { m_label_color=clr;                 }
   void              LabelColorHover(const color clr)               { m_label_color_hover=clr;           }
   void              LabelColorLocked(const color clr)              { m_label_color_locked=clr;          }
   //--- (1) 编辑框大小, (2) 编辑框空余
   void              EditXSize(const int x_size)                    { m_edit_x_size=x_size;              }
   void              EditXGap(const int x_gap)                      { m_edit_x_gap=x_gap;                }
   void              EditYGap(const int y_gap)                      { m_edit_y_gap=y_gap;                }
   //--- (1) 当按下文本标签时的重置模式, (2) 文本选择模式
   bool              ResetMode(void)                                { return(m_reset_mode);              }
   void              ResetMode(const bool mode)                     { m_reset_mode=mode;                 }
  };

若要创建时间控件, 需要五个 private 和一个 public 方法。此控件是复合的, 且需准备 CSpinEdit 控件 作为编辑框。 

class CTimeEdit : public CElement
  {
private:
   //--- 创建控件对象
   CRectLabel        m_area;
   CBmpLabel         m_icon;
   CLabel            m_label;
   CSpinEdit         m_hours;
   CSpinEdit         m_minutes;

   //---
public:
   //--- 创建控件方法
   bool              CreateTimeEdit(const long chart_id,const int subwin,const string label_text,const int x_gap,const int y_gap);
   //---
private:
   bool              CreateArea(void);
   bool              CreateIcon(void);
   bool              CreateLabel(void);
   bool              CreateHoursEdit(void);
   bool              CreateMinutesEdit(void);
   //---
public:
   //--- 返回编辑框指针
   CSpinEdit        *GetHoursEditPointer(void)                      { return(::GetPointer(m_hours));     }
   CSpinEdit        *GetMinutesEditPointer(void)                    { return(::GetPointer(m_minutes));   }

  };

为了以编程方式获取和设置编辑框中的当前值 (小时和分钟), 请使用以下代码清单中的方法: 

class CTimeEdit : public CElement
  {
public:
   //--- 获取和设置编辑框值
   int               GetHours(void)                           const { return((int)m_hours.GetValue());   }
   int               GetMinutes(void)                         const { return((int)m_minutes.GetValue()); }
   void              SetHours(const uint value)                     { m_hours.ChangeValue(value);        }
   void              SetMinutes(const uint value)                   { m_minutes.ChangeValue(value);      }
  };

下面将进一步提供控件在终端图表上的样貌示例。 


复选框列表控件

之前的一篇文章中讨论了列表视图控件 (CListView 类), 可用来从所提供的列表中选择一个项目。但有时您可能需要选择多个项目。例如, 也许需要创建一个品种或时间帧的列表, MQL 应用程序的用户可以只选择他在交易中需要的那些。

用于创建复选框列表控件的组件列表:

  1. 控件的常用背景
  2. 垂直滚动条
  3. 复选框组:
    • 背景
    • 图标
    • 文本标签

图例. 2. 复选框列表控件的组件

图例. 2. 复选框列表控件的组件

 

接下来, 我们简单地研究这种类型的列表 (CCheckBoxList) 和一种简单类型的列表 (CListView) 之间的差异, 这已在前面讨论过。

 

创建复选框列表控件的类

文件 CheckBoxList.mqh 在创建时利用了 CCheckBoxList 类, 包含了函数库中所有控件的标准方法。与 CListView 类型列表的第一个不同之处在于其列表项是由 三个图形对象 构成的 (参见以下代码)。每种对象类型均创建了单独的私有方法。

//+------------------------------------------------------------------+
//| 创建复选框列表的类                        |
//+------------------------------------------------------------------+
class CCheckBoxList : public CElement
  {
private:
   //--- 创建列表对象
   CRectLabel        m_area;
   CEdit             m_items[];
   CBmpLabel         m_checks[];
   CLabel            m_labels[];

   CScrollV          m_scrollv;
   //---
public:
   //--- 创建控件方法
   bool              CreateCheckBoxList(const long chart_id,const int subwin,const int x_gap,const int y_gap);
   //---
private:
   bool              CreateArea(void);
   bool              CreateList(void);
   bool              CreateItems(void);
   bool              CreateChecks(void);
   bool              CreateLabels(void);

   bool              CreateScrollV(void);
  };

除了项目 (项目描述) 的值之外, 还需要复选框状态 (开/关) 的数组。还需要通过列表的指定索引设置和获取数值的方法: 

class CCheckBoxList : public CElement
  {
private:
   //--- 列表中复选框数值和状态的数组
   string            m_item_value[];
   bool              m_item_state[];
   //---
public:
   //--- 返回/保存 (1) 状态和 (2) 指定索引处的列表项目文本
   void              SetItemState(const uint item_index,const bool state);
   void              SetItemValue(const uint item_index,const string value);
   bool              GetItemState(const uint item_index);
   string            GetItemValue(const uint item_index);
  };
//+------------------------------------------------------------------+
//| 设置状态                                                |
//+------------------------------------------------------------------+
void CCheckBoxList::SetItemState(const uint item_index,const bool state)
  {
   uint array_size=::ArraySize(m_item_state);
//--- 如果列表中没有项目, 报告
   if(array_size<1)
      ::Print(__FUNCTION__," > 如果列表内有至少一个项目, 则此方法被调用!");
//--- 范围超出的情况下进行调整
   uint check_index=(item_index>=array_size)?array_size-1 : item_index;
//--- 保存数值
   m_item_state[check_index]=state;
//--- 沿滚动条移动列表
   ShiftList();
  }
//+------------------------------------------------------------------+
//| 获取复选框列表状态                               |
//+------------------------------------------------------------------+
bool CCheckBoxList::GetItemState(const uint item_index)
  {
   uint array_size=::ArraySize(m_item_state);
//--- 如果列表中没有项目, 报告
   if(array_size<1)
      ::Print(__FUNCTION__," > 如果列表内有至少一个项目, 则此方法被调用!");
//--- 范围超出的情况下进行调整
   uint check_index=(item_index>=array_size)?array_size-1 : item_index;
//--- 保存数值
   return(m_item_state[check_index]);
  }

与列表控制相关的方法已相应地进行了修改。您可以自行评估它们。 


表格排序

如果应用程序的图形界面使用带数据的表格, 有时可能需要根据用户指定的列对它们进行排序。在许多图形界面的实现中, 会以如下方式实现: 通过点击列标题对数据进行排序。首次单击标题按照升序对数据进行排序, 即从最小值到最大值。第二次单击按照降序对数据进行排序, 即从最大值到最小值。

下面的屏幕截图显示来自 MetaEditor 的工具箱窗口, 其中含有一个包含三列的表格。表格根据第三列 (包含日期) 进行排序 (降序)。

图例. 3. 拥有排序数据的表格示例

图例. 3. 拥有排序数据的表格示例

 

在列标题中显示典型的箭头作为数据排序的标记。如果方向向下, 如上面截图所示, 那么数据按照降序排序, 反之亦然。

因此, 将在本文中完成 CTable 类型表格的排序。它已经包含列标题的启用能力, 而不仅只需要使其具有交互性。首先, 当鼠标悬浮在标题时, 以及当点击它们时, 有必要改变标题的颜色。为此, 向 CTable类中添加字段和方法, 以便设置不同状态下列标题的颜色 (请参阅下面的代码)。

class CTable : public CElement
  {
private:
   //--- 标题背景色
   color             m_headers_color;
   color             m_headers_color_hover;
   color             m_headers_color_pressed;

   //---
public:
   //--- 标题背景色
   void              HeadersColor(const color clr)                              { m_headers_color=clr;                     }
   void              HeadersColorHover(const color clr)                         { m_headers_color_hover=clr;               }
   void              HeadersColorPressed(const color clr)                       { m_headers_color_pressed=clr;             }

  };

由用户决定表格是否需要排序功能。省缺情况下将禁用排序模式。若要启用它, 使用 CTable::IsSortMode() 方法。 

class CTable : public CElement
  {
private:
   //--- 数据排序模式依据列
   bool              m_is_sort_mode;
   //---
public:
   //--- 数据排序模式
   void              IsSortMode(const bool flag)                                { m_is_sort_mode=flag;                     }
  };

私有 CTable::HeaderColorByHover() 方法将用于鼠标光标悬浮时更改标题颜色。它在控件的事件处理程序中被调用。 

class CTable : public CElement
  {
private:
   //--- 当鼠标光标悬浮时更改表个标题颜色
   void              HeaderColorByHover(void);
  };
//+------------------------------------------------------------------+
//| 当鼠标光标悬浮时更改表个标题颜色     |
//+------------------------------------------------------------------+
void CTable::HeaderColorByHover(void)
  {
//--- 如果禁用列数据排序模式, 离开
   if(!m_is_sort_mode || !m_fix_first_row)
      return;
//---
   for(uint c=0; c<m_visible_columns_total; c++)
     {
      //--- 检查当前聚焦的标题
      bool condition=m_mouse.X()>m_columns[c].m_rows[0].X() && m_mouse.X()<m_columns[c].m_rows[0].X2() &&
                     m_mouse.Y()>m_columns[c].m_rows[0].Y() && m_mouse.Y()<m_columns[c].m_rows[0].Y2();
      //---
      if(!condition)
         m_columns[c].m_rows[0].BackColor(m_headers_color);
      else
        {
         if(!m_mouse.LeftButtonState())
            m_columns[c].m_rows[0].BackColor(m_headers_color_hover);
         else
            m_columns[c].m_rows[0].BackColor(m_headers_color_pressed);
        }
     }
  }

若要为排序数据创建标记图标, 必须添加私有 CTable::CreateSignSortedData() 方法。如果在创建表格之前尚未启用排序, 则 不会创建图标。如果启用排序模式, 则将在其创建后立即 隐藏图标, 表格初始化时数据未排序。 

class CTable : public CElement
  {
private:
   //--- 创建表格对象
   CBmpLabel         m_sort_arrow;
   //---
private:
   //--- 创建表格方法
   bool              CreateSignSortedData(void);
  };
//+------------------------------------------------------------------+
//| 创建一个箭头图标作为数据排序的标志                   |
//+------------------------------------------------------------------+
bool CTable::CreateSignSortedData(void)
  {
//--- 如果禁用排序模式, 离开
   if(!m_is_sort_mode)
      return(true);

//--- 形成对象名
   string name=CElement::ProgramName()+"_table_sort_array_"+(string)CElement::Id();
//--- 坐标
   int x =(m_anchor_right_window_side)?m_columns[0].m_rows[0].X()-m_columns[0].m_rows[0].XSize()+m_sort_arrow_x_gap : m_columns[0].m_rows[0].X2()-m_sort_arrow_x_gap;
   int y =(m_anchor_bottom_window_side)?CElement::Y()-m_sort_arrow_y_gap : CElement::Y()+m_sort_arrow_y_gap;
//--- 如果未指定箭头的图标, 请设置默认值
   if(m_sort_arrow_file_on=="")
      m_sort_arrow_file_on="Images\\EasyAndFastGUI\\Controls\\SpinInc.bmp";
   if(m_sort_arrow_file_off=="")
      m_sort_arrow_file_off="Images\\EasyAndFastGUI\\Controls\\SpinDec.bmp";
//--- 设置对象
   if(!m_sort_arrow.Create(m_chart_id,name,m_subwin,x,y))
      return(false);
//--- 设置属性
   m_sort_arrow.BmpFileOn("::"+m_sort_arrow_file_on);
   m_sort_arrow.BmpFileOff("::"+m_sort_arrow_file_off);
   m_sort_arrow.Corner(m_corner);
   m_sort_arrow.GetInteger(OBJPROP_ANCHOR,m_anchor);
   m_sort_arrow.Selectable(false);
   m_sort_arrow.Z_Order(m_zorder);
   m_sort_arrow.Tooltip("\n");
//--- 保存坐标
   m_sort_arrow.X(x);
   m_sort_arrow.Y(y);
//--- 保存大小 (对象)
   m_sort_arrow.XSize(m_sort_arrow.X_Size());
   m_sort_arrow.YSize(m_sort_arrow.Y_Size());
//--- 距边缘空余
   m_sort_arrow.XGap((m_anchor_right_window_side)?x : x-m_wnd.X());
   m_sort_arrow.YGap((m_anchor_bottom_window_side)?y : y-m_wnd.Y());
//--- 隐藏对象
   m_sort_arrow.Timeframes(OBJ_NO_PERIODS);
//--- 保存对象指针
   CElement::AddToArray(m_sort_arrow);
   return(true);
  }

表格单元的数值和属性的结构如果是实数, 则必须由带 小数位数 的数组补充, 以及表格每列的数据类型字段

class CTable : public CElement
  {
private:
   //--- 表格数值和属性的数组
   struct TOptions
     {
      ENUM_DATATYPE     m_type;
      string            m_vrows[];
      uint              m_digits[];
      ENUM_ALIGN_MODE   m_text_align[];
      color             m_text_color[];
      color             m_cell_color[];
     };
   TOptions          m_vcolumns[];
  };

当在表格单元中输入数值时, 省缺情况下, 其小数位数设置为零: 

class CTable : public CElement
  {
public:
   //--- 设置指定表格单元的数值
   void              SetValue(const uint column_index,const uint row_index,const string value="",const uint digits=0);
  };

若要设置特定表格列中的数据类型, 以及获取类型, 请使用 CTable::DataType() 方法:

class CTable : public CElement
  {
public:
   //--- 获取/设置数据类型
   ENUM_DATATYPE     DataType(const uint column_index)                          { return(m_vcolumns[column_index].m_type); }
   void              DataType(const uint column_index,const ENUM_DATATYPE type) { m_vcolumns[column_index].m_type=type;    }
  };

在创建表格之前, 应指定列和行的总数。m_type 字段和 m_digits 数组使用省缺数值进行初始化。起初, 所有列均有 字符串 (TYPE_STRING) 类型, 所有单元的小数位数为 。 

//+------------------------------------------------------------------+
//| 设置表格大小                                        |
//+------------------------------------------------------------------+
void CTable::TableSize(const uint columns_total,const uint rows_total)
  {
//--- 至少要有一列
   m_columns_total=(columns_total<1) ?1 : columns_total;
//--- 至少要有两行
   m_rows_total=(rows_total<2) ?2 : rows_total;
//--- 设置列数组的大小
   ::ArrayResize(m_vcolumns,m_columns_total);
//--- 设置行数组的大小
   for(uint i=0; i<m_columns_total; i++)
     {
      ::ArrayResize(m_vcolumns[i].m_vrows,m_rows_total);
      ::ArrayResize(m_vcolumns[i].m_digits,m_rows_total);
      ::ArrayResize(m_vcolumns[i].m_text_align,m_rows_total);
      ::ArrayResize(m_vcolumns[i].m_text_color,m_rows_total);
      ::ArrayResize(m_vcolumns[i].m_cell_color,m_rows_total);
      //--- 使用省缺值初始化单元背景颜色数组
      m_vcolumns[i].m_type=TYPE_STRING;
      ::ArrayInitialize(m_vcolumns[i].m_digits,0);
      ::ArrayInitialize(m_vcolumns[i].m_text_align,m_align_mode);
      ::ArrayInitialize(m_vcolumns[i].m_cell_color,m_cell_color);
      ::ArrayInitialize(m_vcolumns[i].m_text_color,m_cell_text_color);
     }
  }

表格数组的排序需要多个私有方法, 它们将执行以下操作:

  • 排序算法。
  • 比较指定条件下的值。
  • 交换数组元素的值。

交换数组元素的值使用 CTable::Swap() 方法完成。在此, 交换通过表格行直接完成。不仅要交换单元值, 而且文本颜色也要交换。 

class CTable : public CElement
  {
private:
   //--- 指定单元数值交换
   void              Swap(uint c,uint r1,uint r2);
  };
//+------------------------------------------------------------------+
//| 交换元素                                                |
//+------------------------------------------------------------------+
void CTable::Swap(uint c,uint r1,uint r2)
  {
//--- 在循环里迭代所有列
   for(uint i=0; i<m_columns_total; i++)
     {
      //--- 交换文本
      string temp_text          =m_vcolumns[i].m_vrows[r1];
      m_vcolumns[i].m_vrows[r1] =m_vcolumns[i].m_vrows[r2];
      m_vcolumns[i].m_vrows[r2] =temp_text;
      //--- 交换文本颜色
      color temp_text_color          =m_vcolumns[i].m_text_color[r1];
      m_vcolumns[i].m_text_color[r1] =m_vcolumns[i].m_text_color[r2];
      m_vcolumns[i].m_text_color[r2] =temp_text_color;
     }
  }

为了正确排序, 应使用与 m_type 字段中指定的类型转换后的数值进行比较。为此目的, 创建了一个单独的 CTable::CheckSortCondition() 方法。

class CTable : public CElement
  {
private:
   //--- 检查排序条件
   bool              CheckSortCondition(uint column_index,uint row_index,const string check_value,const bool direction);
  };
//+------------------------------------------------------------------+
//| 按照指定条件比较数值          |
//+------------------------------------------------------------------+
//| 条件: true (>), false (<)                                   |
//+------------------------------------------------------------------+
bool CTable::CheckSortCondition(uint column_index,uint row_index,const string check_value,const bool direction)
  {
   bool condition=false;
//---
   switch(m_vcolumns[column_index].m_type)
     {
      case TYPE_STRING :
        {
         string v1=m_vcolumns[column_index].m_vrows[row_index];
         string v2=check_value;
         condition=(direction)?v1>v2 : v1<v2;
         break;
        }
      //---
      case TYPE_DOUBLE :
        {
         double v1=double(m_vcolumns[column_index].m_vrows[row_index]);
         double v2=double(check_value);
         condition=(direction)?v1>v2 : v1<v2;
         break;
        }
      //---
      case TYPE_DATETIME :
        {
         datetime v1=::StringToTime(m_vcolumns[column_index].m_vrows[row_index]);
         datetime v2=::StringToTime(check_value);
         condition=(direction)?v1>v2 : v1<v2;
         break;
        }
      //---
      default :
        {
         long v1=(long)m_vcolumns[column_index].m_vrows[row_index];
         long v2=(long)check_value;
         condition=(direction)?v1>v2 : v1<v2;
         break;
        }
     }
//---
   return(condition);
  }

CTable::Swap() 和 CTable::CheckSortCondition() 方法将会在排序算法中使用。我们来看看已经选择了哪种特定算法来对数据进行排序。已经有十种算法经过了测试, 包括使用 ArraySort() 函数的 MQL 标准排序:

  • MQL 排序
  • 选择排序
  • 气泡排序
  • 鸡尾酒摇瓶排序
  • 插入排序
  • 贝壳排序
  • 二叉树排序
  • 快速排序
  • 堆栈排序
  • 合并排序

这些方法已在大小为 100,000 (十万) 个元素的数组上测试过。数组已使用随机数初始化。测试结果显示在下面的直方图中:

图例. 4. 不同排序方法的测试结果图表。数组大小为 100,000 个元素。

图例. 4. 不同排序方法的测试结果图表。数组大小为 100,000 个元素。


快速排序方法被证明是最快的。此方法的代码取自标准函数库 – QuickSort() 方法。在此测试中, 它甚至展示出比 ArraySort() 函数更加的结果。使用 GetMicrosecondCount() 函数测量算法操作时间以获得更高的精度。我们只留下显示最佳结果的算法 (小于 1 秒)。 

图例. 5. 数组大小为 100,000 个元素的排序方法的最佳测试结果图表。

图例. 5. 数组大小为 100,000 个元素的排序方法的最佳测试结果图表。


将数组大小增加 10 倍, 即现在将对 1000000 (一百万) 个元素的数组进行排序。

图例. 6. 数组大小为 1,000,000 个元素的排序方法的测试结果图表。

图例. 6. 数组大小为 1,000,000 个元素的排序方法的测试结果图表。


标准算法 — the ArraySort() 函数在本轮测试中是最好的。快速排序方法证明了自身只是略差, 因此, 它将被选择。ArraySort() 不适合此次任务, 因为: (1) 需要在两个方向上排序的能力, (2) CTable 类使用一个结构数组, 且 (3) 不仅要控制表格单元中数值的位置, 还有它们的其它属性。 

应将 ENUM_SORT_MODE 枚举添加到 Enums.mqh 文件中, 以便进行双向排序:

  • SORT_ASCEND – 升序。
  • SORT_DESCEND – 降序。
//+------------------------------------------------------------------+
//| 排序模式枚举                                 |
//+------------------------------------------------------------------+
enum ENUM_SORT_MODE
  {
   SORT_ASCEND  =0,
   SORT_DESCEND =1
  };

CTable::QuickSort() 方法中使用的当前快速排序算法版本如下面的代码所示。CTable::Swap() 和 CTable::CheckSortCondition() 方法, 已在本文前面展示过, 以黄色高亮显示。 

class CTable : public CElement
  {
private:
   //--- 快速排序方法
   void              QuickSort(uint beg,uint end,uint column,const ENUM_SORT_MODE mode=SORT_ASCEND);
  };
//+------------------------------------------------------------------+
//| 快速排序算法                                              |
//+------------------------------------------------------------------+
void CTable::QuickSort(uint beg,uint end,uint column,const ENUM_SORT_MODE mode=SORT_ASCEND)
  {
   uint   r1         =beg;
   uint   r2         =end;
   uint   c          =column;
   string temp       =NULL;
   string value      =NULL;
   uint   data_total =m_rows_total-1;
//--- 在左侧索引小于最右侧索引时运行算法
   while(r1<end)
     {
      //---从行的中间取值
      value=m_vcolumns[c].m_vrows[(beg+end)>>1];
      //--- 在左侧索引小于找到的右侧索引时运行算法
      while(r1<r2)
        {
         //--- 在查找指定条件的值时向右侧移动索引
         while(CheckSortCondition(c,r1,value,(mode==SORT_ASCEND)?false : true))
           {
            //--- 检查是否超出数组范围
            if(r1==data_total)
               break;
            r1++;
           }
         //--- 在查找指定条件的值时, 将索引向左侧移动
         while(CheckSortCondition(c,r2,value,(mode==SORT_ASCEND)?true : false))
           {
            //--- 检查是否超出数组范围
            if(r2==0)
               break;
            r2--;
           }
         //--- 如果左侧索引仍然不大于右侧的索引
         if(r1<=r2)
           {
            //--- 数值交换
            Swap(c,r1,r2);
            //--- 如果已达到左侧限制
            if(r2==0)
              {
               r1++;
               break;
              }
            //---
            r1++;
            r2--;
           }
        }
      //--- 算法继续递归, 直到达到范围的开始
      if(beg<r2)
         QuickSort(beg,r2,c,mode);
      //--- 缩窄下一次迭代的范围
      beg=r1;
      r2=end;
     }
  }

所有这些方法都是私有的。现在研究公有 CTable::SortData() 方法, 设计将被调用 (1) 当点击表格列标题的头部, 或 (2) 根据 MQL 应用程序的作者编程需要, 在任何其它的时间。CTable::SortData() 应传递列索引, 省缺第一列已排序。如果指定的列首次排序, 或者最后按降序排序, 则值将按升序排序。在 数据排序 之后, 表格将被刷新。并且 CTable::SortData() 方法的最后一行将为已排序表格设置相应的箭头标记图标。 

class CTable : public CElement
  {
private:
   //--- 排序列索引 (WRONG_VALUE – 表格未排序)
   int               m_is_sorted_column_index;
   //--- 最后排序方向
   ENUM_SORT_MODE    m_last_sort_direction;
   //---
public:
   //--- 根据指定列进行数据排序
   void              SortData(const uint column_index=0);
  };
//+------------------------------------------------------------------+
//| 根据指定列进行数据排序                  |
//+------------------------------------------------------------------+
void CTable::SortData(const uint column_index=0)
  {
//--- 排序开始的索引 (考虑头部的存在)
   uint first_index=(m_fix_first_row) ?1 : 0;
//--- 数组的最后索引
   uint last_index=m_rows_total-1;
//--- 第一次它将按升序排序, 每次之后它将按相反的方向排序
   if(m_is_sorted_column_index==WRONG_VALUE || column_index!=m_is_sorted_column_index || m_last_sort_direction==SORT_DESCEND)
      m_last_sort_direction=SORT_ASCEND;
   else
      m_last_sort_direction=SORT_DESCEND;
//--- 保存最后排序的数据列的索引
   m_is_sorted_column_index=(int)column_index;
//--- 排序
   QuickSort(first_index,last_index,column_index,m_last_sort_direction);
//--- 刷新表格
   UpdateTable();
//--- 根据排序方向设置图标
   m_sort_arrow.State((m_last_sort_direction==SORT_ASCEND)?true : false);
  }

需要用于处理点击列标题的方法 — CTable::OnClickTableHeaders(), 当 数据排序模式启用时。属于此控件的对象经过所有检查之后, 在循环中确定标题 (列) 的索引, 然后 表格进行排序。在表格进行排序之后, 立即生成 具有新 ON_SORT_DATA 标识符的事件。除了此事件标识符, 消息包含 (1) 控件标识符, (2) 排序列的索引和 (3) 此列的数据类型。

class CTable : public CElement
  {
private:
   //--- 处理表格标题点击
   bool              OnClickTableHeaders(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| 处理表格标题点击                            |
//+------------------------------------------------------------------+
bool CTable::OnClickTableHeaders(const string clicked_object)
  {
//--- 如果禁用排序模式, 离开
   if(!m_is_sort_mode)
      return(false);

//--- 如果未有表格单元点击, 离开
   if(::StringFind(clicked_object,CElement::ProgramName()+"_table_edit_",0)<0)
      return(false);
//--- 从对象名中获取标识符
   int id=CElement::IdFromObjectName(clicked_object);
//--- 如果标识符不匹配, 离开
   if(id!=CElement::Id())
      return(false);
//--- 如果不是表格标题, 离开
   if(RowIndexFromObjectName(clicked_object)>0)
      return(false);
//--- 用于确定列索引
   uint column_index=0;
//--- 如果启用了固定标题模式, 平移一个索引
   int l=(m_fix_first_column) ?1 : 0;
//--- 获取当前水平滚动条的滑块位置
   int h=m_scrollh.CurrentPos()+l;
//--- 列
   for(uint c=l; c<m_visible_columns_total; c++)
     {
      //--- 如果并非在此单元按下
      if(m_columns[c].m_rows[0].Name()==clicked_object)
        {
         //--- 获取列索引
         column_index=(m_fix_first_column && c==0) ?0 : h;
         break;
        }
      //---
      h++;
     }
//--- 根据指定列进行数据排序
   SortData(column_index);
//--- 发送有关消息
   ::EventChartCustom(m_chart_id,ON_SORT_DATA,CElement::Id(),m_is_sorted_column_index,::EnumToString(DataType(column_index)));
   return(true);
  }

如果列的总数大于可视列的数量, 则在移动水平滚动条时必须调整排序表格的箭头标记位置。这可使用 CTable::ShiftSortArrow() 方法完成。

class CTable : public CElement
  {
private:
   //--- 平移已排序数据的箭头标记
   void              ShiftSortArrow(const uint column);
  };
//+------------------------------------------------------------------+
//| 将箭头平移至已排序表格列                    |
//+------------------------------------------------------------------+
void CTable::ShiftSortArrow(const uint column)
  {
//--- 如果元素未隐藏, 则显示对象
   if(CElement::IsVisible())
      m_sort_arrow.Timeframes(OBJ_ALL_PERIODS);
//--- 计算并设置坐标
   int x=m_columns[column].m_rows[0].X2()-m_sort_arrow_x_gap;
   m_sort_arrow.X(x);
   m_sort_arrow.X_Distance(x);
//--- 距边缘空余
   m_sort_arrow.XGap((m_anchor_right_window_side)?m_wnd.X2()-x : x-m_wnd.X());
  }

此方法将在 CTable::UpdateTable() 方法中, 标题平移的代码块中被调用。以下是 CTable::UpdateTable() 方法的简化版本, 含有 附加的片段。在此, 如果在第一次循环中找到排序的列, 则设置标志并移动箭头标记。循环完成后, 可能会发现存在已排序的列, 但在前一次循环中未发现。这可能意味着它已经脱离可见区域, 应该被隐藏。如果这是第一列 (索引零), 同时它是固定的且不能移动, 则为它设置箭头标记。 

//+------------------------------------------------------------------+
//| 参考最近的变化更新表格数据       |
//+------------------------------------------------------------------+
void CTable::UpdateTable(void)
  {
//...

//--- 在顶行中平移标题
   if(m_fix_first_row)
     {
      //--- 用于确定排序图标的移位
      bool is_shift_sort_arrow=false;
      //--- 列
      for(uint c=l; c<m_visible_columns_total; c++)
        {
         //--- 如果未超出数组范围
         if(h>=l && h<m_columns_total)
           {
            //--- 如果发现已排序列
            if(!is_shift_sort_arrow && m_is_sort_mode && h==m_is_sorted_column_index)
              {
               is_shift_sort_arrow=true;
               //--- 调整排序图标
               uint column=h-(h-c);
               if(column>=l && column<m_visible_columns_total)
                  ShiftSortArrow(column);
              }

            //--- 调整 (1) 数值, (2) 背景颜色, (3) 文本颜色和 (4) 文本在单元内对齐
            SetCellParameters(c,0,m_vcolumns[h].m_vrows[0],m_headers_color,m_headers_text_color,m_vcolumns[h].m_text_align[0]);
           }
         //---
         h++;
        }
      //--- 如果存在已排序表格, 但未发现
      if(!is_shift_sort_arrow && m_is_sort_mode && m_is_sorted_column_index!=WRONG_VALUE)
        {
         //--- 隐藏, 如果索引大于零
         if(m_is_sorted_column_index>0 || !m_fix_first_column)
            m_sort_arrow.Timeframes(OBJ_NO_PERIODS);
         //--- 设置首列的标题
         else
            ShiftSortArrow(0);
        }

     }
//...

  }

含有测试系统的文件可在文章结尾处下载。您可以使用它们自行测试操作。

 

其它函数库更新

作为附加功能, 此版集成编译包括以下函数库更新:

1. 现在可以为每个控件单独设置字体和字号。为此目的, 已将适当的字段和方法添加到控件的 CElement 基类中。省缺时, 使用 Calibri 字体, 字号为 8。 

class CElement
  {
protected:
   //--- 字体
   string            m_font;
   int               m_font_size;
   //---
public:
   //--- (1) 字体和 (2) 字号
   void              Font(const string font)                         { m_font=font;                          }
   string            Font(void)                                const { return(m_font);                       }
   void              FontSize(const int font_size)                   { m_font_size=font_size;                }
   int               FontSize(void)                            const { return(m_font_size);                  }
  };
//+------------------------------------------------------------------+
//| 构造器                                                      |
//+------------------------------------------------------------------+
CElement::CElement(void) : m_font("Calibri"),
                           m_font_size(8)
  {
  }

相应地, 用于创建控件的所有方法从基类中获取数值, 而字体需要指定。以下示例 CCheckBox 类的标签。在函数库的所有类中同样的事情均已完成。 

//+------------------------------------------------------------------+
//| 创建复选框的文本标签                           |
//+------------------------------------------------------------------+
bool CCheckBox::CreateLabel(void)
  {
//--- 形成对象名
   string name=CElement::ProgramName()+"_checkbox_lable_"+(string)CElement::Id();
//--- 坐标
   int x =(m_anchor_right_window_side)?m_x-m_label_x_gap : m_x+m_label_x_gap;
   int y =(m_anchor_bottom_window_side)?m_y-m_label_y_gap : m_y+m_label_y_gap;
//--- 依据状态的文本颜色
   color label_color=(m_check_button_state) ?m_label_color : m_label_color_off;
//--- 设置对象
   if(!m_label.Create(m_chart_id,name,m_subwin,x,y))
      return(false);
//--- 设置属性
   m_label.Description(m_label_text);
   m_label.Font(CElement::Font());
   m_label.FontSize(CElement::FontSize());

   m_label.Color(label_color);
   m_label.Corner(m_corner);
   m_label.Anchor(m_anchor);
   m_label.Selectable(false);
   m_label.Z_Order(m_zorder);
   m_label.Tooltip("\n");
//--- 距边缘的空余
   m_label.XGap((m_anchor_right_window_side)?x : x-m_wnd.X());
   m_label.YGap((m_anchor_bottom_window_side)?y : y-m_wnd.Y());
//--- 初始化梯度数组
   CElement::InitColorArray(label_color,m_label_color_hover,m_label_color_array);
//--- 保存对象指针
   CElement::AddToArray(m_label);
   return(true);
  }

 

2. 图形界面每个控件距表单边缘的空余现在需要直接传递到 创建控件的方法。计算将自动完成。例如, 下面的列表显示了从 CProgram 自定义类创建下拉日历的方法。 

//+------------------------------------------------------------------+
//| 创建下拉日历                                        |
//+------------------------------------------------------------------+
bool CProgram::CreateDropCalendar(const int x_gap,const int y_gap,const string text)
  {
//--- 传递对象至面板
   m_drop_calendar.WindowPointer(m_window);
//--- 附加第二个选项卡
   m_tabs.AddToElementsArray(1,m_drop_calendar);
//--- 在创建之前设置属性
   m_drop_calendar.XSize(140);
   m_drop_calendar.YSize(20);
   m_drop_calendar.AreaBackColor(clrWhite);
//--- 创建控件
   if(!m_drop_calendar.CreateDropCalendar(m_chart_id,m_subwin,text,x_gap,y_gap))
      return(false);
//--- 将控件指针加入基类
   CWndContainer::AddToElementsArray(0,m_drop_calendar);
   return(true);
  }

 


测试控件的应用程序

现在, 我们来创建一个测试 MQL 应用程序, 在此所有新的控件均可以测试。创建一个包括主菜单 (CMenuBar), 带有下拉子菜单, 状态条和两个选项卡。第一个选项卡包含一个 CTable 类型的表格, 已启用排序模式。 

表格的前三列将具有以下数据类型:

其余列省缺为 TYPE_STRING 类型。下面的屏幕截图显示了第一个选项卡上的表格图形界面的外观。 

图例. 7. 根据第二列排序 (升序) 表格的示例。

图例. 7. 根据第二列排序 (升序) 表格的示例。


在第二个选项卡上创建四个控件: 

  • 下拉日历 (CDropCalendar 类)。
  • 时间控件 (CTimeEdit 类)。
  • 复选框列表 (CCheckBoxList 类)。
  • 列表视图 (CListView 类)。

下面的屏幕截图显示了测试 MQL 应用程序的图形界面外观:

图例. 8. 第二个选项卡上的控件。

图例. 8. 第二个选项卡上的控件。


此测试应用程序的源代码在文章结尾处提供。 

 

结论

目前, 创建图形界面的函数库的一般示意图如下所示:

图例. 9. 当前开发阶段的库结构

图例. 9. 当前开发阶段的库结构


这并非关于图形界面系列的最后一篇文章。我们将继续改进它, 并为它补充新的功能。您可以从下面下载最新版本的函数库和文件进行测试。

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