English Русский Español Deutsch 日本語 Português
图形界面 X: 升级渲染表格及代码优化 (集成编译 10)

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

MetaTrader 5示例 | 19 四月 2017, 15:57
1 188 0
Anatoli Kazharski
Anatoli Kazharski

内容

 

概论

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

我们继续补充渲染表格 (CCanvasTable) 与新功能。此次将添加以下功能。

  • 悬浮时高亮显示表格行。
  • 为每个单元格添加一个图标数组的能力以及一种切换它们的方法。
  • 在运行时设置并修改单元格中文本的能力。 

此外, 代码和某些算法已经优化, 以便更快地重绘表格。 

 

光标在指定画板上的相对坐标

为了消除很多类中的重复代码, 以及计算画板上相对坐标的方法, 已将 CMouse::RelativeX() 和 CMouse::RelativeY() 方法添加到 CMouse 类中以便检索坐标。必须将 CRectCanvas 类型对象的引用传递给这些方法, 以便 参考画板可见部分的当前偏移量 来计算相对坐标。

//+------------------------------------------------------------------+
//| 获取鼠标参数的类                                                    |
//+------------------------------------------------------------------+
class CMouse
  {
public:
   //--- 返回所传递画板对象的鼠标光标相对坐标
   int               RelativeX(CRectCanvas &object);
   int               RelativeY(CRectCanvas &object);
  };
//+------------------------------------------------------------------+
//| 返回鼠标光标相对 X 坐标                                              |
//| 来自所传递画板对象                                                   |
//+------------------------------------------------------------------+
int CMouse::RelativeX(CRectCanvas &object)
  {
   return(m_x-object.X()+(int)object.GetInteger(OBJPROP_XOFFSET));
  }
//+------------------------------------------------------------------+
//| 返回鼠标光标相对 Y 坐标                                              |
//| 来自所传递画板对象                                                  |
//+------------------------------------------------------------------+
int CMouse::RelativeY(CRectCanvas &object)
  {
   return(m_y-object.Y()+(int)object.GetInteger(OBJPROP_YOFFSET));
      }

在含数库的进一步开发中, 将使用这些方法来获取所有绘制控件的相对坐标。 

 

表格结构变化

为了尽可能优化渲染表格代码的执行效率, 需要对 CTOptions 类型的表格结构稍微进行修改和补充, 并添加允许构建多维数组的新结构。此处的任务是基于以前计算的数值来重新绘制表格的某些片段。例如, 这有可能是表格列和行边界的坐标。

例如, 计算并保存列边框的 X 坐标 仅在 CCanvasTable::DrawGrid() 方法中才合理, 该方法仅在绘制整个表格时用来绘制网格。而当用户选择表格行时, 可以使用预定值。这同样适用于悬浮时表格行时的高亮显示 (这将会在本文中进一步讨论)。 

创建一个单独的结构 (CTRowOptions) 并声明其实例的数组 来保存表格行的 Y 坐标, 以及其它未来可能的行属性。行的 Y 坐标是在 CCanvasTable::DrawRows() 方法里进行计算, 设计用于绘制行的背景。由于此方法是在绘制网格之前调用, CCanvasTable::DrawGrid() 方法使用来自 CTRowOptions 结构的预算数值。 

创建一个单独的 CTCell 类型结构 用来保存表格单元的属性。CTRowOptions 结构中的实例数组会声明为此类型, 如同一个 表格行数组。此结构将存储:

  • 图标数组
  • 图标大小的数组
  • 单元格中所选 (所显示) 图标的索引
  • 完整文本
  • 缩写文本
  • 文本颜色

由于每个图标都是一组像素的数组, 所以需要一个单独的 结构 (CTImage) 和一个用于存储它们的动态数组。这些结构的代码可以在下面的列表中找到:

class CCanvasTable : public CElement
  {
private:
   //--- 图标像素的数组
   struct CTImage { uint m_image_data[]; };
   //--- 表格单元的属性
   struct CTCell
     {
      CTImage           m_images[];       // 图标数组
      uint              m_image_width[];  // 图标宽度数组
      uint              m_image_height[]; // 图标高度数组
      int               m_selected_image; // 所选 (所显示) 图标的索引
      string            m_full_text;      // 完整文本
      string            m_short_text;     // 缩写文本
      color             m_text_color;     // 文本颜色
     };
   //--- 表格行和列的属性数组
   struct CTOptions
     {
      int               m_x;             // 列的左边缘 X 坐标
      int               m_x2;            // 列的右边缘 X 坐标
      int               m_width;         // 列宽
      ENUM_ALIGN_MODE   m_text_align;    // 列单元内的文本对齐模式
      int               m_text_x_offset; // 文本偏移量
      string            m_header_text;   // 列标题文本
      CTCell            m_rows[];        // 表格行数组
     };
   CTOptions         m_columns[];
   //--- 表格行属性数组
   struct CTRowOptions
     {
      int               m_y;  // 行顶边缘 Y 坐标
      int               m_y2; // 行底边缘 Y 坐标
     };
   CTRowOptions      m_rows[];
      };

所用数据类型的全部方法均进行了适当地修改。 

 

确定可视部分的行范围

由于一个表格内也许含有很多行, 所以在某一行上搜索焦点之后重新绘制表可能会显著拖慢进程。这同样适用于选择一行并手动更改列宽时调整文本长度。为了避免延迟, 有必要确定表格的可视部分中第一个和最后一个的索引, 并分配一个循环, 在该范围内进行迭代。CCanvasTable::VisibleTableIndexes() 方法即是为此目的而实现的。它首先确定可视部分的边界。上边界是可视部分沿 Y 轴的偏移, 而 下边界定义为上边界 + 可视部分沿 Y 轴的尺寸

现在, 将所获得的边界值除以表格设置中定义的行高, 即足以确定可视部分的顶行和底行的索引。在最后一列超出范围的情况下, 在方法结束时进行调整。

class CCanvasTable : public CElement
  {
private:
   //--- 用于确定表格可视部分的索引
   int               m_visible_table_from_index;
   int               m_visible_table_to_index;
   //---
private:
   //--- 确定表格可视部分的索引
   void              VisibleTableIndexes(void);
  };
//+------------------------------------------------------------------+
//| 构造器                                                            |
//+------------------------------------------------------------------+
CCanvasTable::CCanvasTable(void) : m_visible_table_from_index(WRONG_VALUE),
                                   m_visible_table_to_index(WRONG_VALUE)
  {
...
  }
//+------------------------------------------------------------------+
//| 确定表格可视部分的索引                                               |
//+------------------------------------------------------------------+
void CCanvasTable::VisibleTableIndexes(void)
  {
//--- 确定边界并考虑到表格可视部分的偏移
   int yoffset1 =(int)m_table.GetInteger(OBJPROP_YOFFSET);
   int yoffset2 =yoffset1+m_table_visible_y_size;
//--- 确定表格可视部分的第一个和最后一个索引
   m_visible_table_from_index =int(double(yoffset1/m_cell_y_size));
   m_visible_table_to_index   =int(double(yoffset2/m_cell_y_size));
//--- 如果未超出范围, 低端索引加一
   m_visible_table_to_index=(m_visible_table_to_index+1>m_rows_total)? m_rows_total : m_visible_table_to_index+1;
      }

索引将在 CCanvasTable::DrawTable() 方法里确定。可为方法传递一个 参数 来指定必须 只重新绘制表格的可视部分。参数的省缺值是 false, 即 表示重绘整个表格。下面的代码列表展示了此方法的缩写版本。

//+------------------------------------------------------------------+
//| 绘制表格                                                           |
//+------------------------------------------------------------------+
void CCanvasTable::DrawTable(const bool only_visible=false)
  {
//--- If not indicated to redraw only the visible part of the table
   if(!only_visible)
     {
      //--- 为整个表格设置从开始到结束的行索引
      m_visible_table_from_index =0;
      m_visible_table_to_index   =m_rows_total;
     }
//--- 获取表格可视部分的行索引
   else
      VisibleTableIndexes();
//--- 绘制表格行背景
//--- 绘制所选行
//--- 绘制网格
//--- 绘制图标
//--- 绘制文本
//--- 显示最后绘制的变化
//--- 如果启用, 更新标题
//--- 相对于滚动条调整表格
      }

在确定表格行上的焦点方法中也需要 调用 CCanvasTable::VisibleTableIndexes()

//+------------------------------------------------------------------+
//| 检查表格行上的焦点                                                   |
//+------------------------------------------------------------------+
int CCanvasTable::CheckRowFocus(void)
  {
   int item_index_focus=WRONG_VALUE;
//--- 获取鼠标光标之下的相对 Y 坐标
   int y=m_mouse.RelativeY(m_table);
///--- 获取表格局部区域的索引
   VisibleTableIndexes();
//--- 搜索焦点
   for(int i=m_visible_table_from_index; i<m_visible_table_to_index; i++)
     {
      //--- 如果行焦点已改变
      if(y>m_rows[i].m_y && y<=m_rows[i].m_y2)
        {
         item_index_focus=i;
         break;
        }
     }
//--- 返回焦点行的索引
   return(item_index_focus);
      }

 

 

表格单元中的图标

可以为每个单元分配多个图标, 并可在程序运行期间进行切换。添加了用于设置图标自单元顶部和左边缘偏移量的字段和方法:

class CCanvasTable : public CElement
  {
private:
   //--- 图标自单元边缘的偏移量
   int               m_image_x_offset;
   int               m_image_y_offset;
   //---
public:
   //--- 图标自单元边缘的偏移量
   void              ImageXOffset(const int x_offset)     { m_image_x_offset=x_offset;       }
   void              ImageYOffset(const int y_offset)     { m_image_y_offset=y_offset;       }
      };

若要将图标分配给指定的单元, 必须传递它们的终端本地目录路径。在此之前, 它们必须作为资源 (#resource) 包含在 MQL 应用程序中。CCanvasTable::SetImages() 方法即是为此设计的。在此, 如果传递了一个空数组, 或者检测到数组超限, 则程序离开方法。

如果检查通过, 则调整单元数组的大小。之后, 循环使用 ::ResourceReadImage() 方法将图标内容读取到一维数组, 将每个像素的颜色存储到数组中图标大小存储到相应的数组。需要分配循环将图标绘制到画板上。省缺情况下, 将在单元格中选择数组的第一个图标

class CCanvasTable : public CElement
  {
public:
   //--- 为指定单元格设置图标
   void              SetImages(const uint column_index,const uint row_index,const string &bmp_file_path[]);
  };
//+------------------------------------------------------------------+
//| 为指定单元格设置图标                                                 |
//+------------------------------------------------------------------+
void CCanvasTable::SetImages(const uint column_index,const uint row_index,const string &bmp_file_path[])
  {
   int total=0;
//--- 如果所传递的数组大小为零, 离开
   if((total=CheckArraySize(bmp_file_path))==WRONG_VALUE)
      return;
//--- 检查是否超出数组范围
   if(!CheckOutOfRange(column_index,row_index))
      return;
//--- 调整数组大小
   ::ArrayResize(m_columns[column_index].m_rows[row_index].m_images,total);
   ::ArrayResize(m_columns[column_index].m_rows[row_index].m_image_width,total);
   ::ArrayResize(m_columns[column_index].m_rows[row_index].m_image_height,total);
//---
   for(int i=0; i<total; i++)
     {
      //--- 省缺选择数组的第一个图标
      m_columns[column_index].m_rows[row_index].m_selected_image=0;
      //--- 将传递的图标写入数组并存储其大小
      if(!ResourceReadImage(bmp_file_path[i],m_columns[column_index].m_rows[row_index].m_images[i].m_image_data,
         m_columns[column_index].m_rows[row_index].m_image_width[i],
         m_columns[column_index].m_rows[row_index].m_image_height[i]))
        {
         Print(__FUNCTION__," > 错误: ",GetLastError());
         return;
        }
     }
      }

若要查找特定单元格有多少图标, 请使用 CCanvasTable::ImagesTotal() 方法:

class CCanvasTable : public CElement
  {
public:
   //--- 返回指定单元格中图标的总数
   int               ImagesTotal(const uint column_index,const uint row_index);
  };
//+------------------------------------------------------------------+
//| 返回指定单元格中图标的总数                                            |
//+------------------------------------------------------------------+
int CCanvasTable::ImagesTotal(const uint column_index,const uint row_index)
  {
//--- 检查是否超出数组范围
   if(!CheckOutOfRange(column_index,row_index))
      return(WRONG_VALUE);
//--- 返回图标数组的大小
   return(::ArraySize(m_columns[column_index].m_rows[row_index].m_images));
      }

现在研究用来绘制图标的方法。首先, 新的 CColors::BlendColors() 方法 已被添加到 CColors 类中, 考虑到重叠图标透明度的情况下这样可以正确混合上、下端颜色。以及用于获取所传递颜色透明度值的辅助 CColors::GetA() 方法

Colors::BlendColors() 方法中, 所传递的颜色首先被分离出 RGB 分量, 并从顶部颜色中提取阿尔法通道。阿尔法通道将被转换为零到一之间的值。如果所传递的颜色不包含透明度, 则不进行混合。在有透明度的情况下, 则 两个所传递颜色的每个分量均会参考顶部颜色的透明度进行混色。之后, 如果它们超出范围 (255), 则调整所获分量的值。 

//+------------------------------------------------------------------+
//| 操纵颜色的类                                                       |
//+------------------------------------------------------------------+
class CColors
  {
public:
   double            GetA(const color aColor);
   color             BlendColors(const uint lower_color,const uint upper_color);
  };
//+------------------------------------------------------------------+
//| 获取 A 分量值                                                      |
//+------------------------------------------------------------------+
double CColors::GetA(const color aColor)
  {
   return(double(uchar((aColor)>>24)));
  }
//+------------------------------------------------------------------+
//| 参考顶部颜色的透明度, 混合两种颜色                                     |
//+------------------------------------------------------------------+
color CColors::BlendColors(const uint lower_color,const uint upper_color)
  {
   double r1=0,g1=0,b1=0;
   double r2=0,g2=0,b2=0,alpha=0;
   double r3=0,g3=0,b3=0;
//--- 以 ARGB 格式转换颜色
   uint pixel_color=::ColorToARGB(upper_color);
//--- 获取上、下端颜色的分量
   ColorToRGB(lower_color,r1,g1,b1);
   ColorToRGB(pixel_color,r2,g2,b2);
//--- 获取的透明度百分比从 0.00 到1.00
   alpha=GetA(upper_color)/255.0;
//--- 如果有透明度
   if(alpha<1.0)
     {
      //--- 参考阿尔法通道混合分量
      r3=(r1*(1-alpha))+(r2*alpha);
      g3=(g1*(1-alpha))+(g2*alpha);
      b3=(b1*(1-alpha))+(b2*alpha);
      //--- 调整所获值
      r3=(r3>255)? 255 : r3;
      g3=(g3>255)? 255 : g3;
      b3=(b3>255)? 255 : b3;
     }
   else
     {
      r3=r2;
      g3=g2;
      b3=b2;
     }
//--- 组合所获分量并返回颜色
   return(RGBToColor(r3,g3,b3));
      }

现在, 编写一个绘制图标的方法很容易。CCanvasTable::DrawImage() 方法的代码展示如下。它必须传递表格单元的索引, 即图标将被绘制之处。在方法伊始, 参考偏移量, 以及所选择单元的索引, 及其大小, 获取图标的坐标。然后, 通过双循环逐个输出图标。如果指定的像素为空 (没有颜色), 则循环将转到下一个像素。若有颜色, 则确定单元格背景颜色和当前像素颜色, 参考重叠颜色的透明度将这两种颜色混合在一起, 并将所得颜色绘制在画板上 。

class CCanvasTable : public CElement
  {
private:
   //--- 在指定的单元格中绘制一个图标
   void              DrawImage(const int column_index,const int row_index);
  };
//+------------------------------------------------------------------+
//| 在指定的单元格中绘制一个图标                                          |
//+------------------------------------------------------------------+
void CCanvasTable::DrawImage(const int column_index,const int row_index)
  {
//--- 计算坐标
   int x =m_columns[column_index].m_x+m_image_x_offset;
   int y =m_rows[row_index].m_y+m_image_y_offset;
//--- 在单元格中选择的图标及其大小
   int  selected_image =m_columns[column_index].m_rows[row_index].m_selected_image;
   uint image_height   =m_columns[column_index].m_rows[row_index].m_image_height[selected_image];
   uint image_width    =m_columns[column_index].m_rows[row_index].m_image_width[selected_image];
//--- 绘图
   for(uint ly=0,i=0; ly<image_height; ly++)
     {
      for(uint lx=0; lx<image_width; lx++,i++)
        {
         //--- 如果没有颜色, 转到下一个像素
         if(m_columns[column_index].m_rows[row_index].m_images[selected_image].m_image_data[i]<1)
            continue;
         //--- 获取下层 (单元格背景) 的颜色, 以及图标指定像素的颜色
         uint background  =(row_index==m_selected_item)? m_selected_row_color : m_table.PixelGet(x+lx,y+ly);
         uint pixel_color =m_columns[column_index].m_rows[row_index].m_images[selected_image].m_image_data[i];
         //--- 混合颜色
         uint foreground=::ColorToARGB(m_clr.BlendColors(background,pixel_color));
         //--- 绘制重叠图标的像素
         m_table.PixelSet(x+lx,y+ly,foreground);
        }
     }
      }

CCanvasTable::DrawImages() 方法设计用于一次性绘制表格的所有图标, 并考虑到何时有必要仅绘制表格的可视部分 。在当前版本的表格中, 只有当列中的文本与左侧对齐时, 才能绘制图标。此外, 每次迭代检查图标是否分配给单元格, 以及其像素的数组是否为空。如果所有检查都通过, 则调用 CCanvasTable::DrawImage() 方法绘制图标。 

class CCanvasTable : public CElement
  {
private:
   //--- 绘制表格的所有图标
   void              DrawImages(void);
  };
//+------------------------------------------------------------------+
//| 绘制表格的所有图标                                                   |
//+------------------------------------------------------------------+
void CCanvasTable::DrawImages(void)
  {
//--- 计算坐标
   int x=0,y=0;
//--- 列
   for(int c=0; c<m_columns_total; c++)
     {
      //--- 如果文本未与左侧对齐, 转到下一列
      if(m_columns[c].m_text_align!=ALIGN_LEFT)
         continue;
      //--- 行
      for(int r=m_visible_table_from_index; r<m_visible_table_to_index; r++)
        {
         //--- 如果这个单元格不包含图标, 转到下一个
         if(ImagesTotal(c,r)<1)
            continue;
         //--- 单元格中选定的图标 (省缺情况下选择第一个 [0])
         int selected_image=m_columns[c].m_rows[r].m_selected_image;
         //--- 如果像素数组为空, 转到下一个
         if(::ArraySize(m_columns[c].m_rows[r].m_images[selected_image].m_image_data)<1)
            continue;
         //--- 绘制图标
         DrawImage(c,r);
        }
     }
      }

下面的屏幕截图显示一个在单元格中含有图标的表格示例:

 图例. 1. 在单元格中含有图标的表格。

图例. 1. 在单元格中含有图标的表格。 


 

悬浮时高亮显示表格行

对于悬浮时要高亮显示的渲染表格行, 将需要额外的字段和方法。使用 CCanvasTable::LightsHover() 方法 来启用高亮模式。行的颜色可在 CCanvasTable::CellColorHover() 方法的帮助下设置。

class CCanvasTable : public CElement
  {
private:
   //--- 单元格在不同状态时的颜色
   color             m_cell_color;
   color             m_cell_color_hover;
   //--- 悬浮时行高亮显示模式
   bool              m_lights_hover;
   //---
public:
   //--- 单元格在不同状态时的颜色
   void              CellColor(const color clr)           { m_cell_color=clr;                }
   void              CellColorHover(const color clr)      { m_cell_color_hover=clr;          }
   //--- 悬浮时行高亮显示模式
   void              LightsHover(const bool flag)         { m_lights_hover=flag;             }
      };

高亮显示一行不需要在光标移动时重新绘制整个表格。再有, 强烈建议不要这样做, 因为它大大拖慢了应用程序, 占用了太多的 CPU 资源。在鼠标光标第一次/新进入到表格区域时, 只需查找焦点一次就足够了 (遍历整个行数组)。CCanvasTable::CheckRowFocus() 方法即用于此目的。一旦找到焦点并保存行索引后, 只需在移动光标时进行简单检查, 焦点是否在已保存索引的行上发生变化。所描述的算法已在 CCanvasTable::ChangeRowsColor() 方法里实现, 如下面的列表所示。CCanvasTable::RedrawRow() 方法用来修改行的颜色, 其代码稍后介绍。CCanvasTable::ChangeRowsColor() 方法是在 CCanvasTable::ChangeObjectsColor() 方法里调用, 用来修改表格对象的颜色。 

class CCanvasTable : public CElement
  {
private:
   //--- 确定行焦点
   int               m_item_index_focus;
   //--- 确定鼠标光标从一行转换到另一行的时刻
   int               m_prev_item_index_focus;
   //---
private:
   //--- 悬停时更改行颜色
   void              ChangeRowsColor(void);
  };
//+------------------------------------------------------------------+
//| 悬停时更改行颜色                                                    |
//+------------------------------------------------------------------+
void CCanvasTable::ChangeRowsColor(void)
  {
//--- 如果禁用悬停时的行高亮显示, 离开
   if(!m_lights_hover)
      return;
//--- 如果不在焦点
   if(!m_table.MouseFocus())
     {
      //--- 如果还未指出不在焦点
      if(m_prev_item_index_focus!=WRONG_VALUE)
        {
         m_item_index_focus=WRONG_VALUE;
         //--- 改变颜色
         RedrawRow();
         m_table.Update();
         //--- 重置焦点
         m_prev_item_index_focus=WRONG_VALUE;
        }
     }
//--- 如果在焦点
   else
     {
      //--- 检查行上的焦点
      if(m_item_index_focus==WRONG_VALUE)
        {
         //--- 获取焦点所在行的索引
         m_item_index_focus=CheckRowFocus();
         //--- 改变行颜色
         RedrawRow();
         m_table.Update();
         //--- 保存为以前的焦点索引
         m_prev_item_index_focus=m_item_index_focus;
         return;
        }
      //--- 获取鼠标光标之下的相对 Y 坐标
      int y=m_mouse.RelativeY(m_table);
      //--- 验证焦点
      bool condition=(y>m_rows[m_item_index_focus].m_y && y<=m_rows[m_item_index_focus].m_y2);
      //--- 如果焦点改变了
      if(!condition)
        {
         //--- 获取焦点所在行的索引
         m_item_index_focus=CheckRowFocus();
         //--- 改变行颜色
         RedrawRow();
         m_table.Update();
         //--- 保存为以前的焦点索引
         m_prev_item_index_focus=m_item_index_focus;
        }
     }
      }

CCanvasTable::RedrawRow() 方法有两种快速重绘表格行模式:

  •  当选择一行时
  •  处于悬浮时高亮显示一行模式

该方法需要传递相应的参数来指定所需模式。省缺情况下, 参数设置为 false, 表示在高亮显示表格行模式下使用该方法。该类还包含两种模式的特殊字段来确定当前和先前 所选择的/高亮 表格行。因此, 标记其它行, 仅需要重绘前一行和当前行, 而不是整个表。

如果没有定义索引 (WRONG_VALUE), 程序将离开该方法。接下来, 需要确定已定义了多少个索引。如果这是第一此进入表格, 并且只定义了一个索引 (当前), 则相应地, 颜色将只在当前行中更改。如果是再次进入, 颜色将会更改两行 (当前和以前)。 

现在需要确定更改行颜色的顺序。如果当前行的索引大于前一行的索引, 则表示光标向下移动。然后, 首先更改前一个索引的颜色, 然后在当前索引中更改颜色。在相反的情况下, 也反过来做。方法还要考虑离开表格区域时刻, 此时没有定义当前行索引, 而前一行的索引仍然存在。

一旦所有参与操作的局部变量被初始化, 行背景, 网格, 图标和文本将被严格地绘制。

class CCanvasTable : public CElement
  {
private:
   //--- 根据指定的模式重绘指定的表格行
   void              RedrawRow(const bool is_selected_row=false);
  };
//+------------------------------------------------------------------+
//| 根据指定的模式重绘指定的表行                                          |
//+------------------------------------------------------------------+
void CCanvasTable::RedrawRow(const bool is_selected_row=false)
  {
//--- 当前以及前一行的索引
   int item_index      =WRONG_VALUE;
   int prev_item_index =WRONG_VALUE;
//--- 相对于指定模式初始化行索引
   if(is_selected_row)
     {
      item_index      =m_selected_item;
      prev_item_index =m_prev_selected_item;
     }
   else
     {
      item_index      =m_item_index_focus;
      prev_item_index =m_prev_item_index_focus;
     }
//--- 如果没有定义索引, 离开
   if(prev_item_index==WRONG_VALUE && item_index==WRONG_VALUE)
      return;
//--- 将要绘制的行数和列数
   int rows_total    =(item_index!=WRONG_VALUE && prev_item_index!=WRONG_VALUE)? 2 : 1;
   int columns_total =m_columns_total-1;
//--- 坐标
   int x1=1,x2=m_table_x_size;
   int y1[2]={0},y2[2]={0};
//--- 确定序列中的数值数组
   int indexes[2];
//--- 如果 (1) 鼠标光标向下移动, 或如果 (2) 首次进入
   if(item_index>m_prev_item_index_focus || item_index==WRONG_VALUE)
     {
      indexes[0]=(item_index==WRONG_VALUE || prev_item_index!=WRONG_VALUE)? prev_item_index : item_index;
      indexes[1]=item_index;
     }
//--- 如果鼠标光标向上移动
   else
     {
      indexes[0]=item_index;
      indexes[1]=prev_item_index;
     }
//--- 绘制行的背景
   for(int r=0; r<rows_total; r++)
     {
      //--- 计算行的上、下边界坐标
      y1[r]=m_rows[indexes[r]].m_y+1;
      y2[r]=m_rows[indexes[r]].m_y2-1;
      //--- 确定处于高亮显示模式的焦点所在行
      bool is_item_focus=false;
      if(!m_lights_hover)
         is_item_focus=(indexes[r]==item_index && item_index!=WRONG_VALUE);
      else
         is_item_focus=(item_index==WRONG_VALUE)?(indexes[r]==prev_item_index) :(indexes[r]==item_index);
      //--- 绘制行背景
      m_table.FillRectangle(x1,y1[r],x2,y2[r],RowColorCurrent(indexes[r],is_item_focus));
     }
//--- 网格颜色
   uint clr=::ColorToARGB(m_grid_color);
//--- 绘制边界
   for(int r=0; r<rows_total; r++)
     {
      for(int c=0; c<columns_total; c++)
         m_table.Line(m_columns[c].m_x2,y1[r],m_columns[c].m_x2,y2[r],clr);
     }
//--- 绘制图标
   for(int r=0; r<rows_total; r++)
     {
      for(int c=0; c<m_columns_total; c++)
        {
         //--- 绘制图标, 如果 (1) 它存在于单元格中, 且 (2) 此列的文本靠左侧对齐
         if(ImagesTotal(c,r)>0 && m_columns[c].m_text_align==ALIGN_LEFT)
            DrawImage(c,indexes[r]);
        }
     }
//--- 计算坐标
   int x=0,y=0;
//--- 文本对齐模式
   uint text_align=0;
//--- 绘制文字
   for(int c=0; c<m_columns_total; c++)
     {
      //--- 获取 (1) 文本的 X 坐标, 和 (2) 文本对齐模式
      x          =TextX(c);
      text_align =TextAlign(c,TA_TOP);
      //---
      for(int r=0; r<rows_total; r++)
        {
         //--- (1) 计算坐标, 并 (2) 绘制文本
         y=m_rows[indexes[r]].m_y+m_text_y_offset;
         m_table.TextOut(x,y,m_columns[c].m_rows[indexes[r]].m_short_text,TextColor(c,indexes[r]),text_align);
        }
     }
      }

结果如下:

 图例. 2. 悬浮时高亮显示表格行的演示。

图例. 2. 悬浮时高亮显示表格行的演示。 

 

 

快速重绘表格单元的方法

快速重绘表格行的方法已经研究完毕。现在将展示快速重绘单元格的方法。例如, 如果需要更改表格任何单元格中的文本, 颜色或图标, 只需重绘单元格, 而非整个表格。私有 CCanvasTable::RedrawCell() 方法即用于此目的。只有单元格内容将被重绘, 而其框架将不会被更新。如果启用了高亮模式, 则要确定背景颜色。在确定值并初始化局部变量之后, 将在单元格中绘制背景, 图标 (如果分配了, 且如果文本左侧对齐) 和文本。

class CCanvasTable : public CElement
  {
private:
   //--- 重绘指定的表格单元
   void              RedrawCell(const int column_index,const int row_index);
  };
//+------------------------------------------------------------------+
//| 重绘指定的表格单元                                                  |
//+------------------------------------------------------------------+
void CCanvasTable::RedrawCell(const int column_index,const int row_index)
  {
//--- 坐标
   int x1=m_columns[column_index].m_x+1;
   int x2=m_columns[column_index].m_x2-1;
   int y1=m_rows[row_index].m_y+1;
   int y2=m_rows[row_index].m_y2-1;
//--- 计算坐标
   int  x=0,y=0;
//--- 检查焦点
   bool is_row_focus=false;
//--- 如果启用行高亮显示模式
   if(m_lights_hover)
     {
      //--- (1) 获取鼠标光标的相对 Y 坐标, 和 (2) 指定表格行上的焦点
      y=m_mouse.RelativeY(m_table);
      is_row_focus=(y>m_rows[row_index].m_y && y<=m_rows[row_index].m_y2);
     }
//--- 绘制单元背景
   m_table.FillRectangle(x1,y1,x2,y2,RowColorCurrent(row_index,is_row_focus));
//--- 绘制图标, 如果 (1) 它存在于单元格中, 且 (2) 此列的文本靠左侧对齐
   if(ImagesTotal(column_index,row_index)>0 && m_columns[column_index].m_text_align==ALIGN_LEFT)
      DrawImage(column_index,row_index);
//--- 获取文本对齐模式
   uint text_align=TextAlign(column_index,TA_TOP);
//--- 绘制文字
   for(int c=0; c<m_columns_total; c++)
     {
      //--- 获取文本的 X 坐标
      x=TextX(c);
      //--- 停止循环
      if(c==column_index)
         break;
     }
//--- (1) 计算 Y 坐标, 并 (2) 绘制文本
   y=y1+m_text_y_offset-1;
   m_table.TextOut(x,y,m_columns[column_index].m_rows[row_index].m_short_text,TextColor(column_index,row_index),text_align);
      }

现在我们来研究方法, 它们允许在单元格中更改文本, 文本颜色和图标 (从已分配的之中选择)。公有 CCanvasTable::SetValue()CCanvasTable::TextColor() 方法必须用来设置文本和其颜色。这些方法需要传递单元格 (列和行) 的索引, 以及要设置的数值。对于 CCanvasTable::SetValue() 方法, 它是要在单元格中显示的字符串值。此处, 完整传递的字符串及其缩写版本 (如果全部字符串不适合单元格宽度) 存储在表格的结构 (CTCell) 的相应字段中。文本颜色必须传递给 CCanvasTable::TextColor() 方法。作为这两种方法中的第四个参数, 您可以指定 是否需要立即重新绘制单元格, 或者稍后 通过调用 CCanvasTable::UpdateTable () 方法。

class CCanvasTable : public CElement
  {
private:
   //--- 为指定的表格单元设置数值
   void              SetValue(const uint column_index,const uint row_index,const string value,const bool redraw=false);
   //--- 为指定的表格单元设置文本颜色
   void              TextColor(const uint column_index,const uint row_index,const color clr,const bool redraw=false);
  };
//+------------------------------------------------------------------+
//| 在指定的索引处填充数组                                               |
//+------------------------------------------------------------------+
void CCanvasTable::SetValue(const uint column_index,const uint row_index,const string value,const bool redraw=false)
  {
//--- 检查是否超出数组范围
   if(!CheckOutOfRange(column_index,row_index))
      return;
//--- 将数值存储到数组中
   m_columns[column_index].m_rows[row_index].m_full_text=value;
//--- 调整并存储文本, 如果它不适合单元格
   m_columns[column_index].m_rows[row_index].m_short_text=CorrectingText(column_index,row_index);
//--- 如果已指定, 重绘单元格
   if(redraw)
      RedrawCell(column_index,row_index);
  }
//+------------------------------------------------------------------+
//| 填充文本颜色数组                                                    |
//+------------------------------------------------------------------+
void CCanvasTable::TextColor(const uint column_index,const uint row_index,const color clr,const bool redraw=false)
  {
//--- 检查是否超出数组范围
   if(!CheckOutOfRange(column_index,row_index))
      return;
//--- 将文本颜色存储在公共数组中
   m_columns[column_index].m_rows[row_index].m_text_color=clr;
//--- 如果已指定, 重绘单元格
   if(redraw)
      RedrawCell(column_index,row_index);
      }

单元中的图标可以通过 CCanvasTable::ChangeImage() 方法更改。即将切换的图表索引 必须在此指定为第三个参数。如前面所描述的用于改变单元格属性的方法, 能够指定是否要立即或稍后重新绘制单元格。 

class CCanvasTable : public CElement
  {
private:
   //--- 更改指定单元格中的图标
   void              ChangeImage(const uint column_index,const uint row_index,const uint image_index,const bool redraw=false);
  };
//+------------------------------------------------------------------+
//| 更改指定单元格中的图标                                               |
//+------------------------------------------------------------------+
void CCanvasTable::ChangeImage(const uint column_index,const uint row_index,const uint image_index,const bool redraw=false)
  {
//--- 检查是否超出数组范围
   if(!CheckOutOfRange(column_index,row_index))
      return;
//--- 获取单元格图标的数量
   int images_total=ImagesTotal(column_index,row_index);
//--- 如果 (1) 没有图标, 或是 (2) 超出范围, 离开
   if(images_total==WRONG_VALUE || image_index>=(uint)images_total)
      return;
//--- 如果指定的图标与已选定的图标匹配, 离开
   if(image_index==m_columns[column_index].m_rows[row_index].m_selected_image)
      return;
//--- 保存单元格所选图标的索引
   m_columns[column_index].m_rows[row_index].m_selected_image=(int)image_index;
//--- 如果已指定, 重绘单元格
   if(redraw)
      RedrawCell(column_index,row_index);
      }

重绘整个表格需要另一个公共方法 — CCanvasTable::UpdateTable()。它可以在两种模式下调用: 

  1. 当有必要简单地更新表格以便显示由上述方法所做的最新变化时。
  2. 如果进行了更改, 有必要完全重绘表格时。

省缺情况下, 该方法的唯一参数设置为 false, 表示刷新无需重绘。 

class CCanvasTable : public CElement
  {
private:
   //--- 更新表格
   void              UpdateTable(const bool redraw=false);
  };
//+------------------------------------------------------------------+
//| 更新表格                                                           |
//+------------------------------------------------------------------+
void CCanvasTable::UpdateTable(const bool redraw=false)
  {
//--- 如果指定,重绘表格
   if(redraw)
      DrawTable();
//--- 更新表格
   m_table.Update();
      }

以下是完工后的结果:

 图例. 3. 渲染表格新功能演示。

图例. 3. 渲染表格新功能演示。


展示此结果的智能交易系统可在本文附带的文件中下载。在程序执行期间, 所有表格单元格 (5 列和 30 行) 中的图标将以 100 毫秒的频率刷新。下面的屏幕截图显示了 CPU 负载, 没有用户通过图形界面与 MQL 应用程序交互。刷新频率为 100 毫秒时的 CPU 负载不超过 3%。

 图例. 4. 执行 MQL 应用程序测试期间的 CPU 负载。

图例. 4. 执行 MQL 应用程序测试期间的 CPU 负载。 

 

 

用于测试控件的应用程序

例如, 当前版本渲染表格的 "智能" 已经足以创建与"市场观察" 窗口中相同的表格。让我们试着展示这一点。例如, 创建一个 5 列和 25 行的表格。在 MetaQuotes-Demo 服务器上有 25 个可用的品种。表中的数据如下:

  • Symbol – 金融工具 (当前货币对)。
  • Bid – 供给价。
  • Ask – 采购价。
  • Spread (!) – 供给价和采购价之间的点差。
  • Time – 最后报价的时间。

让我们准备相同的图标来表示最新的价格变化, 如同 "市场观察" 窗口的表格。在创建控件的方法中, 表格单元的首次初始化将立即进行, 并且通过调用自定义类的辅助 CProgram::InitializingTable() 方法来执行。 

//+------------------------------------------------------------------+
//| 创建应用程序的类                                                    |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- 初始化表格
   void              InitializingTable(void);
  };
//+------------------------------------------------------------------+
//| 初始化表格                                                         |
//+------------------------------------------------------------------+
void CProgram::InitializingTable(void)
  {
//--- 标题数组
   string text_headers[COLUMNS1_TOTAL]={"Symbol","Bid","Ask","!","Time"};
//--- 品种数组
   string text_array[25]=
     {
      "AUDUSD","GBPUSD","EURUSD","USDCAD","USDCHF","USDJPY","NZDUSD","USDSEK","USDHKD","USDMXN",
      "USDZAR","USDTRY","GBPAUD","AUDCAD","CADCHF","EURAUD","GBPCHF","GBPJPY","NZDJPY","AUDJPY",
      "EURJPY","EURCHF","EURGBP","AUDCHF","CHFJPY"
     };
//--- 图标数组
   string image_array[3]=
     {
      "::Images\\EasyAndFastGUI\\Icons\\bmp16\\circle_gray.bmp",
      "::Images\\EasyAndFastGUI\\Icons\\bmp16\\arrow_up.bmp",
      "::Images\\EasyAndFastGUI\\Icons\\bmp16\\arrow_down.bmp"
     };
//---
   for(int c=0; c<COLUMNS1_TOTAL; c++)
     {
      //--- 设置标题
      m_canvas_table.SetHeaderText(c,text_headers[c]);
      //---
      for(int r=0; r<ROWS1_TOTAL; r++)
        {
         //--- 设置图标
         m_canvas_table.SetImages(c,r,image_array);
         //--- 设置品名
         if(c<1)
            m_canvas_table.SetValue(c,r,text_array[r]);
         //--- 所有单元的省缺值
         else
            m_canvas_table.SetValue(c,r,"-");
        }
     }
      }

省缺的表格单元刷新率在运行期间为 16 毫秒。另一个辅助 CProgram::UpdateTable() 方法已为此目的而创建。此处, 若是周末则程序离开方法 (周六或周日)。然后, 双重循环遍历表格的所有列和行。在双重循环中 获取每个品种的最后两笔即时报价, 且在 分析价格变化 之后, 设置相应的数值。 

class CProgram : public CWndEvents
  {
private:
   //--- 初始化表格
   void              InitializingTable(void);
  };
//+------------------------------------------------------------------+
//| 更新表格数值                                                       |
//+------------------------------------------------------------------+
void CProgram::UpdateTable(void)
  {
   MqlDateTime check_time;
   ::TimeToStruct(::TimeTradeServer(),check_time);
//--- 如果是星期六或星期天, 离开
   if(check_time.day_of_week==0 || check_time.day_of_week==6)
      return;
//---
   for(int c=0; c<m_canvas_table.ColumnsTotal(); c++)
     {
      for(int r=0; r<m_canvas_table.RowsTotal(); r++)
        {
         //--- 获取数据的品种
         string symbol=m_canvas_table.GetValue(0,r);
         //--- 获取最后两笔即时报价的数据
         MqlTick ticks[];
         if(::CopyTicks(symbol,ticks,COPY_TICKS_ALL,0,2)<2)
            continue;
         //--- 将数组设置为时间序列
         ::ArraySetAsSeries(ticks,true);
         //--- 品种列 - 品种。Determine the price direction.
         if(c==0)
           {
            int index=0;
            //--- 如果价格未变化
            if(ticks[0].ask==ticks[1].ask && ticks[0].bid==ticks[1].bid)
               index=0;
            //--- 如果供给价格向上变化
            else if(ticks[0].bid>ticks[1].bid)
               index=1;
            //--- 如果供给价格向下变化
            else if(ticks[0].bid<ticks[1].bid)
               index=2;
            //--- 设置相应的图标
            m_canvas_table.ChangeImage(c,r,index,true);
           }
         else
           {
            //--- 价格差异列 - 点差 (!)
            if(c==3)
              {
               //--- 获取并设置点差的点数大小
               int spread=(int)::SymbolInfoInteger(symbol,SYMBOL_SPREAD);
               m_canvas_table.SetValue(c,r,string(spread),true);
               continue;
              }
            //--- 获取小数位数
            int digit=(int)::SymbolInfoInteger(symbol,SYMBOL_DIGITS);
            //--- 供给价列
            if(c==1)
              {
               m_canvas_table.SetValue(c,r,::DoubleToString(ticks[0].bid,digit));
               //--- 如果价格有变化, 设定与方向对应的颜色
               if(ticks[0].bid!=ticks[1].bid)
                  m_canvas_table.TextColor(c,r,(ticks[0].bid<ticks[1].bid)? clrRed : clrBlue,true);
               //---
               continue;
              }
            //--- 采购价列
            if(c==2)
              {
               m_canvas_table.SetValue(c,r,::DoubleToString(ticks[0].ask,digit));
               //--- 如果价格有变化, 设定与方向对应的颜色
               if(ticks[0].ask!=ticks[1].ask)
                  m_canvas_table.TextColor(c,r,(ticks[0].ask<ticks[1].ask)? clrRed : clrBlue,true);
               //---
               continue;
              }
            //--- 最后品种价格的到达时刻列
            if(c==4)
              {
               long   time     =::SymbolInfoInteger(symbol,SYMBOL_TIME);
               string time_msc =::IntegerToString(ticks[0].time_msc);
               int    length   =::StringLen(time_msc);
               string msc      =::StringSubstr(time_msc,length-3,3);
               string str      =::TimeToString(time,TIME_MINUTES|TIME_SECONDS)+"."+msc;
               //---
               color clr=clrBlack;
               //--- 如果价格未变化
               if(ticks[0].ask==ticks[1].ask && ticks[0].bid==ticks[1].bid)
                  clr=clrBlack;
               //--- 如果供给价格向上变化
               else if(ticks[0].bid>ticks[1].bid)
                  clr=clrBlue;
               //--- 如果供给价格向下变化
               else if(ticks[0].bid<ticks[1].bid)
                  clr=clrRed;
               //--- 设置数值和文本颜色
               m_canvas_table.SetValue(c,r,str);
               m_canvas_table.TextColor(c,r,clr,true);
               continue;
              }
           }
        }
     }
//--- 更新表格
   m_canvas_table.UpdateTable();
      }

得到以下结果:

图例. 5. 市场观察窗口中的数据与自定义模拟的比较。

图例. 5. 市场观察窗口中的数据与自定义模拟的比较。 


本文中的测试应用程序可以使用以下链接进行下载, 以便进一步学习。 

 

结论

创建图形界面的函数库的当前开发阶段如下图所示。

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

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


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

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


本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/3042

附加的文件 |
图形界面 X: 排序、重建表格和单元格中的控件 (集成编译 11) 图形界面 X: 排序、重建表格和单元格中的控件 (集成编译 11)
我们继续向渲染表格添加新功能: 数据排序, 管理列和行数, 设置表格单元类型以将控件放入其中。
计算赫斯特指数 计算赫斯特指数
本文彻底解释了赫斯特指数背后的思想, 以及其价值观和计算算法的含义。分析了多个金融市场片段, 并介绍了使用 MetaTrader 5 产品实现分形分析的方法。
交易货币篮子时可用的形态第二部分 交易货币篮子时可用的形态第二部分
我们继续讨论,当交易货币篮子时交易者可以参考的形态。在这一部分中,我们将探讨当使用组合的趋势指标时构建的形态,会使用基于货币指数的指标作为分析工具。
带有图形界面的通用通道 带有图形界面的通用通道
所有通道指标显示为三条线, 包括中心, 顶部和底部线。中心线的绘图原理与移动平均线相似, 而移动均线指标主要用于绘制通道。顶部线和底部线的位置距中心线距离相等。距离的确定可以按照点为单位, 作为价格百分比 (包络指标), 使用标准偏差值 (布林带) 或 ATR 值 (Keltner 通道)。