English Русский Español Deutsch 日本語 Português
图形界面 X: 渲染表格的新功能 (集成编译 9)

图形界面 X: 渲染表格的新功能 (集成编译 9)

MetaTrader 5示例 | 5 四月 2017, 13:17
2 855 0
Anatoli Kazharski
Anatoli Kazharski

内容


概论

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

时至今日, CTable 是函数库中所含的最先进类型表格。表格由 OBJ_EDIT 类型的编辑框汇集而成, 但其进一步开发成为问题。例如, 它难以实现通过手动拖动标题边框调整列的大小, 且不能管理表格的各个图形对象的可见区域。这已达到极限。

因此, 在函数库开发的当前阶段, 进而开发 CCanvasTable 类型的表格更为合理。有关渲染表格以前版本和更新的信息, 请参见此处:

屏幕截图展示了最新版渲染表格的外观。如您所见, 此刻它毫无生气。它只是一个带有数据的表格单元格。可以为单元格指定对齐方法。除了滚动条和窗体大小的自动调整之外, 该表格还没有其它交互性。

 图例. 1. 渲染表格的以前版本。

图例. 1. 渲染表格的以前版本。

为了纠正这种状况, 我们来用新功能补充渲染表格。在当前更新中将包含以下功能:

  • 格式化斑马样式
  • 选择表格行, 再次单击时弃选
  • 添加了列标题, 可在鼠标悬浮和点击时改变颜色
  • 当单元格空间不足时, 则依照文本自动调整列宽
  • 能够通过拖动其边框来改变每列的标题宽度

格式化斑马样式

在最近的一篇文章里已将格式化斑马样式添加到 CTable 表格里。如果表格包含很多单元格, 它有助于更好地表格导航。让我们在渲染表格中实现这种模式。

使用 CCanvasTable::IsZebraFormatRows() 方法启用此模式。它将传递第二个颜色用于样式, 而通常的单元格颜色将使用第一个颜色。

//+------------------------------------------------------------------+
//| 创建渲染表格的类                                                    |
//+------------------------------------------------------------------+
class CCanvasTable : public CElement
  {
private:
   //--- 格式化斑马样式模式
   color             m_is_zebra_format_rows;
   //---
public:
   //--- 格式化斑马样式
   void              IsZebraFormatRows(const color clr)   { m_is_zebra_format_rows=clr;      }
      };

在不同类型的表格中这种风格的可视化方法有所不同。在 CCanvasTable 的情况下, 正常模式中 表格背景 (绘图画板) 完全用普通单元格颜色填充。当斑马样式被激活时, 开始一个循环。每次迭代中计算每行的坐标, 且用两种颜色交替为区域着色。这是利用绘制填充矩形的 FillRectangle() 方法完成的。 

class CCanvasTable : public CElement
  {
public:
   //--- 绘制表格行的背景
   void              DrawRows(void);
  };
//+------------------------------------------------------------------+
//| 绘制表格行的背景                                                    |
//+------------------------------------------------------------------+
void CCanvasTable::DrawRows(void)
  {
//--- 如果禁用格式化斑马样式模式
   if(m_is_zebra_format_rows==clrNONE)
     {
      //--- 用一种颜色填充画板
      m_table.Erase(::ColorToARGB(m_cell_color));
      return;
     }
//--- 标题坐标
   int x1=0,x2=m_table_x_size;
   int y1=0,y2=0;
//--- 格式化斑马样式
   for(int r=0; r<m_rows_total; r++)
     {
      //--- 计算坐标
      y1=(r*m_cell_y_size)-r;
      y2=y1+m_cell_y_size;
      //--- 行颜色
      uint clr=::ColorToARGB((r%2!=0)? m_is_zebra_format_rows : m_cell_color);
      //--- 绘制行背景
      m_table.FillRectangle(x1,y1,x2,y2,clr);
     }
      }

行颜色可根据您的喜好设置。结果就是, 斑马模式下的渲染表格将如下所示:

 图例. 2. 格式化斑马样式模式下的渲染表格。

图例. 2. 格式化斑马样式模式下的渲染表格。 

 


选择和弃选表格行

行选择将需要其它字段和方法进行存储和设置:

  • 所选行的背景和文本的颜色
  • 索引和文本

class CCanvasTable : public CElement
  {
private:
   //--- 颜色用于 (1) 背景 (2) 所选行文本
   color             m_selected_row_color;
   color             m_selected_row_text_color;
   //--- (1) 索引 (2) 所选行文本
   int               m_selected_item;
   string            m_selected_item_text;
   //---
public:
   //--- 返回 (1) 索引 (2) 表格中所选行文本
   int               SelectedItem(void)             const { return(m_selected_item);         }
   string            SelectedItemText(void)         const { return(m_selected_item_text);    }
   //---
private:
   //--- 绘制表格行的背景
   void              DrawRows(void);
      };

可以通过 CCanvasTable::SelectableRow() 方法启用/禁用可选行模式:

class CCanvasTable : public CElement
  {
private:
   //--- 可选行模式
   bool              m_selectable_row;
   //---
public:
   //--- 行选择
   void              SelectableRow(const bool flag)       { m_selectable_row=flag;           }
      };

为了选择一行, 需要一个用于绘制用户定义区域的单独方法。下面给出了 CCanvasTable::DrawSelectedRow() 方法的代码。它计算画板上所选区域的坐标, 然后用于 绘制填充矩形。 

class CCanvasTable : public CElement
  {
private:
   //--- 绘制所选行
   void              DrawSelectedRow(void);
  };
//+------------------------------------------------------------------+
//| 绘制所选行                                                         |
//+------------------------------------------------------------------+
void CCanvasTable::DrawSelectedRow(void)
  {
//--- 设置初始坐标以便检查条件
   int y_offset=(m_selected_item*m_cell_y_size)-m_selected_item;
//--- 坐标
   int x1=0,x2=0,y1=0,y2=0;
//---
   x1=0;
   y1=y_offset;
   x2=m_table_x_size;
   y2=y_offset+m_cell_y_size-1;
//--- 绘制填充矩形
   m_table.FillRectangle(x1,y1,x2,y2,::ColorToARGB(m_selected_row_color));
      }

一个辅助的 CCanvasTable::TextColor() 方法用来重绘文本, 它会判断单元格中的文本颜色: 

class CCanvasTable : public CElement
  {
private:
   //--- 返回单元格文本的颜色
   uint              TextColor(const int row_index);
  };
//+------------------------------------------------------------------+
//| 返回单元格文本的颜色                                                 |
//+------------------------------------------------------------------+
uint CCanvasTable::TextColor(const int row_index)
  {
   uint clr=::ColorToARGB((row_index==m_selected_item)? m_selected_row_text_color : m_cell_text_color);
//--- 返回标题颜色
   return(clr);
      }

若要选择表格行, 需要双击它。这需要 CCanvasTable::OnClickTable() 方法, 它通过 CHARTEVENT_OBJECT_CLICK 标识符在控件的事件处理器中调用。

在方法开始时应通过几个检查。程序会离开方法, 如果:

  • 行选择模式被禁用;
  • 滚动条已激活;
  • 未在表格上点击。 

如果检查通过, 则为了计算点击坐标, 需要获取 距画板边缘的当前偏移量鼠标光标的 Y 坐标。在这之后, 在循环里 判断所点击的行。一旦找到该行, 需要检查当前是否已被选中, 如果是 — 取消选择。如果已选择该行, 则需要存储其索引和第一列中的文本。在循环中完成行搜索后重新绘制表格。发送一条消息, 其中包含:

  • ON_CLICK_LIST_ITEM 事件的标识符
  • 控件的标识符
  • 所选行的索引
  • 所选行的文本。 
class CCanvasTable : public CElement
  {
private:
   //--- 在元素上点按处理
   bool              OnClickTable(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| 点击控件处理                                                       |
//+------------------------------------------------------------------+
bool CCanvasTable::OnClickTable(const string clicked_object)
  {
//--- 如果禁用选择行模式, 离开
   if(!m_selectable_row)
      return(false);
//--- 如果滚动条激活, 离开
   if(m_scrollv.ScrollState() || m_scrollh.ScrollState())
      return(false);
//--- 如果对象名不同, 离开
   if(m_table.Name()!=clicked_object)
      return(false);
//--- 获取 X 和 Y 轴的偏移量
   int xoffset=(int)m_table.GetInteger(OBJPROP_XOFFSET);
   int yoffset=(int)m_table.GetInteger(OBJPROP_YOFFSET);
//--- 确定鼠标光标下方的文本编辑框坐标
   int y=m_mouse.Y()-m_table.Y()+yoffset;
//--- 确定点击的行
   for(int r=0; r<m_rows_total; r++)
     {
      //--- 设置初始坐标以便检查条件
      int y_offset=(r*m_cell_y_size)-r;
      //--- 沿 Y 轴检查状态
      bool y_pos_check=(y>=y_offset && y<y_offset+m_cell_y_size);
      //--- 如果点击并非此行, 则转至下一个
      if(!y_pos_check)
         continue;
      //--- 如果点击已选行, 取消选择
      if(r==m_selected_item)
        {
         m_selected_item      =WRONG_VALUE;
         m_selected_item_text ="";
         break;
        }
      //--- 保存行索引 
      m_selected_item      =r;
      m_selected_item_text =m_vcolumns[0].m_vrows[r];
      break;
     }
//--- 表格重绘
   DrawTable();
//--- 发送有关消息
   ::EventChartCustom(m_chart_id,ON_CLICK_LIST_ITEM,CElementBase::Id(),m_selected_item,m_selected_item_text);
   return(true);
      }

带有选定行的渲染表格如下方式显示:

图例. 3. 选择和弃选渲染表格一行的演示。

图例. 3. 选择和弃选渲染表格一行的演示。 

 

列标题

任何没有标题的表格都是空白的。标题也将在这种类型的表中绘制, 但会在单独的画板上。为了做到这一点, 在 CCanvasTable 类中包含其它 CRectCanvas 类的实例, 并 创建一个单独的画板创建方法。此方法的代码不会在这里提供: 它几乎与创建表格相同。仅有的区别是预设对象的大小和位置。

class CCanvasTable : public CElement
  {
private:
   //--- 创建表格的对象
   CRectCanvas       m_headers;
   //---
private:
   bool              CreateHeaders(void);
      };

现在研究与列标题相关的属性。它们可在创建表之前进行配置。

  • 表头的显示模式。
  • 标题的大小 (高度)。
  • 不同状态下的标题背景颜色。
  • 标题文本颜色。

与这些属性相关的字段和方法: 

class CCanvasTable : public CElement
  {
private:
   //--- 表头的显示模式
   bool              m_show_headers;
   //--- 标题的大小 (高度)
   int               m_header_y_size;
   //--- 不同状态下的标题 (背景) 颜色
   color             m_headers_color;
   color             m_headers_color_hover;
   color             m_headers_color_pressed;
   //--- 标题文本颜色
   color             m_headers_text_color;
   //---
public:
   //--- (1) 标题显示模式, (2) 标题高度
   void              ShowHeaders(const bool flag)         { m_show_headers=flag;             }
   void              HeaderYSize(const int y_size)        { m_header_y_size=y_size;          }
   //--- (1) 背景 (2) 标题文本颜色
   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;     }
   void              HeadersTextColor(const color clr)    { m_headers_text_color=clr;        }
      };

方法需要 设置标题名称。此外, 还需要一个 数组来保存这些数值。数组的大小等于列数, 并在设置表格大小时使用相同的 CCanvasTable::TableSize() 方法中设置。 

class CCanvasTable : public CElement
  {
private:
   //--- 标题文本
   string            m_header_text[];
   //---
public:
   //--- 设置指定标题的文本
   void              SetHeaderText(const int column_index,const string value);
  };
//+------------------------------------------------------------------+
//| 在指定索引处填充标题数组                                              |
//+------------------------------------------------------------------+
void CCanvasTable::SetHeaderText(const uint column_index,const string value)
  {
//--- 检查列超界
   uint csize=::ArraySize(m_vcolumns);
   if(csize<1 || column_index>=csize)
      return;
//--- 将数值保存到数组中
   m_header_text[column_index]=value;
      }

在单元格和标题中的文本对齐将使用通用的 CCanvasTable::TextAlign() 方法进行。在表格单元中沿 X 轴的对齐与标题相匹配, 而 沿 Y 轴的对齐通过传递的数值定义。在此版本里, 标题文本在 Y 轴上将位于中间 — TA_VCENTER, 且单元格的便宜自单元格的顶部边缘进行调整 — TA_TOP。 

class CCanvasTable : public CElement
  {
private:
   //--- 返回指定列中的文本对齐模式
   uint              TextAlign(const int column_index,const uint anchor);
  };
//+------------------------------------------------------------------+
//| 返回指定列中的文本对齐模式                                            |
//+------------------------------------------------------------------+
uint CCanvasTable::TextAlign(const int column_index,const uint anchor)
  {
   uint text_align=0;
//--- 当前列的文本对齐
   switch(m_vcolumns[column_index].m_text_align)
     {
      case ALIGN_CENTER :
         text_align=TA_CENTER|anchor;
         break;
      case ALIGN_RIGHT :
         text_align=TA_RIGHT|anchor;
         break;
      case ALIGN_LEFT :
         text_align=TA_LEFT|anchor;
         break;
     }
//--- 返回对齐类型
   return(text_align);
      }

在许多表格以及操作系统环境中, 当光标悬浮在两个标题之间的边界时, 指针会改变。以下截图通过 MetaTrader 5 交易终端工具箱窗口中表格的示例描述了这种情形。如果点击这个新出现的指针, 它将切换更改列宽度的模式。此列的背景颜色也会改变。

图例. 4. 鼠标指针悬浮在标题边界关联处。

图例. 4. 鼠标指针悬浮在标题边界关联处。

 

我们来为所开发的函数库准备相同的图像。文章末尾的附件包含一个文件夹, 内有函数库控件的所有图像。在 Enums.mqh 文件里添加 新的指针标识符至 ENUM_MOUSE_POINTER 枚举, 用于沿 XY 轴的大小改变: 

//+------------------------------------------------------------------+
//| 指针类型的枚举                                                      |
//+------------------------------------------------------------------+
enum ENUM_MOUSE_POINTER
  {
   MP_CUSTOM            =0,
   MP_X_RESIZE          =1,
   MP_Y_RESIZE          =2,
   MP_XY1_RESIZE        =3,
   MP_XY2_RESIZE        =4,
   MP_X_RESIZE_RELATIVE =5,
   MP_Y_RESIZE_RELATIVE =6,
   MP_X_SCROLL          =7,
   MP_Y_SCROLL          =8,
   MP_TEXT_SELECT       =9
      };

需要在 CPointer 类中相应进行添加, 以使该指针类型可在控件的类中使用。 

//+------------------------------------------------------------------+
//|                                                      Pointer.mqh |
//|                                 版权所有 2015, MetaQuotes 软件公司  |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
//--- 资源
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp"
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp"
//+------------------------------------------------------------------+
//| 创建鼠标光标的类                                                    |
//+------------------------------------------------------------------+
class CPointer : public CElement
  {
private:
   //--- 设置鼠标光标的图像
   void              SetPointerBmp(void);
  };
//+------------------------------------------------------------------+
//| 基于光标类型设置光标图标                                              |
//+------------------------------------------------------------------+
void CPointer::SetPointerBmp(void)
  {
   switch(m_type)
     {
      ...
      case MP_X_RESIZE_RELATIVE :
         m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp";
         m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp";
         break;
      case MP_Y_RESIZE_RELATIVE :
         m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp";
         m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp";
         break;
      ...
     }
//--- 如果指定为自定义类型 (MP_CUSTOM)
   if(m_file_on=="" || m_file_off=="")
      ::Print(__FUNCTION__," > 必须为光标设置两个图像");
      }

此处需要附加的字段:

  • 确定拖动标题边框的时刻
  • 为了确定鼠标光标从一个标题区域移动到另一个标题区域的时刻。这需要节省资源, 因此只有当相邻区域的边界交错时才会重新绘制标题。

class CCanvasTable : public CElement
  {
private:
   //--- 确定鼠标光标从一个标题转换到另一个标题的时刻
   int               m_prev_header_index_focus;
   //--- 拖动标题边框以改变列宽的状态
   int               m_column_resize_control;
      };

CCanvasTable::HeaderColorCurrent() 方法可以根据当前模式、鼠标光标位置和鼠标左键状态获取标题的当前颜色。焦点覆盖标题 将在 CCanvasTable::DrawHeaders() 方法里判断, 该方法设计用于绘制标题背景, 并将被传递到此作为检查结果。

class CCanvasTable : public CElement
  {
private:
   //--- 返回当前的标题背景色
   uint              HeaderColorCurrent(const bool is_header_focus);
  };
//+------------------------------------------------------------------+
//| 返回当前的标题背景色                                                 |
//+------------------------------------------------------------------+
uint CCanvasTable::HeaderColorCurrent(const bool is_header_focus)
  {
   uint clr=clrNONE;
//--- 如果无焦点
   if(!is_header_focus || !m_headers.MouseFocus())
      clr=m_headers_color;
   else
     {
      //--- 如果鼠标左键按下且未处于列宽改变进程
      bool condition=(m_mouse.LeftButtonState() && m_column_resize_control==WRONG_VALUE);
      clr=(condition)? m_headers_color_pressed : m_headers_color_hover;
     }
//--- 返回标题颜色
   return(::ColorToARGB(clr));
      }

CCanvasTable::DrawHeaders() 方法的代码表述如下。此处, 如果鼠标光标不在标题区域, 则 整个画板将以指定的颜色填充。如果焦点在标题上, 那么有必要确定它们当中的哪一个拥有焦点。为此, 需要确定鼠标光标的相对坐标, 并在计算标题坐标的循环中检查每个标题是否处于焦点。另外, 在此还要考虑改变列宽模式。在计算此模式中使用 附加偏移量如果找到焦点, 则必须保存列索引。 

class CCanvasTable : public CElement
  {
private:
   //--- 自分隔线边界的偏移, 按照更改列宽模式显示鼠标指针
   int               m_sep_x_offset;
   //---
private:
   //--- 绘制标题
   void              DrawHeaders(void);
  };
//+------------------------------------------------------------------+
//| 绘制标题背景                                                       |
//+------------------------------------------------------------------+
void CCanvasTable::DrawHeaders(void)
  {
//--- 如果非焦点, 重置标题颜色
   if(!m_headers.MouseFocus())
     {
      m_headers.Erase(::ColorToARGB(m_headers_color));
      return;
     }
//--- 检查焦点是否在标题上
   bool is_header_focus=false;
//--- 鼠标光标坐标
   int x=0;
//--- 坐标
   int x1=0,x2=0,y1=0,y2=m_header_y_size;
//--- 得到鼠标光标的相对坐标
   if(::CheckPointer(m_mouse)!=POINTER_INVALID)
     {
      //--- 获取 X 轴偏移量
      int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
      //--- 确定鼠标光标坐标
      x=m_mouse.X()-m_headers.X()+xoffset;
     }
//--- 清除标题背景
   m_headers.Erase(::ColorToARGB(clrNONE,0));
//--- 考虑到更改列宽模式偏移
   int sep_x_offset=(m_column_resize_mode)? m_sep_x_offset : 0;
//--- 绘制标题背景
   for(int i=0; i<m_columns_total; i++)
     {
      //--- 计算坐标
      x2+=m_vcolumns[i].m_width;
      //--- 检查焦点
      if(is_header_focus=x>x1+((i!=0)? sep_x_offset : 0) && x<=x2+sep_x_offset)
         m_prev_header_index_focus=i;
      //--- 绘制标题背景
      m_headers.FillRectangle(x1,y1,x2,y2,HeaderColorCurrent(is_header_focus));
      //--- 计算下一标题偏移
      x1+=m_vcolumns[i].m_width;
     }
      }

一旦绘制了标题背景, 就需要绘制网格 (标题框)。CCanvasTable::DrawHeadersGrid() 方法用于此目的。首先, 绘制通用框, 之后在循环里画 分割线

class CCanvasTable : public CElement
  {
private:
   //--- 绘制表格标题网格
   void              DrawHeadersGrid(void);
  };
//+------------------------------------------------------------------+
//| 绘制表格标题网格                                                    |
//+------------------------------------------------------------------+
void CCanvasTable::DrawHeadersGrid(void)
  {
//--- 网格颜色
   uint clr=::ColorToARGB(m_grid_color);
//--- 坐标
   int x1=0,x2=0,y1=0,y2=0;
   x2=m_table_x_size-1;
   y2=m_header_y_size-1;
//--- 绘制边框
   m_headers.Rectangle(x1,y1,x2,y2,clr);
//--- 分割线
   x2=x1=m_vcolumns[0].m_width;
   for(int i=1; i<m_columns_total; i++)
     {
      m_headers.Line(x1,y1,x2,y2,clr);
      x2=x1+=m_vcolumns[i].m_width;
     }
      }

最后, 绘制标题文本。此任务用 CCanvasTable::DrawHeadersText() 方法执行。在此, 需要循环查看每个标题, 在每次迭代时确定 文本坐标对齐方式。标题名称作为循环的最后一个操作。相对于列宽调整文本也在此进行。CCanvasTable::CorrectingText() 方法 即用于此目的。在本文的下一节将对其进行更详细的描述。 

class CCanvasTable : public CElement
  {
private:
   //--- 绘制表格标题文本
   void              DrawHeadersText(void);
  };
//+------------------------------------------------------------------+
//| 绘制表格标题文本                                                    |
//+------------------------------------------------------------------+
void CCanvasTable::DrawHeadersText(void)
  {
//--- 计算坐标和偏移
   int x=0,y=m_header_y_size/2;
   int column_offset =0;
   uint text_align   =0;
//--- 文本颜色
   uint clr=::ColorToARGB(m_headers_text_color);
//--- 字体属性
   m_headers.FontSet(CElementBase::Font(),-CElementBase::FontSize()*10,FW_NORMAL);
//---绘制文本
   for(int c=0; c<m_columns_total; c++)
     {
      //--- 获取文本的 X 坐标
      x=TextX(c,column_offset);
      //--- 获取文本的对齐模式
      text_align=TextAlign(c,TA_VCENTER);
      //--- 绘制列名称
      m_headers.TextOut(x,y,CorrectingText(c,0,true),clr,text_align);
     }
      }

所有列出的绘制标题的方法均在通用的 CCanvasTable::DrawTableHeaders() 方法里调用。如果标题显示模式被禁用, 则禁止进入该方法。 

class CCanvasTable : public CElement
  {
private:
   //--- 绘制表格标题
   void              DrawTableHeaders(void);
  };
//+------------------------------------------------------------------+
//| 绘制表格标题                                                       |
//+------------------------------------------------------------------+
void CCanvasTable::DrawTableHeaders(void)
  {
//--- 如果禁用标题, 离开
   if(!m_show_headers)
      return;
//--- 绘制标题
   DrawHeaders();
//--- 绘制网格
   DrawHeadersGrid();
//--- 绘制标题文本
   DrawHeadersText();
      }

利用 CCanvasTable::CheckHeaderFocus() 方法检查焦点是否在标题上。程序在两种情况下会离开方法:

  • 如果标题显示模式被禁用
  • 或者如果更改列宽的过程已经开始。

此后, 获取光标在画板上的相对坐标。循环 搜索任何标题上的焦点 以及 检查自最后一次调用该方法后是否有变化。如果注册了新焦点 (标题边界交错的时刻), 则 需要重置之前保存的标题索引 并停止循环。

class CCanvasTable : public CElement
  {
private:
   //--- 检查标题上的焦点
   void              CheckHeaderFocus(void);
  };
//+------------------------------------------------------------------+
//| 检查标题上的焦点                                                    |
//+------------------------------------------------------------------+
void CCanvasTable::CheckHeaderFocus(void)
  {
//--- 如果 (1) 标题被禁用 (2) 列宽变更已开始, 离开
   if(!m_show_headers || m_column_resize_control!=WRONG_VALUE)
      return;
//--- 标题坐标
   int x1=0,x2=0;
//--- 获取 X 轴偏移量
   int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
//--- 得到鼠标光标的相对坐标
   int x=m_mouse.X()-m_headers.X()+xoffset;
//--- 考虑到更改列宽模式偏移
   int sep_x_offset=(m_column_resize_mode)? m_sep_x_offset : 0;
//--- 搜索焦点
   for(int i=0; i<m_columns_total; i++)
     {
      //--- 计算右坐标
      x2+=m_vcolumns[i].m_width;
      //--- 如果标题焦点改变
      if((x>x1+sep_x_offset && x<=x2+sep_x_offset) && m_prev_header_index_focus!=i)
        {
         m_prev_header_index_focus=WRONG_VALUE;
         break;
        }
      //--- 计算左坐标
      x1+=m_vcolumns[i].m_width;
     }
      }

与其相对, 只有当边界交错时, 标题才会重绘。这样可以节省 CPU 资源。CCanvasTable::ChangeHeadersColor() 方法设计用于此任务。在此, 如果标题显示模式被禁用, 或正在改变其宽度的过程, 程序将离开该方法。如果方法开头的检查通过, 则检查标题的焦点, 并重绘它们。 

class CCanvasTable : public CElement
  {
private:
   //--- 改变标题颜色
   void              ChangeHeadersColor(void);
  };
//+------------------------------------------------------------------+
//| 改变标题颜色                                                       |
//+------------------------------------------------------------------+
void CCanvasTable::ChangeHeadersColor(void)
  {
//--- 如果禁用标题, 离开
   if(!m_show_headers)
      return;
//--- 如果光标已激活
   if(m_column_resize.IsVisible() && m_mouse.LeftButtonState())
     {
      //--- 保存拖拽列的索引
      if(m_column_resize_control==WRONG_VALUE)
         m_column_resize_control=m_prev_header_index_focus;
      //---
      return;
     }
//--- 非焦点
   if(!m_headers.MouseFocus())
     {
      //--- 如果尚未指出不在焦点
      if(m_prev_header_index_focus!=WRONG_VALUE)
        {
         //--- 重置焦点
         m_prev_header_index_focus=WRONG_VALUE;
         //--- 改变颜色
         DrawTableHeaders();
         m_headers.Update();
        }
     }
//--- 若在焦点
   else
     {
      //--- 检查标题的焦点
      CheckHeaderFocus();
      //--- 如果无焦点
      if(m_prev_header_index_focus==WRONG_VALUE)
        {
         //--- 改变颜色
         DrawTableHeaders();
         m_headers.Update();
        }
     }
      }

以下是 CCanvasTable::CheckColumnResizeFocus() 方法的代码。需要确定标题间边界的焦点, 并负责在更改列宽时显示/隐藏光标。在方法开始时有两次检查。如果列宽改变模式被禁用, 程序将离开该方法。如果启用并正在更改列宽, 则需要更新鼠标光标坐标并离开该方法。

如果更改列宽度的过程尚未开始, 那么如果光标位于标题区域中, 则 尝试在循环中确定其中一个 的边框上的焦点。如果发现焦点, 更新鼠标光标坐标, 令其可见 并离开方法。如果焦点未发现, 则 指针将被隐藏。 

class CCanvasTable : public CElement
  {
private:
   //--- 检查标题边框上的焦点以更改其宽度
   void              CheckColumnResizeFocus(void);
  };
//+------------------------------------------------------------------+
//| 检查标题边框上的焦点以更改其宽度                                       |
//+------------------------------------------------------------------+
void CCanvasTable::CheckColumnResizeFocus(void)
  {
//--- 如果禁用更改列宽模式, 离开
   if(!m_column_resize_mode)
      return;
//--- 如果开始更改列宽, 离开
   if(m_column_resize_control!=WRONG_VALUE)
     {
      //--- 更新光标坐标并令其可见
      m_column_resize.Moving(m_mouse.X(),m_mouse.Y());
      return;
     }
//--- 检查标题边框上的焦点
   bool is_focus=false;
//--- 如果鼠标光标位于标题区域
   if(m_headers.MouseFocus())
     {
      //--- 标题坐标
      int x1=0,x2=0;
      //--- 获取 X 轴偏移量
      int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
      //--- 得到鼠标光标的相对坐标
      int x=m_mouse.X()-m_headers.X()+xoffset;
      //--- 搜索焦点
      for(int i=0; i<m_columns_total; i++)
        {
         //--- 计算坐标
         x1=x2+=m_vcolumns[i].m_width;
         //--- 验证焦点
         if(is_focus=x>x1-m_sep_x_offset && x<=x2+m_sep_x_offset)
            break;
        }
      //--- 如果这是焦点
      if(is_focus)
        {
         //--- 更新光标坐标并令其可见
         m_column_resize.Moving(m_mouse.X(),m_mouse.Y());
         //--- 显示光标
         m_column_resize.Show();
         return;
        }
     }
//--- 如果非焦点, 隐藏指针
   if(!m_headers.MouseFocus() || !is_focus)
      m_column_resize.Hide();
      }

最终结果如下:

 图例. 5. 列标题。

图例. 5. 列标题。

 

 


相对于列宽调整字符串长度

此前, 若要使文本不与相邻单元格重叠, 必须手动选择列宽并重新编译文件来查看结果。很自然, 这极不方便。

我们让字符串长度自动调整, 如果它不适合表格单元格。重新绘制表格时, 以前调整过的字符串将不会再次调整。将另一个数组添加到表格属性的结构中 以便保存这些字符串。

class CCanvasTable : public CElement
  {
private:
   //--- 表格数值和属性的数组
   struct CTOptions
     {
      string            m_vrows[];
      string            m_text[];
      int               m_width;
      ENUM_ALIGN_MODE   m_text_align;
     };
   CTOptions         m_vcolumns[];
      };

作为结果, m_vrows[] 将保存全部文本, 而 m_text[] 数组将保存调整版本的文本。

CCanvasTable::CorrectingText() 方法将负责调整 标题和表格单元两者内的 字符串长度。在识别操作的文本之后, 获取其宽度。接下来, 检查字符串的全文是否适合单元格, 同时考虑单元格边缘的所有偏移量。如果它适合, 将之保存在 m_text[] 数组里并离开方法。在当前版本中, 仅保存调整过的单元格文本, 但不针对标题。

如果文本不合适, 则 应修剪多余的字符, 并添加省略号 ('…')。省略号将表明所显示的文本已被裁剪。这个过程很容易实现:

1)获取字符串长度。

2)之后在循环里从最后一个字符开始遍历所有字符, 删除最后一个字符并将修剪后的文本保存在临时变量中。

如果没有字符剩下, 返回一个空字符串。

4)只要有字符剩下, 获取包含省略号在内的结果字符串宽度。

5)检查依次形成的字符串是否适合表格单元, 同时考虑自单元格边缘指定的偏移。

6)如果字符串合适, 则将其保存到方法的局部变量中并停止循环。

7)此后, 将调整后的字符串保存到 m_text[] 数组中, 并从方法返回。 

class CCanvasTable : public CElement
  {
private:
   //--- 返回依据列宽调整的文本
   string            CorrectingText(const int column_index,const int row_index,const bool headers=false);
  };
//+------------------------------------------------------------------+
//| 返回依据列宽调整的文本                                               |
//+------------------------------------------------------------------+
string CCanvasTable::CorrectingText(const int column_index,const int row_index,const bool headers=false)
  {
//--- 获取当前文本
   string corrected_text=(headers)? m_header_text[column_index]: m_vcolumns[column_index].m_vrows[row_index];
//--- 自单元格边缘在 X 轴上的偏移
   int x_offset=m_text_x_offset*2;
//--- 获取画板对象的指针
   CRectCanvas *obj=(headers)? ::GetPointer(m_headers) : ::GetPointer(m_table);
//--- 获取文本宽度
   int full_text_width=obj.TextWidth(corrected_text);
//--- 如果它适合单元格, 将调整后的文本保存在单独的数组中并返回
   if(full_text_width<=m_vcolumns[column_index].m_width-x_offset)
     {
      //--- 如果这些不是标题, 保存调整后的文本
      if(!headers)
         m_vcolumns[column_index].m_text[row_index]=corrected_text;
      //---
      return(corrected_text);
     }
//--- 如果文本不适合单元格, 则需要调整文本 (修剪过多字符并添加省略号)
   else
     {
      //--- 用来操纵字符串
      string temp_text="";
      //--- 获取字符串长度
      int total=::StringLen(corrected_text);
      //--- 从字符串中逐个删除字符, 直到所需文本宽度
      for(int i=total-1; i>=0; i--)
        {
         //--- 删除一个字符
         temp_text=::StringSubstr(corrected_text,0,i);
         //--- 如果什么都没剩下, 保留空字符串
         if(temp_text=="")
           {
            corrected_text="";
            break;
           }
         //--- 在检查之前添加省略号
         int text_width=obj.TextWidth(temp_text+"...");
         //--- 如果适合单元格
         if(text_width<m_vcolumns[column_index].m_width-x_offset)
           {
            //--- 保存文本并停止循环
            corrected_text=temp_text+"...";
            break;
           }
        }
     }
//--- 如果这些不是标题, 保存调整后的文本
   if(!headers)
      m_vcolumns[column_index].m_text[row_index]=corrected_text;
//--- 返回调整后的文字
   return(corrected_text);
      }

当重绘表格时使用调整后的字符串, 在更改列宽过程中尤为重要。不必在所有表格单元格中反复替换调整的文本, 只需在列单元格中进行此操作, 宽度将会被改变。这样可以节省 CPU 资源。

CCanvasTable::Text() 方法将确定是否需要调整指定列的文本, 或者发送以前调整的版本即可足矣。其代码如下所见: 
class CCanvasTable : public CElement
  {
private:
   //--- 返回文本
   string            Text(const int column_index,const int row_index);
  };
//+------------------------------------------------------------------+
//| 返回文本                                                           |
//+------------------------------------------------------------------+
string CCanvasTable::Text(const int column_index,const int row_index)
  {
   string text="";
//--- 如果不是更改列宽模式, 调整文本
   if(m_column_resize_control==WRONG_VALUE)
      text=CorrectingText(column_index,row_index);
//--- 如果处于更改列宽度模式下, 则...
   else
     {
      //--- ...仅宽度改变的列需要调整文本
      if(column_index==m_column_resize_control)
         text=CorrectingText(column_index,row_index);
      //--- 对于所有其它, 使用以前调整的文字
      else
         text=m_vcolumns[column_index].m_text[row_index];
     }
//--- 返回文本
   return(text);
      }

以下是 CCanvasTable::ChangeColumnWidth() 方法的代码, 设计用于改变列宽。

最小列宽设为 30 像素。如果标题显示被禁用, 程序将离开方法。如果检查通过, 则在标题的边界处检查焦点。如果检查确定进程尚未开始/完成, 辅助变量为零, 程序离开方法。如果进程正在运行, 则获取光标的相对 X 坐标。如果 进程恰好刚刚开始, 则必须保存当前光标的 X 坐标 (x_fixed 变量) 和所拖拽列的宽度 (prev_width 变量)。涉及此目的的局部变量是静态的。因此, 每次进入此方法时, 它们的值将会保存, 直到进程完成为止。 

现在, 计算列的新宽度。如果结果达到 最小列宽度, 程序将离开方法。否则, 指定列的新宽度保存在表格属性的结构中。此后, 重新计算并重新应用表格维度, 并在方法结束时重新绘制表格。 

class CCanvasTable : public CElement
  {
private:
   //--- 列的最小宽度
   int               m_min_column_width;
   //---
private:
   //--- 更改拖拽列的宽度
   void              ChangeColumnWidth(void);
  };
//+------------------------------------------------------------------+
//| 构造器                                                            |
//+------------------------------------------------------------------+
CCanvasTable::CCanvasTable(void) : m_min_column_width(30)
  {
   ...
  }
//+------------------------------------------------------------------+
//| 更改拖拽列的宽度                                                    |
//+------------------------------------------------------------------+
void CCanvasTable::ChangeColumnWidth(void)
  {
//--- 如果禁用标题, 离开
   if(!m_show_headers)
      return;
//--- 检查标题边界的焦点
   CheckColumnResizeFocus();
//--- 辅助变量
   static int x_fixed    =0;
   static int prev_width =0;
//--- 如果完成, 重置该值
   if(m_column_resize_control==WRONG_VALUE)
     {
      x_fixed    =0;
      prev_width =0;
      return;
     }
//--- 获取 X 轴偏移量
   int xoffset=(int)m_headers.GetInteger(OBJPROP_XOFFSET);
//--- 得到鼠标光标的相对坐标
   int x=m_mouse.X()-m_headers.X()+xoffset;
//--- 如果更改列宽的过程刚刚开始
   if(x_fixed<1)
     {
      //--- 保存当前列的 X 坐标和宽度
      x_fixed    =x;
      prev_width =m_vcolumns[m_column_resize_control].m_width;
     }
//--- 计算列的新宽度
   int new_width=prev_width+(x-x_fixed);
//--- 如果小于指定的极限, 保持不变
   if(new_width<m_min_column_width)
      return;
//--- 保存列的新宽度
   m_vcolumns[m_column_resize_control].m_width=new_width;
//--- 计算表格大小
   CalculateTableSize();
//--- 调整表的大小
   ChangeTableSize();
//--- 表格重绘
   DrawTable();
      }

结果如下:

 图例. 5. 相对于列的可变宽度调整字符串长度。

图例. 5. 相对于列的可变宽度调整字符串长度。 

 


事件处理

表格对象的颜色管理更变列宽度 是通过控件的鼠标移动事件 (CHARTEVENT_MOUSE_MOVE) 进行处理。 

//+------------------------------------------------------------------+
//| 事件处理器                                                         |
//+------------------------------------------------------------------+
void CCanvasTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- 处理光标移动事件
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- 如果控件被隐藏, 离开
      if(!CElementBase::IsVisible())
         return;
      //--- 如果子窗口的数字不匹配, 离开
      if(!CElementBase::CheckSubwindowNumber())
         return;
      //--- 检查焦点覆盖在元素上
      CElementBase::CheckMouseFocus();
      m_headers.MouseFocus(m_mouse.X()>m_headers.X() && m_mouse.X()<m_headers.X2() &&
                           m_mouse.Y()>m_headers.Y() && m_mouse.Y()<m_headers.Y2());
      //--- 如果滚动条处于激活状态
      if(m_scrollv.ScrollBarControl() || m_scrollh.ScrollBarControl())
        {
         ShiftTable();
         return;
        }
      //--- 更改对象颜色
      ChangeObjectsColor();
      //--- 更改拖拽列的宽度
      ChangeColumnWidth();
      return;
     }
   ...
      }

需要另一个新的事件标识符, 以便确定鼠标左键状态的变更时刻。需要在事件处理器程序代码的多个模块中摆脱重复检查和并发处理。将 ON_CHANGE_MOUSE_LEFT_BUTTON 标识符添加到 Define.mqh 文件: 

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                                 版权所有 2015, MetaQuotes 软件公司|
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
  #define ON_CHANGE_MOUSE_LEFT_BUTTON (33) // 改变鼠标左键的状态

此外, CMouse::CheckChangeLeftButtonState() 方法已经添加到类中以获取鼠标的当前参数 (CMouse)。它可以确定鼠标左键状态的变化时刻。该方法在类的处理器中调用。如果鼠标左键的状态发生变化, 方法发送消息 ON_CHANGE_MOUSE_LEFT_BUTTON 标识符。消息随后可在任何控件中被接收并处理。 

//+------------------------------------------------------------------+
//| 获取鼠标参数的类                                                    |
//+------------------------------------------------------------------+
class CMouse
  {
private:
   //--- 检查鼠标左键的状态变化
   bool              CheckChangeLeftButtonState(const string mouse_state);
  };
//+------------------------------------------------------------------+
//| 处理移动鼠标光标的事件                                               |
//+------------------------------------------------------------------+
void CMouse::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- 处理光标移动事件
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- 坐标和鼠标左键的状态
      m_x                 =(int)lparam;
      m_y                 =(int)dparam;
      m_left_button_state =CheckChangeLeftButtonState(sparam);
      ...
     }
  }
//+------------------------------------------------------------------+
//| 检查鼠标左键的状态变化                                               |
//+------------------------------------------------------------------+
bool CMouse::CheckChangeLeftButtonState(const string mouse_state)
  {
   bool left_button_state=(bool)int(mouse_state);
//--- 发送关于鼠标左键状态更改的消息
   if(m_left_button_state!=left_button_state)
      ::EventChartCustom(m_chart.ChartId(),ON_CHANGE_MOUSE_LEFT_BUTTON,0,0.0,"");
//---
   return(left_button_state);
      }

所需的 ON_CHANGE_MOUSE_LEFT_BUTTON 标识符的事件处理在 CCanvasTable 类之中:

  •  将类中的某些字段归零;
  •  调整滚动条;
  •  重绘表格
//+------------------------------------------------------------------+
//| 事件处理器                                                         |
//+------------------------------------------------------------------+
void CCanvasTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- 更改鼠标左键的状态
   if(id==CHARTEVENT_CUSTOM+ON_CHANGE_MOUSE_LEFT_BUTTON)
     {
      //--- 如果禁用标题, 离开
      if(!m_show_headers)
         return;
      //--- 如果鼠标左键释放
      if(!m_mouse.LeftButtonState())
        {
         //--- 重置宽度更改模式
         m_column_resize_control=WRONG_VALUE;
         //--- 隐藏光标
         m_column_resize.Hide();
         //--- 考虑最近变化来调整滚动条
         HorizontalScrolling(m_scrollh.CurrentPos());
        }
      //--- 重置标题上最后一个焦点的索引
      m_prev_header_index_focus=WRONG_VALUE;
      //--- 更改对象颜色
      ChangeObjectsColor();
     }
      }

本文的动画截图展示了 MQL 应用程序的运行结果, 可从下面的链接下载, 以便进一步学习。

 

结论

函数库的当前更新改进了 CCanvasTable 类型的渲染表格。这不是表格的最终版本。它将进一步开发, 并将添加新的功能。

创建图形界面函数库的当前规划图如下所示。

 图例. 6. 函数库结构的当前开发状态。

图例. 6. 函数库结构的当前开发状态。

 

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

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

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

附加的文件 |
带有图形界面的通用通道 带有图形界面的通用通道
所有通道指标显示为三条线, 包括中心, 顶部和底部线。中心线的绘图原理与移动平均线相似, 而移动均线指标主要用于绘制通道。顶部线和底部线的位置距中心线距离相等。距离的确定可以按照点为单位, 作为价格百分比 (包络指标), 使用标准偏差值 (布林带) 或 ATR 值 (Keltner 通道)。
可视化!类似于 R 语言 "plot (绘图)" 的 MQL5 图形库 可视化!类似于 R 语言 "plot (绘图)" 的 MQL5 图形库
在研究交易逻辑时, 图形形式的直观表达是非常重要的。科学界中流行的一些编程语言 (如 R 和 Python) 拥有可视化的特殊 "plot (绘图)" 功能。它能够以直观方式绘制线, 点分布和直方图。在 MQL5 中, 您可以使用 CGraphics 类完成相同的操作。
计算赫斯特指数 计算赫斯特指数
本文彻底解释了赫斯特指数背后的思想, 以及其价值观和计算算法的含义。分析了多个金融市场片段, 并介绍了使用 MetaTrader 5 产品实现分形分析的方法。
图形界面 X: 多行文本框控件 (集成编译 8) 图形界面 X: 多行文本框控件 (集成编译 8)
讨论多行文本框控件。不同于 OBJ_EDIT 类型的图形对象, 这一版本没有输入字符数量的限制。它还添加了将文本框转换为简单文本编辑器的模式, 其内可以使用鼠标或键盘移动光标。