请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化平台 帖子:3365083 新帖:0

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

汇市江湖百晓生发表于:4 月 17 日 15:33回复(1)


内容

  • 概论

  • 格式化斑马样式

  • 选择和弃选表格行

  • 列标题

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

  • 事件处理

  • 结论


概论

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

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

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

  • 图形界面 VII: 表格控件 (第 1 章)

  • 图形界面 X: 函数库的轻松快捷更新 (集成编译 3)


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

 图例. 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. 函数库结构的当前开发状态。

 

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

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


全部回复

0/140

量化课程

    移动端课程