下载MetaTrader 5

图形界面 X: 排序、重建表格和单元格中的控件 (集成编译 11)

24 四月 2017, 10:11
Anatoli Kazharski
0
1 581

内容

‌‌

概论

首篇文章 图形界面 I: 函数库结构的准备 (第 1 章) 详细研究了函数库的作用。您将在每章结尾处找到文章列表及链接。从那里, 您还可以下载当前开发阶段的函数库完整版本。文件必须位于与存档相同的目录中。

我们继续开发渲染表格。我们来列举要加入的新功能。

  • 表格数据排序。
  • 管理列和行的数量: 添加和删除指定索引处的列和行; 彻底清理表格 (只留下一列和一行); 重建表格 (清除表格并设置新维度)。
  • 在图形界面上扩展用户管理: 添加双击事件的处理。
  • 我们将开始向表格单元添加控件: 复选框和按钮。

若您已在函数库的帮助下创建了图形界面, 并使用 CTable 类型的表格来显示数据, 现在建议您转到 CCanvasTable 类型的表格 。从本文开始, 它完全兼容这个函数库中其它类型的表格, 在某些方面甚至超越了它们。

表格排序

用于排序表数据的大多数方法与 CTable 中的相同。文章 图形界面 X: 时间控件, 复选框列表控件和表格排序 描述了所有这些是如何编排的细节。在此, 将会简要提及有关 CCanvasTable 类型表格相关的变化和附加功能。

绘制排序表格符号需要一个具有两个元素的 CTImage 类型的 静态数组。它将包含用于显示排序方向的图像路径。如果用户没有设置自定义图像, 则 将使用省缺图标。使用省缺值进行初始化, 并在创建标题的 CCanvasTable::CreateHeaders() 方法中执行 填充图像数组。 

//+------------------------------------------------------------------+
//| 创建渲染表格的类                                                    |
//+------------------------------------------------------------------+
class CCanvasTable : public CElement
  {
private:
   //--- 数据已排序的符号图标
   CTImage           m_sort_arrows[2];
   //---
public:
   //--- 设置已排序数据符号的图标
   void              SortArrowFileAscend(const string path)  { m_sort_arrows[0].m_bmp_path=path; }
   void              SortArrowFileDescend(const string path) { m_sort_arrows[1].m_bmp_path=path; }
  };
//+------------------------------------------------------------------+
//| 构造器                                                            |
//+------------------------------------------------------------------+
CCanvasTable::CCanvasTable(void) 
  {
...
//--- 初始化数据排序符号的结构
   m_sort_arrows[0].m_bmp_path="";
   m_sort_arrows[1].m_bmp_path="";
  }
//+------------------------------------------------------------------+
//| 创建表格标题                                                       |
//+------------------------------------------------------------------+
bool CCanvasTable::CreateHeaders(void)
  {
//--- 如果标题被禁用, 离开
   if(!m_show_headers)
      return(true);
//--- 形成对象名称
   string name=CElementBase::ProgramName()+"_table_headers_"+(string)CElementBase::Id();
//--- 坐标
   int x =m_x+1;
   int y =m_y+1;
//--- 将图标定义为表格排序的可能性符号
   if(m_sort_arrows[0].m_bmp_path=="")
      m_sort_arrows[0].m_bmp_path="::Images\\EasyAndFastGUI\\Controls\\SpinInc.bmp";
   if(m_sort_arrows[1].m_bmp_path=="")
      m_sort_arrows[1].m_bmp_path="::Images\\EasyAndFastGUI\\Controls\\SpinDec.bmp";
//---
   for(int i=0; i<2; i++)
     {
      ::ResetLastError();
      if(!::ResourceReadImage(m_sort_arrows[i].m_bmp_path,m_sort_arrows[i].m_image_data,
         m_sort_arrows[i].m_image_width,m_sort_arrows[i].m_image_height))
        {
         ::Print(__FUNCTION__," > 错误: ",::GetLastError());
        }
     }
//--- 创建对象
   ::ResetLastError();
   if(!m_headers.CreateBitmapLabel(m_chart_id,m_subwin,name,x,y,m_table_x_size,m_header_y_size,COLOR_FORMAT_ARGB_NORMALIZE))
     {
      ::Print(__FUNCTION__," > 创建绘制表格标题的画板失败: ",::GetLastError());
      return(false);
     }
//--- 挂载到图表
//--- 设置属性
//--- 坐标
//--- 保存大小
//--- 从面板边缘的空隙
//--- 保存对象指针
//--- 设置可视区域大小
//--- 设置图像内沿 X 和 Y 轴的框架偏移
...
   return(true);
  }

CCanvasTable::DrawSignSortedData() 方法将用来绘制已排序数组的符号。仅当 (1) 排序模式已启用 且 (2) 表格数据已执行排序 时才会绘制此元素。偏移量可以使用 CCanvasTable::SortArrowXGap() 和 CCanvasTable::SortArrowYGap() 方法进行控制。该图标表示 按升序进行排序的索引为 0, 而降序为 – 1。然后进入绘制图像的双重循环。此主题已经 详细讨论过

class CCanvasTable : public CElement
  {
private:
   //--- 已排序数据符号图标的偏移量
   int               m_sort_arrow_x_gap;
   int               m_sort_arrow_y_gap;
   //---
public:
   //--- 已排序表格符号的偏移量
   void              SortArrowXGap(const int x_gap)          { m_sort_arrow_x_gap=x_gap;         }
   void              SortArrowYGap(const int y_gap)          { m_sort_arrow_y_gap=y_gap;         }
   //---
private:
   //--- 绘制表格排序可能性符号
   void              DrawSignSortedData(void);
  };
//+------------------------------------------------------------------+
//| 构造器                                                            |
//+------------------------------------------------------------------+
CCanvasTable::CCanvasTable(void) : m_sort_arrow_x_gap(20),
                                   m_sort_arrow_y_gap(6)
  {
...
  }
//+------------------------------------------------------------------+
//| 绘制表格排序可能性符号                                               |
//+------------------------------------------------------------------+
void CCanvasTable::DrawSignSortedData(void)
  {
//--- 如果 (1) 排序被禁用, 或者 (2) 尚未执行, 离开
   if(!m_is_sort_mode || m_is_sorted_column_index==WRONG_VALUE)
      return;
//--- 计算坐标
   int x =m_columns[m_is_sorted_column_index].m_x2-m_sort_arrow_x_gap;
   int y =m_sort_arrow_y_gap;
//--- 所选择的排序方向图标
   int image_index=(m_last_sort_direction==SORT_ASCEND)? 0 : 1;
//--- 绘制
   for(uint ly=0,i=0; ly<m_sort_arrows[image_index].m_image_height; ly++)
     {
      for(uint lx=0; lx<m_sort_arrows[image_index].m_image_width; lx++,i++)
        {
         //--- 如果没有颜色, 转到下一个像素
         if(m_sort_arrows[image_index].m_image_data[i]<1)
            continue;
         //--- 获取下层 (标题背景) 的颜色和图标指定像素的颜色
         uint background  =m_headers.PixelGet(x+lx,y+ly);
         uint pixel_color =m_sort_arrows[image_index].m_image_data[i];
         //--- 混色
         uint foreground=::ColorToARGB(m_clr.BlendColors(background,pixel_color));
         //--- 绘制叠加图标的像素
         m_headers.PixelSet(x+lx,y+ly,foreground);
        }
     }
  }

CCanvasTable::Swap() 方法用于在排序时交换表格数值。这里的区别在于, 不仅需要移动在单元格中显示的文本和/或图像, 还需要移动大量的单元格属性。出于便利, 且消除重复的代码片段, 当移动图像时将需要辅助 CCanvasTable::ImageCopy() 方法。它要传递了两个 CTImage 类型的数组 — 接收器和数据源。复制的图像索引作为第三个参数传递, 因为单个单元可能包含多个图像。

class CCanvasTable : public CElement
  {
private:
   //--- 从一个数组复制图像到另一个数组
   void              ImageCopy(CTImage &destination[],CTImage &source[],const int index);
  };
//+------------------------------------------------------------------+
//| 从一个数组复制图像到另一个数组                                         |
//+------------------------------------------------------------------+
void CCanvasTable::ImageCopy(CTImage &destination[],CTImage &source[],const int index)
  {
//--- 复制图像像素
   ::ArrayCopy(destination[index].m_image_data,source[index].m_image_data);
//--- 复制图像属性
   destination[index].m_image_width  =source[index].m_image_width;
   destination[index].m_image_height =source[index].m_image_height;
   destination[index].m_bmp_path     =source[index].m_bmp_path;
  }

CCanvasTable::Swap() 的结果代码如下所列。在移动图像之前, 首先需要检查它们是否存在, 如果没有, 那么转到下一列如果其中一个单元包含图像, 则使用 CCanvasTable::ImageCopy() 方法。此操作与移动单元其它属性的情况相同。即, 首先保存第一个单元的属性。然后, 将该属性从第二个单元复制到第一个单元的位置。最后, 将先前保存的第一单元的值移动到第二单元的位置。

class CCanvasTable : public CElement
  {
private:
   //--- 交换指定单元中的数值
   void              Swap(uint r1,uint r2);
  };
//+------------------------------------------------------------------+
//| 交换元素                                                           |
//+------------------------------------------------------------------+
void CCanvasTable::Swap(uint r1,uint r2)
  {
//--- 在一次循环中迭代所有列
   for(uint c=0; c<m_columns_total; c++)
     {
      //--- 交换全部文本
      string temp_text                    =m_columns[c].m_rows[r1].m_full_text;
      m_columns[c].m_rows[r1].m_full_text =m_columns[c].m_rows[r2].m_full_text;
      m_columns[c].m_rows[r2].m_full_text =temp_text;
      //--- 交换缩写文本
      temp_text                            =m_columns[c].m_rows[r1].m_short_text;
      m_columns[c].m_rows[r1].m_short_text =m_columns[c].m_rows[r2].m_short_text;
      m_columns[c].m_rows[r2].m_short_text =temp_text;
      //--- 交换小数位数
      uint temp_digits                 =m_columns[c].m_rows[r1].m_digits;
      m_columns[c].m_rows[r1].m_digits =m_columns[c].m_rows[r2].m_digits;
      m_columns[c].m_rows[r2].m_digits =temp_digits;
      //--- 交换文本颜色
      color temp_text_color                =m_columns[c].m_rows[r1].m_text_color;
      m_columns[c].m_rows[r1].m_text_color =m_columns[c].m_rows[r2].m_text_color;
      m_columns[c].m_rows[r2].m_text_color =temp_text_color;
      //--- 交换所选图标的索引
      int temp_selected_image                  =m_columns[c].m_rows[r1].m_selected_image;
      m_columns[c].m_rows[r1].m_selected_image =m_columns[c].m_rows[r2].m_selected_image;
      m_columns[c].m_rows[r2].m_selected_image =temp_selected_image;
      //--- 检查单元是否包含图像
      int r1_images_total=::ArraySize(m_columns[c].m_rows[r1].m_images);
      int r2_images_total=::ArraySize(m_columns[c].m_rows[r2].m_images);
      //--- 如果两个单元都没有图像, 转到下一列
      if(r1_images_total<1 && r2_images_total<1)
         continue;
      //--- 交换图像
      CTImage r1_temp_images[];
      //---
      ::ArrayResize(r1_temp_images,r1_images_total);
      for(int i=0; i<r1_images_total; i++)
         ImageCopy(r1_temp_images,m_columns[c].m_rows[r1].m_images,i);
      //---
      ::ArrayResize(m_columns[c].m_rows[r1].m_images,r2_images_total);
      for(int i=0; i<r2_images_total; i++)
         ImageCopy(m_columns[c].m_rows[r1].m_images,m_columns[c].m_rows[r2].m_images,i);
      //---
      ::ArrayResize(m_columns[c].m_rows[r2].m_images,r1_images_total);
      for(int i=0; i<r1_images_total; i++)
         ImageCopy(m_columns[c].m_rows[r2].m_images,r1_temp_images,i);
     }
  }

单击其中一个标题调用 CCanvasTable::OnClickHeaders() 方法, 该方法启动数据排序。放在这个类中比之在 CTable 类型的表格中要简单得多: 比较并自我观察。

class CCanvasTable : public CElement
  {
private:
   //--- 处理在标题上单击
   bool              OnClickHeaders(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| 处理在标题上单击                                                    |
//+------------------------------------------------------------------+
bool CCanvasTable::OnClickHeaders(const string clicked_object)
  {
//--- 如果 (1) 排序模式被禁用,  或 (2) 在更改列宽度的过程中, 离开
   if(!m_is_sort_mode || m_column_resize_control!=WRONG_VALUE)
      return(false);
//--- 如果滚动条激活, 离开
   if(m_scrollv.ScrollState() || m_scrollh.ScrollState())
      return(false);
//--- 如果它有一个不同的对象名称, 离开
   if(m_headers.Name()!=clicked_object)
      return(false);
//--- 用于确定列索引
   uint column_index=0;
//--- 获取鼠标光标下方的相对 X 坐标
   int x=m_mouse.RelativeX(m_headers);
//--- 确定点击的标题
   for(uint c=0; c<m_columns_total; c++)
     {
      //--- 如果找到标题, 保存其索引
      if(x>=m_columns[c].m_x && x<=m_columns[c].m_x2)
        {
         column_index=c;
         break;
        }
     }
//--- 根据指定的列排序数据
   SortData(column_index);
//--- 发送有关消息
   ::EventChartCustom(m_chart_id,ON_SORT_DATA,CElementBase::Id(),m_is_sorted_column_index,::EnumToString(DataType(column_index)));
   return(true);
  }

其余的数据排序方法保持不变, 与 之前研究的 没有区别。因此, 只在 CCanvasTable 类中包含这些字段和方法声明的列表:

class CCanvasTable : public CElement
  {
private:
   //--- 按照列排序数据的模式
   bool              m_is_sort_mode;
   //--- 排序列的索引 (WRONG_VALUE – 表格未排序)
   int               m_is_sorted_column_index;
   //--- 最后排序方向
   ENUM_SORT_MODE    m_last_sort_direction;
   //---
public:
   //--- 数据排序模式
   void              IsSortMode(const bool flag)             { m_is_sort_mode=flag;              }

   //--- 获取/设置数据类型
   ENUM_DATATYPE     DataType(const uint column_index);
   void              DataType(const uint column_index,const ENUM_DATATYPE type);
   //--- 根据指定的列排序数据
   void              SortData(const uint column_index=0);
   //---
private:
   //--- 处理在标题上单击
   bool              OnClickHeaders(const string clicked_object);

   //--- 快速排序方法
   void              QuickSort(uint beg,uint end,uint column,const ENUM_SORT_MODE mode=SORT_ASCEND);
   //--- 检查排序条件
   bool              CheckSortCondition(uint column_index,uint row_index,const string check_value,const bool direction);
  };


在这种类型的表格中进行排序展示如下:

 图例. 1. 在 CCanvasTable 类型的表格中排序的演示。

图例. 1. 在 CCanvasTable 类型的表格中排序的演示。


添加和删除列和行

前文之一 已研究过为 CTable 类型的表格添加/删除列和行的方法。该版本只允许在表格的末尾添加。这个麻烦现在将被修改: 我们可以按照指示的索引添加列或行。 

示例: 在表格的开头添加一列数据: 即, 新列必须为第一个 (索引 0)。列数组必须增加一个元素, 而所有表格单元的属性和值必须向右移动一格, 只留下新的空白列单元。在表格中添加新行时, 适用相同的原则。 

当需要删除列或行时, 运用相反的原理操作。我们尝试从前面的例子中删除第一列: 首先, 单元格的数据和属性将被左移一个元素, 之后, 列数组的大小将减少一个元素。

此处实现的辅助方法可便利地创建添加/删除列和行的方法。CCanvasTable::ColumnInitialize() 和 CCanvasTable::CellInitialize() 方法将用于初始化单元格, 使用省缺值添加列和行。

class CCanvasTable : public CElement
  {
private:
   //--- 使用省缺值初始化指定的列
   void              ColumnInitialize(const uint column_index);
   //--- 使用省缺值初始化指定的单元格
   void              CellInitialize(const uint column_index,const uint row_index);
  };
//+------------------------------------------------------------------+
//| 使用省缺值初始化指定的列                                              |
//+------------------------------------------------------------------+
void CCanvasTable::ColumnInitialize(const uint column_index)
  {
//--- 使用省缺值初始化指定列的属性
   m_columns[column_index].m_x              =0;
   m_columns[column_index].m_x2             =0;
   m_columns[column_index].m_width          =100;
   m_columns[column_index].m_type           =TYPE_STRING;
   m_columns[column_index].m_text_align     =ALIGN_CENTER;
   m_columns[column_index].m_text_x_offset  =m_text_x_offset;
   m_columns[column_index].m_image_x_offset =m_image_x_offset;
   m_columns[column_index].m_image_y_offset =m_image_y_offset;
   m_columns[column_index].m_header_text    ="";
  }
//+------------------------------------------------------------------+
//| 使用省缺值初始化指定的单元格                                           |
//+------------------------------------------------------------------+
void CCanvasTable::CellInitialize(const uint column_index,const uint row_index)
  {
   m_columns[column_index].m_rows[row_index].m_full_text      ="";
   m_columns[column_index].m_rows[row_index].m_short_text     ="";
   m_columns[column_index].m_rows[row_index].m_selected_image =0;
   m_columns[column_index].m_rows[row_index].m_text_color     =m_cell_text_color;
   m_columns[column_index].m_rows[row_index].m_digits         =0;
   m_columns[column_index].m_rows[row_index].m_type           =CELL_SIMPLE;
//--- 省缺情况下, 单元格不包含图像
   ::ArrayFree(m_columns[column_index].m_rows[row_index].m_images);
  }

要将列的属性复制到另一列, 必须使用 CCanvasTable::ColumnCopy() 方法。其代码在下面的列表中提供:

class CCanvasTable : public CElement
  {
private:
   //--- 将指定列 (源) 复制到新位置 (目的)。
   void              ColumnCopy(const uint destination,const uint source);
  };
//+------------------------------------------------------------------+
//| 将指定列 (源) 复制到新位置 (目的)。                                    |
//+------------------------------------------------------------------+
void CCanvasTable::ColumnCopy(const uint destination,const uint source)
  {
   m_columns[destination].m_header_text    =m_columns[source].m_header_text;
   m_columns[destination].m_width          =m_columns[source].m_width;
   m_columns[destination].m_type           =m_columns[source].m_type;
   m_columns[destination].m_text_align     =m_columns[source].m_text_align;
   m_columns[destination].m_text_x_offset  =m_columns[source].m_text_x_offset;
   m_columns[destination].m_image_x_offset =m_columns[source].m_image_x_offset;
   m_columns[destination].m_image_y_offset =m_columns[source].m_image_y_offset;
  }

CCanvasTable::CellCopy() 方法将指定的单元格复制到另一处。为此, 需要传递 接收单元格的列、行索引 以及 源单元格的列、行索引

class CCanvasTable : public CElement
  {
private:
   //--- 将指定单元格 (源) 复制到新位置 (目的)。
   void              CellCopy(const uint column_dest,const uint row_dest,const uint column_source,const uint row_source);
  };
//+------------------------------------------------------------------+
//| 将指定单元格 (源) 复制到新位置 (目的)。                                |
//+------------------------------------------------------------------+
void CCanvasTable::CellCopy(const uint column_dest,const uint row_dest,const uint column_source,const uint row_source)
  {
   m_columns[column_dest].m_rows[row_dest].m_type           =m_columns[column_source].m_rows[row_source].m_type;
   m_columns[column_dest].m_rows[row_dest].m_digits         =m_columns[column_source].m_rows[row_source].m_digits;
   m_columns[column_dest].m_rows[row_dest].m_full_text      =m_columns[column_source].m_rows[row_source].m_full_text;
   m_columns[column_dest].m_rows[row_dest].m_short_text     =m_columns[column_source].m_rows[row_source].m_short_text;
   m_columns[column_dest].m_rows[row_dest].m_text_color     =m_columns[column_source].m_rows[row_source].m_text_color;
   m_columns[column_dest].m_rows[row_dest].m_selected_image =m_columns[column_source].m_rows[row_source].m_selected_image;
//--- 将数组大小从源复制到接收者
   int images_total=::ArraySize(m_columns[column_source].m_rows[row_source].m_images);
   ::ArrayResize(m_columns[column_dest].m_rows[row_dest].m_images,images_total);
//---
   for(int i=0; i<images_total; i++)
     {
      //--- 若有图像, 复制
      if(::ArraySize(m_columns[column_source].m_rows[row_source].m_images[i].m_image_data)<1)
         continue;
      //--- 制作图像副本
      ImageCopy(m_columns[column_dest].m_rows[row_dest].m_images,m_columns[column_source].m_rows[row_source].m_images,i);
     }
  }

从方法中消除部分重复的代码, 因为方法中已经实现了另一种简单的方法, 在表格内容变化之后需要重建时调用。每次添加/删除行和列时, 必须重新计算表格并调整大小, 然后重新绘制表格来反映这些变化。CCanvasTable::RecalculateAndResizeTable() 方法即用于此目的。在此, 唯一的参数表示是否需要彻底重绘表格 (true) 或只是简单刷新 (false)。

class CCanvasTable : public CElement
  {
private:
   //--- 考虑到最近的变化重新计算并调整表格的大小
   void              RecalculateAndResizeTable(const bool redraw=false);
  };
//+------------------------------------------------------------------+
//| 考虑到最近的变化重新计算并调整表格的大小                                 |
//+------------------------------------------------------------------+
void CCanvasTable::RecalculateAndResizeTable(const bool redraw=false)
  {
//--- 计算表格大小
   CalculateTableSize();
//--- 调整表格大小
   ChangeTableSize();
//--- 更新表格
   UpdateTable(redraw);
  }

添加/删除行和列的方法比之 CTable 类型的表格中的这些方法的版本要简单得多, 且易于理解。您可以自行比较这些方法的代码。在此, 只给出添加/删除列的方法代码作为例子。用来处理行的代码其动作顺序与本节前面所述相同。请注意: 当 添加/删除 时, 已排序表格的符号也将随同已排序列一并移动。在删除已排序列的情况下, 包含排序列索引的字段为

class CCanvasTable : public CElement
  {
public:
   //--- 在表格的指定索引处添加一列
   void              AddColumn(const int column_index,const bool redraw=false);
   //--- 从表格的指定索引处删除一列
   void              DeleteColumn(const int column_index,const bool redraw=false);
  };
//+------------------------------------------------------------------+
//| 在表格的指定索引处添加一列                                            |
//+------------------------------------------------------------------+
void CCanvasTable::AddColumn(const int column_index,const bool redraw=false)
  {
//--- 将数组大小增加一个元素
   int array_size=(int)ColumnsTotal();
   m_columns_total=array_size+1;
   ::ArrayResize(m_columns,m_columns_total);
//--- 设置行数组的大小
   ::ArrayResize(m_columns[array_size].m_rows,m_rows_total);
//--- 在超出范围的情况下调整索引
   int checked_column_index=(column_index>=(int)m_columns_total)? (int)m_columns_total-1 : column_index;
//--- 平移其它列 (从数组的末尾开始添加列的索引)
   for(int c=array_size; c>=checked_column_index; c--)
     {
      //--- 平移排序数组的符号
      if(c==m_is_sorted_column_index && m_is_sorted_column_index!=WRONG_VALUE)
         m_is_sorted_column_index++;
      //--- 前一列的索引
      int prev_c=c-1;
      //--- 使用省缺值初始化新列
      if(c==checked_column_index)
         ColumnInitialize(c);
      //--- 将数据从前一列移动到当前列
      else
         ColumnCopy(c,prev_c);
      //---
      for(uint r=0; r<m_rows_total; r++)
        {
         //--- 使用省缺值初始化新的列单元
         if(c==checked_column_index)
           {
            CellInitialize(c,r);
            continue;
           }
         //--- 将数据从前一列单元动到当前列单元
         CellCopy(c,r,prev_c,r);
        }
     }
//--- 计算并调整到表格大小
   RecalculateAndResizeTable(redraw);
  }
//+------------------------------------------------------------------+
//| 从表格指定索引处删除一列                                              |
//+------------------------------------------------------------------+
void CCanvasTable::DeleteColumn(const int column_index,const bool redraw=false)
  {
//--- 获取列数组的大小
   int array_size=(int)ColumnsTotal();
//--- 在超出范围的情况下调整索引
   int checked_column_index=(column_index>=array_size)? array_size-1 : column_index;
//--- 平移其它列 (从指定索引开始到最后一列)
   for(int c=checked_column_index; c<array_size-1; c++)
     {
      //--- 平移排序数组的符号
      if(c!=checked_column_index)
        {
         if(c==m_is_sorted_column_index && m_is_sorted_column_index!=WRONG_VALUE)
            m_is_sorted_column_index--;
        }
      //--- 如果排序的列已被删除, 则为零
      else
         m_is_sorted_column_index=WRONG_VALUE;
      //--- 下一列的索引
      int next_c=c+1;
      //--- 将数据从下一列移动到当前列
      ColumnCopy(c,next_c);
      //--- 将下一个列单元中的数据移动到当前列单元
      for(uint r=0; r<m_rows_total; r++)
         CellCopy(c,r,next_c,r);
     }
//--- 将列数组减少一个元素
   m_columns_total=array_size-1;
   ::ArrayResize(m_columns,m_columns_total);
//--- 计算并调整到表格大小
   RecalculateAndResizeTable(redraw);
  }

彻底重建和清除表格的方法也非常简单, 不同于 CTable 类型表格中的类似方法。在此, 通过调用 CCanvasTable::TableSize() 方法来设置新的大小就足够了。清除表格时设置最小值, 仅留下一列和一行。包含所选行、排序方向和排序列索引值的字段在清除时也为一并清零。在两种方法的最后, 计算并设置新的表格大小

class CCanvasTable : public CElement
  {
public:
   //--- 重建表格
   void              Rebuilding(const int columns_total,const int rows_total,const bool redraw=false);   
   //--- 清除表格只留下一列和一行。
   void              Clear(const bool redraw=false);
  };
//+------------------------------------------------------------------+
//| 重建表格                                                           |
//+------------------------------------------------------------------+
void CCanvasTable::Rebuilding(const int columns_total,const int rows_total,const bool redraw=false)
  {
//--- 设置新的大小
   TableSize(columns_total,rows_total);
//--- 计算并调整到表格大小
   RecalculateAndResizeTable(redraw);
  }
//+------------------------------------------------------------------+
//| 清除表格。仅留下一列一行。                                            |
//+------------------------------------------------------------------+
void CCanvasTable::Clear(const bool redraw=false)
  {
//--- 设置最小大小 1x1
   TableSize(1,1);
//--- 设置省缺值
   m_selected_item_text     ="";
   m_selected_item          =WRONG_VALUE;
   m_last_sort_direction    =SORT_ASCEND;
   m_is_sorted_column_index =WRONG_VALUE;
//--- 计算并调整到表格大小
   RecalculateAndResizeTable(redraw);
  }

它是如何工作的:

 图例. 2. 管理表格大小的演示。

图例. 2. 管理表格大小的演示。


上述动画中的测试应用程序可以在文章末尾下载。


鼠标左键双击事件

有时候也许要通过双击来调用某个事件。我们在函数库中实现一个这种事件的生成。为此, 在 Defines.mqh 文件里添加鼠标左键双击事件的标识符 ON_DOUBLE_CLICK:

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                                 版权所有 2015, MetaQuotes 软件公司  |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
...
#define ON_DOUBLE_CLICK             (34) // 鼠标左键双击事件
...

ON_DOUBLE_CLICK 标识符的事件将会在 CMouse 类中生成。在操作系统中, 这个事件通常是由鼠标左键的 "按下-释放-按下" 动作产生的。但在终端环境中的测试表明, 当 CHARTEVENT_MOUSE_MOVE 事件 (参数 sparam ) 到来时, 它并不能够总是立即跟踪鼠标左键按下的事件。有鉴于此, 决定通过 "按下-释放-按下" 动作实现事件的产生。这些动作可在 CHARTEVENT_CLICK 事件到来时准确地予以跟踪。 

省缺情况下, 两次点击之间至少需要 300 毫秒。为了跟踪 CHARTEVENT_CLICK 事件, CMouse::CheckDoubleClick() 方法 将会在鼠标事件处理器中被调用。在此方法的开头部分声明了两个静态变量, 每次调用该方法时将保存先前和当前的计数器 (自系统启动以来所经过的毫秒数) 数值。如果这些值之间的毫秒数小于 m_pause_between_clicks 字段中指定的毫秒数, 则 生成鼠标左键双击事件。‌

//+------------------------------------------------------------------+
//| 获取鼠标参数的类                                                    |
//+------------------------------------------------------------------+
class CMouse
  {
private:
   //--- 暂停按钮鼠标左键点击 (用于判断双击)
   uint              m_pause_between_clicks;
   //---
private:
   //--- 检查鼠标左键双击
   bool              CheckDoubleClick(void);
  };
//+------------------------------------------------------------------+
//| 构造器                                                            |
//+------------------------------------------------------------------+
CMouse::CMouse(void) : m_pause_between_clicks(300)
  {
...
  }
//+------------------------------------------------------------------+
//| 鼠标事件处理                                                       |
//+------------------------------------------------------------------+
void CMouse::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- 处理图表点击事件
   if(id==CHARTEVENT_CLICK)
     {
      //--- 检查鼠标左键双击
      CheckDoubleClick();
      return;
     }
  }
//+------------------------------------------------------------------+
//| 检查鼠标左键双击                                                  |
//+------------------------------------------------------------------+
void CMouse::CheckDoubleClick(void)
  {
   static uint prev_depressed =0;
   static uint curr_depressed =::GetTickCount();
//--- 更新数值
   prev_depressed =curr_depressed;
   curr_depressed =::GetTickCount();
//--- 判断点击之间的时间
   uint counter=curr_depressed-prev_depressed;
//--- 如果两次点击之间的时间少于指定的时间, 发送有关双击的消息
   if(counter<m_pause_between_clicks)
      ::EventChartCustom(m_chart.ChartId(),ON_DOUBLE_CLICK,counter,0.0,"");
  }

所以, 所有函数库类的事件处理程序允许在图表区域的任何位置跟踪鼠标左键双击, 且无论光标之下是否存在图形对象。


表格单元中的控件

本文将开始表格单元中的控件主题。例如, 当需要创建多参数智能交易系统时, 也许需要该功能。系统的图形界面用表格来实现是很便利的。我们开始通过复选框和按钮将这些控件添加到表格单元中。 

首先, 这需要一个新的 ENUM_TYPE_CELL 枚举, 其中将定义单元类型:

//+------------------------------------------------------------------+
//| 表格单元类型枚举                                                    |
//+------------------------------------------------------------------+
enum ENUM_TYPE_CELL
  {
   CELL_SIMPLE   =0,
   CELL_BUTTON   =1,
   CELL_CHECKBOX =2
  };

省缺情况下, 在初始化期间, 为表格单元分配了简单类型 – CELL_SIMPLE, 这意味着 "单元无控件"。要设置或获取单元类型, 请使用 CCanvasTable::CellType() 方法, 它们的代码在下面的列表中提供。如果为单元分配了 CELL_CHECKBOX (复选框) 类型, 还需要为复选框状态设置图像。此种单元类型的最小图像数量为 2。可以设置两个以上的图标, 这样允许使用多参数复选框。

class CCanvasTable : public CElement
  {
   //--- 设置/获取单元类型
   void              CellType(const uint column_index,const uint row_index,const ENUM_TYPE_CELL type);
   ENUM_TYPE_CELL    CellType(const uint column_index,const uint row_index);
  };
//+------------------------------------------------------------------+
//| 设置单元类型                                                        |
//+------------------------------------------------------------------+
void CCanvasTable::CellType(const uint column_index,const uint row_index,const ENUM_TYPE_CELL type)
  {
//--- 检查超出数组范围
   if(!CheckOutOfRange(column_index,row_index))
      return;
//--- 设置单元类型
   m_columns[column_index].m_rows[row_index].m_type=type;
  }
//+------------------------------------------------------------------+
//| 获取单元类型                                                        |
//+------------------------------------------------------------------+
ENUM_TYPE_CELL CCanvasTable::CellType(const uint column_index,const uint row_index)
  {
//--- 检查超出数组范围
   if(!CheckOutOfRange(column_index,row_index))
      return(WRONG_VALUE);
//--- 返回指定列的数据类型
   return(m_columns[column_index].m_rows[row_index].m_type);
  }

复选框和按钮的一系列图像可以具有不同的大小, 因此, 有必要分别设置每一列图像的 XY偏移量。这可以使用 CCanvasTable::ImageXOffset() 和 CCanvasTable::ImageYOffset() 方法来完成:

class CCanvasTable : public CElement
  {
public:
   //--- 图像沿 X 轴和 Y 轴的偏移量
   void              ImageXOffset(const int &array[]);
   void              ImageYOffset(const int &array[]);
  };

另一补充是当再次点击时禁用行选择取消模式。此模式仅在行选择模式启用时有效。

class CCanvasTable : public CElement
  {
private:
   //--- 再次点击时行不会取消
   bool              m_is_without_deselect;
   //---
public:
   //--- "再次点击时行不会取消" 模式
   void              IsWithoutDeselect(const bool flag)   { m_is_without_deselect=flag;      }
  };

分开的 CCanvasTable::PressedRowIndex() 和 CCanvasTable::PressedCellColumnIndex() 方法现在用于判断点击的列和行索引。之前它们被认为是 CCanvasTable::OnClickTable() 方法中的模块。它们新添加的完整代码可在文章附带的文件中找到。必须分别注意, 使用这两种方法可以帮助判断鼠标左键点击的单元。接下来, 研究在何处传递接收列和行的索引。

class CCanvasTable : public CElement
  {
private:
   //--- 返回点击行的索引
   int               PressedRowIndex(void);
   //--- 返回点击单元的列索引
   int               PressedCellColumnIndex(void);
  };

当单击表格单元时, 需要获取其类型, 如果包含控件, 则还需要判断是否激活。为此目的, 已实现了一些私有方法, 其中主要的是 CCanvasTable::CheckCellElement()。来自 CCanvasTable::PressedRowIndex() 和 CCanvasTable::PressedCellColumnIndex() 方法的接收列和行的索引被传递至此。方法有两种模式。使用第三个参数指定事件是否为双击, 并依据事件类型调用处理方法。

之后, 检查单元类型, 并根据其类型调用适当的方法。如果单元包含 "按钮" 控件, 则会调用 CCanvasTable::CheckPressedButton() 方法CCanvasTable::CheckPressedCheckBox() 方法 适用于带复选框的单元。

class CCanvasTable : public CElement
  {
private:
   //--- 当点击时, 检查单元控件是否被激活
   bool              CheckCellElement(const int column_index,const int row_index,const bool double_click=false);
  };
//+------------------------------------------------------------------+
//| 当点击时, 检查单元控件是否被激活                                       |
//+------------------------------------------------------------------+
bool CCanvasTable::CheckCellElement(const int column_index,const int row_index,const bool double_click=false)
  {
//--- 如果单元内无控件, 离开
   if(m_columns[column_index].m_rows[row_index].m_type==CELL_SIMPLE)
      return(false);
//---
   switch(m_columns[column_index].m_rows[row_index].m_type)
     {
      //--- 如果它是按钮单元
      case CELL_BUTTON :
        {
         if(!CheckPressedButton(column_index,row_index,double_click))
            return(false);
         //---
         break;
        }
      //--- 如果它是复选框单元
      case CELL_CHECKBOX :
        {
         if(!CheckPressedCheckBox(column_index,row_index,double_click))
            return(false);
         //---
         break;
        }
     }
//---
   return(true);
  }

我们来看看 CCanvasTable::CheckPressedButton() 和 CCanvasTable::CheckPressedCheckBox() 方法的结构。在两种方法的开头均检查单元内图像数量。按钮单元必须至少包含一个图标, 复选框单元至少包含两个图标。之后必需判断是否点击的是图像。是复选框的情况下, 实现两种切换方式。如果点击了复选框的图标, 只有单击可以工作。双击单元中的任意位置可以切换复选框。只要符合所有条件, 两种方法均会 生成与控件相对应的标识符事件。图像的索引作为 double 参数传递, string 参数则由列和行索引的字符串形成。

class CCanvasTable : public CElement
  {
private:
   //--- 检查单元中的按钮是否被点击
   bool              CheckPressedButton(const int column_index,const int row_index,const bool double_click=false);
   //--- 检查单元中的复选框是否被单击
   bool              CheckPressedCheckBox(const int column_index,const int row_index,const bool double_click=false);
  };
//+------------------------------------------------------------------+
//| 检查单元中的按钮是否被点击                                            |
//+------------------------------------------------------------------+
bool CCanvasTable::CheckPressedButton(const int column_index,const int row_index,const bool double_click=false)
  {
//--- 如果单元中没有图像, 离开
   if(ImagesTotal(column_index,row_index)<1)
     {
      ::Print(__FUNCTION__," > 至少需要分配一个图像给单元按钮!");
      return(false);
     }
//--- 获取鼠标光标下的相对坐标
   int x=m_mouse.RelativeX(m_table);
// --- 获取图像的右边框
   int image_x  =int(m_columns[column_index].m_x+m_columns[column_index].m_image_x_offset);
   int image_x2 =int(image_x+m_columns[column_index].m_rows[row_index].m_images[0].m_image_width);
//--- 如果点击并不在图像上, 离开
   if(x>image_x2)
      return(false);
   else
     {
      //--- 如果这不是双击, 则发送一条消息
      if(!double_click)
        {
         int image_index=m_columns[column_index].m_rows[row_index].m_selected_image;
         ::EventChartCustom(m_chart_id,ON_CLICK_BUTTON,CElementBase::Id(),image_index,string(column_index)+"_"+string(row_index));
        }
     }
//---
   return(true);
  }
//+------------------------------------------------------------------+
//| 检查是否点击单元中的复选框                                            |
//+------------------------------------------------------------------+
bool CCanvasTable::CheckPressedCheckBox(const int column_index,const int row_index,const bool double_click=false)
  {
//--- 如果单元中没有图像, 离开
   if(ImagesTotal(column_index,row_index)<2)
     {
      ::Print(__FUNCTION__," > 至少需要分配两个图像给单元复选框!");
      return(false);
     }
//--- 获取鼠标光标下的相对坐标
   int x=m_mouse.RelativeX(m_table);
// --- 获取图像的右边框
   int image_x  =int(m_columns[column_index].m_x+m_image_x_offset);
   int image_x2 =int(image_x+m_columns[column_index].m_rows[row_index].m_images[0].m_image_width);
//--- 如果 (1) 点击不在图像上, 且 (2) 它不是双击, 离开
   if(x>image_x2 && !double_click)
      return(false);
   else
     {
      //--- 所选图像的当前索引
      int image_i=m_columns[column_index].m_rows[row_index].m_selected_image;
      //--- 确定图像的下一个索引
      int next_i=(image_i<ImagesTotal(column_index,row_index)-1)? ++image_i : 0;
      //--- 选择下一张图片并更新表格
      ChangeImage(column_index,row_index,next_i,true);
      m_table.Update(false);
      //--- 发送有关消息
      ::EventChartCustom(m_chart_id,ON_CLICK_CHECKBOX,CElementBase::Id(),next_i,string(column_index)+"_"+string(row_index));
     }
//---
   return(true);
  }

结果就是, 用于处理点击表格的 CCanvasTable::OnClickTable() 和 CCanvasTable::OnDoubleClickTable() 方法变得更加易于理解且合理 (参见以下代码)。根据启用的模式和点击的单元类型, 生成相应的事件。

class CCanvasTable : public CElement
  {
private:
   //--- 处理在表格上的点击
   bool              OnClickTable(const string clicked_object);
   //--- 处理在表格上的双击
   bool              OnDoubleClickTable(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| 处理在表格上的点击                                                  |
//+------------------------------------------------------------------+
bool CCanvasTable::OnClickTable(const string clicked_object)
  {
//--- 如果 (1) 行选择模式被禁用, 或 (2) 在更改列宽度的过程中, 离开
   if(m_column_resize_control!=WRONG_VALUE)
      return(false);
//--- 如果滚动条激活, 离开
   if(m_scrollv.ScrollState() || m_scrollh.ScrollState())
      return(false);
//--- 如果它有一个不同的对象名称, 离开
   if(m_table.Name()!=clicked_object)
      return(false);
//--- 确定点击的行
   int r=PressedRowIndex();
//--- 确定点击的单元格
   int c=PressedCellColumnIndex();
//--- 检查单元格中的控件是否已激活
   bool is_cell_element=CheckCellElement(c,r);
//--- 如果 (1) 行选择模式被启用, 且 (2) 单元控件未激活
   if(m_selectable_row && !is_cell_element)
     {
      //--- 改变颜色
      RedrawRow(true);
      m_table.Update();
      //--- 发送有关消息
      ::EventChartCustom(m_chart_id,ON_CLICK_LIST_ITEM,CElementBase::Id(),m_selected_item,string(c)+"_"+string(r));
     }
//---
   return(true);
  }
//+------------------------------------------------------------------+
//| 处理在表格上的双击                                                   |
//+------------------------------------------------------------------+
bool CCanvasTable::OnDoubleClickTable(const string clicked_object)
  {
   if(!m_table.MouseFocus())
      return(false);
//--- 确定点击的行
   int r=PressedRowIndex();
//--- 确定点击的单元格
   int c=PressedCellColumnIndex();
//--- 检查单元格中的控件是否已激活并返回结果
   return(CheckCellElement(c,r,true));
  }

在一个单元格上的鼠标左键双击事件是在控件的 ON_DOUBLE_CLICK 事件处理器里进行处理: 

//+------------------------------------------------------------------+
//| 事件处理器                                                         |
//+------------------------------------------------------------------+
void CCanvasTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- 处理鼠标左键双击
   if(id==CHARTEVENT_CUSTOM+ON_DOUBLE_CLICK)
     {
      //--- 点击表格
      if(OnDoubleClickTable(sparam))
         return;
      //---
      return;
     }
  }

最后, 一切操作都像这样:

 图例. 3. 与表格单元格中的控件进行交互的演示。

图例. 3. 与表格单元格中的控件进行交互的演示。


文章中的应用程序可以使用下面的链接下载。


结论

当前, 创建图形界面的函数库的一般原理图如下所示:

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

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


您可从下面下载最新版本的函数库和文件进行测试。

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

本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/3104

附加的文件 |
图形界面 X: 升级渲染表格及代码优化 (集成编译 10) 图形界面 X: 升级渲染表格及代码优化 (集成编译 10)

我们继续为渲染表格 (CCanvasTable) 完善新的功能。表格现在将具有: 当悬浮时高亮显示; 为每个单元格添加一个图标数组的能力, 以及一种切换它们的方法; 在运行时设置或修改单元格文本的能力等等。

计算赫斯特指数 计算赫斯特指数

本文彻底解释了赫斯特指数背后的思想, 以及其价值观和计算算法的含义。分析了多个金融市场片段, 并介绍了使用 MetaTrader 5 产品实现分形分析的方法。

交易货币篮子时可用的形态第二部分 交易货币篮子时可用的形态第二部分

我们继续讨论,当交易货币篮子时交易者可以参考的形态。在这一部分中,我们将探讨当使用组合的趋势指标时构建的形态,会使用基于货币指数的指标作为分析工具。

带有图形界面的通用趋势 带有图形界面的通用趋势

在本文中将会创建一款基于众多标准指标的通用趋势指标。附带创建的图形界面允许选择指标类型并调整其参数。指标在单独窗口中显示多行彩色图标。