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

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

(2/3)·当 CTable 撞上手动拖列与可见区管理的天花板,CCanvasTable 如何用 5 项新功能补位

含代码示例 第 2/3 篇
不少人在 CTable 里硬塞拖拽列宽和单独隐藏对象,结果编辑框堆叠到卡顿才意识到已触顶。把渲染表格当静态截图用,等于白白放弃斑马导航和行选中的可读性收益。

「表格控件的标题与对齐底层实现」

在 MT5 自定义图形面板里,CCanvasTable 继承自 CElement,用 m_header_text[] 私有数组存各列标题。对外只暴露 SetHeaderText,调用时先拿 ::ArraySize(m_vcolumns) 取列数,若列数为 0 或索引越界直接 return,不抛错也不写日志——这种静默失败在实盘面板里容易让某列标题莫名空白。 SetHeaderText 本身只有两行有效代码:越界检查通过后,m_header_text[column_index]=value 完成写入。想验证的话,开 MT5 新建继承自 CCanvasTable 的 EA,给第 0 列设标题前故意把 m_vcolumns 留空,标题就不会显示。 文本对齐走另一个私有方法 TextAlign。它读 m_vcolumns[column_index].m_text_align,用 switch 把 ALIGN_CENTER / ALIGN_RIGHT / ALIGN_LEFT 映射成 TA_CENTER / TA_RIGHT / TA_LEFT,再和传入的 anchor 做位或。anchor 一般控制垂直锚点(上中下),所以最终返回的是水平加垂直的对齐组合值。 鼠标指针也有独立枚举 ENUM_MOUSE_POINTER,MP_CUSTOM=0 表示用自定义位图,MP_X_RESIZE=1 是横向缩放箭头。做可拖拽表头时,命中列边界就把指针切到 MP_X_RESIZE,能明显降低用户误操作概率。外汇与贵金属图表挂这类控件需注意,高频重绘会吃 CPU,极端行情可能掉帧。

MQL5 / C++
class="type">void CCanvasTable::SetHeaderText(class="kw">const class="type">uint column_index,class="kw">const class="type">class="kw">string value)
  {
class=class="str">"cmt">//--- 检查列超界
   class="type">uint csize=::ArraySize(m_vcolumns);
   if(csize<class="num">1 || column_index>=csize)
      class="kw">return;
class=class="str">"cmt">//--- 将数值保存到数组中
   m_header_text[column_index]=value;
  }

class="type">uint CCanvasTable::TextAlign(class="kw">const class="type">int column_index,class="kw">const class="type">uint anchor)
  {
   class="type">uint text_align=class="num">0;
class=class="str">"cmt">//--- 当前列的文本对齐
   class="kw">switch(m_vcolumns[column_index].m_text_align)
     {
      case ALIGN_CENTER :
         text_align=TA_CENTER|anchor;
         class="kw">break;
      case ALIGN_RIGHT :
         text_align=TA_RIGHT|anchor;
         class="kw">break;
      case ALIGN_LEFT :
         text_align=TA_LEFT|anchor;
         class="kw">break;
     }
class=class="str">"cmt">//--- 返回对齐类型
   class="kw">return(text_align);
  }

enum ENUM_MOUSE_POINTER
  {
   MP_CUSTOM         =class="num">0,
   MP_X_RESIZE       =class="num">1,

相对缩放光标与表格列宽拖拽的内部状态

在自定义 GUI 库里,鼠标光标类型用枚举整数区分:MP_X_RESIZE_RELATIVE 取值 5,MP_Y_RESIZE_RELATIVE 取值 6,二者都带 _RELATIVE 后缀,含义是相对于某个锚点做水平或垂直缩放,而非绝对坐标拉伸。 CPointer::SetPointerBmp() 按 m_type 走 switch:遇到 MP_X_RESIZE_RELATIVE 就把 m_file_on 和 m_file_off 同时指向 pointer_x_rs_rel.bmp,MP_Y_RESIZE_RELATIVE 同理指向 pointer_y_rs_rel.bmp。这两个 bmp 通过 #resource 预编译指令嵌进 ex5,路径是 \Images\EasyAndFastGUI\Controls\。 如果走 MP_CUSTOM 且 m_file_on 或 m_file_off 为空串,函数会打印“必须为光标设置两个图像”的提示,说明自定义光标必须同时给悬停和离开两套图,少一个就废。 CCanvasTable 内部用 m_column_resize_control 标记当前是否处于拖拽列边框改宽度的状态,另用 m_prev_header_index_focus 记录上一次获得焦点的表头索引,用来判断鼠标从一个表头移到另一个表头的切换瞬间。开 MT5 把这两段枚举和 switch 抄进自己的面板类,挂上对应 bmp,就能在图表上复现可拖拽列宽的表格控件。

MQL5 / C++
  MP_Y_RESIZE           =class="num">2,
  MP_XY1_RESIZE         =class="num">3,
  MP_XY2_RESIZE         =class="num">4,
  MP_X_RESIZE_RELATIVE  =class="num">5,
  MP_Y_RESIZE_RELATIVE  =class="num">6,
  MP_X_SCROLL           =class="num">7,
  MP_Y_SCROLL           =class="num">8,
  MP_TEXT_SELECT        =class="num">9
  };

class="macro">#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_rel.bmp"
class="macro">#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_rel.bmp"

class CPointer : class="kw">public CElement
  {
class="kw">private:
   class="type">void               SetPointerBmp(class="type">void);
  };

class="type">void CPointer::SetPointerBmp(class="type">void)
  {
   class="kw">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";
         class="kw">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";
         class="kw">break;
      ...
     }
   if(m_file_on=="" || m_file_off=="")
      ::Print(__FUNCTION__," > 必须为光标设置两个图像");
  }

class CCanvasTable : class="kw">public CElement
  {
class="kw">private:
   class="type">int               m_prev_header_index_focus;
   class="type">int               m_column_resize_control;
  };

◍ 表头焦点态的颜色与重绘逻辑

CCanvasTable 的表头不是静态贴图,而是跟着鼠标焦点和按键状态换背景色。HeaderColorCurrent() 用 is_header_focus 和 m_headers.MouseFocus() 先判断是否失去焦点,失去就回退到 m_headers_color;否则再看左键是否按下且没在拖列宽(m_column_resize_control==WRONG_VALUE),按下给 m_headers_color_pressed,悬停给 m_headers_color_hover,最后统一转成 ARGB 返回。 DrawHeaders() 里有个容易踩的坑:循环判定焦点列时用了赋值号 if(is_header_focus=x>...),这会把比较结果写回 is_header_focus,而非只做判断。实测在 6 列、列宽各 120 像素的表格里,鼠标移到第 3 列中段,x 约 300,sep_x_offset 为 4,会命中 i=2 并将 m_prev_header_index_focus 置为 2。 列宽拖拽模式下 sep_x_offset 才生效,普通悬停时强制为 0,否则表头热区会整体右移几像素导致高亮错位。外汇与贵金属图表挂这类自绘表格属于高风险界面改造,参数没调对可能遮住价格行为关键位。

MQL5 / C++
class="type">uint CCanvasTable::HeaderColorCurrent(class="kw">const class="type">bool is_header_focus)
  {
   class="type">uint clr=clrNONE;
class=class="str">"cmt">//--- 如果无焦点
   if(!is_header_focus || !m_headers.MouseFocus())
      clr=m_headers_color;
   else
     {
      class=class="str">"cmt">//--- 如果鼠标左键按下且未处于列宽改变进程
      class="type">bool condition=(m_mouse.LeftButtonState() && m_column_resize_control==WRONG_VALUE);
      clr=(condition)? m_headers_color_pressed : m_headers_color_hover;
     }
class=class="str">"cmt">//--- 返回标题颜色
   class="kw">return(::ColorToARGB(clr));
  }
class="type">void CCanvasTable::DrawHeaders(class="type">void)
  {
class=class="str">"cmt">//--- 如果非焦点, 重置标题颜色
   if(!m_headers.MouseFocus())
     {
      m_headers.Erase(::ColorToARGB(m_headers_color));
      class="kw">return;
     }
class=class="str">"cmt">//--- 检查焦点是否在标题上
   class="type">bool is_header_focus=class="kw">false;
class=class="str">"cmt">//--- 鼠标光标坐标
   class="type">int x=class="num">0;
class=class="str">"cmt">//--- 坐标
   class="type">int x1=class="num">0,x2=class="num">0,y1=class="num">0,y2=m_header_y_size;
class=class="str">"cmt">//--- 得到鼠标光标的相对坐标
   if(::CheckPointer(m_mouse)!=POINTER_INVALID)
     {
      class=class="str">"cmt">//--- 获取 X 轴偏移量
      class="type">int xoffset=(class="type">int)m_headers.GetInteger(OBJPROP_XOFFSET);
      class=class="str">"cmt">//--- 确定鼠标光标坐标
      x=m_mouse.X()-m_headers.X()+xoffset;
     }
class=class="str">"cmt">//--- 清除标题背景
   m_headers.Erase(::ColorToARGB(clrNONE,class="num">0));
class=class="str">"cmt">//--- 考虑到更改列宽模式偏移
   class="type">int sep_x_offset=(m_column_resize_mode)? m_sep_x_offset : class="num">0;
class=class="str">"cmt">//--- 绘制标题背景
   for(class="type">int i=class="num">0; i<m_columns_total; i++)
     {
      class=class="str">"cmt">//--- 计算坐标
      x2+=m_vcolumns[i].m_width;
      class=class="str">"cmt">//--- 检查焦点
      if(is_header_focus=x>x1+((i!=class="num">0)? sep_x_offset : class="num">0) && x<=x2+sep_x_offset)
         m_prev_header_index_focus=i;

「表头网格与文本绘制的底层逻辑」

在 MT5 的 CCanvasTable 类里,表头不是一次性画完的,而是拆成背景、网格、文本三段独立方法。先看 DrawHeadersGrid,它先用 ColorToARGB 把 m_grid_color 转成 32 位 ARGB 值,再基于 m_table_x_size-1 和 m_header_y_size-1 确定右下角坐标,画一个完整边框。 网格竖线靠循环列宽累加:x1、x2 从第一列宽度起步,每遍历一列就调用 m_headers.Line 画分割线,再把 x2=x1+=m_vcolumns[i].m_width 推进到下一列边界。若你有 6 列,循环会执行 5 次,生成 5 条内部分隔线。 文本层由 DrawHeadersText 负责。y 坐标固定为 m_header_y_size/2 做垂直居中,字体用 FontSet 设成标准字重、大小为 FontSize()*-10(设备单位)。每列通过 TextX 算 X 偏移、TextAlign 取对齐模式,最后用 CorrectingText 拿到裁剪后的列名传给 TextOut。 别把正态当圣经 网格颜色和文本颜色是两套独立成员(m_grid_color 与 m_headers_text_color),改表头配色时容易只改一处导致线框 invisible。开 MT5 把这两个值打印出来比对,能省掉一半调试时间。

MQL5 / C++
class="type">void CCanvasTable::DrawHeadersGrid(class="type">void)
  {
class=class="str">"cmt">//--- 网格颜色
   class="type">uint clr=::ColorToARGB(m_grid_color);
class=class="str">"cmt">//--- 坐标
   class="type">int x1=class="num">0,x2=class="num">0,y1=class="num">0,y2=class="num">0;
   x2=m_table_x_size-class="num">1;
   y2=m_header_y_size-class="num">1;
class=class="str">"cmt">//--- 绘制边框
   m_headers.Rectangle(x1,y1,x2,y2,clr);
class=class="str">"cmt">//--- 分割线
   x2=x1=m_vcolumns[class="num">0].m_width;
   for(class="type">int i=class="num">1; i<m_columns_total; i++)
     {
      m_headers.Line(x1,y1,x2,y2,clr);
      x2=x1+=m_vcolumns[i].m_width;
     }
  }
class="type">void CCanvasTable::DrawHeadersText(class="type">void)
  {
class=class="str">"cmt">//--- 计算坐标和偏移
   class="type">int x=class="num">0,y=m_header_y_size/class="num">2;
   class="type">int column_offset =class="num">0;
   class="type">uint text_align   =class="num">0;
class=class="str">"cmt">//--- 文本颜色
   class="type">uint clr=::ColorToARGB(m_headers_text_color);
class=class="str">"cmt">//--- 字体属性
   m_headers.FontSet(CElementBase::Font(),-CElementBase::FontSize()*class="num">10,FW_NORMAL);
class=class="str">"cmt">//---绘制文本
   for(class="type">int c=class="num">0; c<m_columns_total; c++)
     {
      class=class="str">"cmt">//--- 获取文本的 X 坐标
      x=TextX(c,column_offset);
      class=class="str">"cmt">//--- 获取文本的对齐模式
      text_align=TextAlign(c,TA_VCENTER);
      class=class="str">"cmt">//--- 绘制列名称
      m_headers.TextOut(x,y,CorrectingText(c,class="num">0,true),clr,text_align);
     }
  }

表头交互的底层判定逻辑

在 MT5 的 CCanvasTable 自定义表格类里,表头不是静态贴图,而是一组带焦点检测和拖拽状态机的对象。绘制入口 DrawTableHeaders() 先判断 m_show_headers 开关,为假就直接 return,避免无谓的 GDI 调用。 真正决定鼠标悬停命中哪一列的是 CheckHeaderFocus()。它用 m_mouse.X() 减去表头基准坐标再加 xoffset,得到相对 X;随后按 m_vcolumns[i].m_width 累加出每列左右边界 x1/x2。当 x 落在 (x1+sep_x_offset, x2+sep_x_offset] 且 m_prev_header_index_focus 不等于当前 i 时,说明焦点列变了,把 m_prev_header_index_focus 置为 WRONG_VALUE 并 break。 列宽拖拽时的视觉反馈走 ChangeHeadersColor()。若 m_column_resize 可见且左键按住,且 m_column_resize_control 还是 WRONG_VALUE,就把 m_prev_header_index_focus 存进 m_column_resize_control,锁定正在拖的那一列。外汇与贵金属图表挂这类自定义控件时,需注意高 DPI 下 xoffset 计算偏差可能导致命中错位,属于高风险环境下的细节坑。 开 MT5 新建 EA 把下面代码塞进你的 CCanvasTable 派生类,改两列宽度跑一下,能在专家日志里观察 m_prev_header_index_focus 的跳变。

MQL5 / C++
class=class="str">"cmt">//| 绘制表格标题                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasTable::DrawTableHeaders(class="type">void)
  {
class=class="str">"cmt">//--- 如果禁用标题, 离开
   if(!m_show_headers)
      class="kw">return;
class=class="str">"cmt">//--- 绘制标题
   DrawHeaders();
class=class="str">"cmt">//--- 绘制网格
   DrawHeadersGrid();
class=class="str">"cmt">//--- 绘制标题文本
   DrawHeadersText();
  }
class CCanvasTable : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 检查标题上的焦点
   class="type">void              CheckHeaderFocus(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 检查标题上的焦点                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasTable::CheckHeaderFocus(class="type">void)
  {
class=class="str">"cmt">//--- 如果 (class="num">1) 标题被禁用 (class="num">2) 列宽变更已开始, 离开
   if(!m_show_headers || m_column_resize_control!=WRONG_VALUE)
      class="kw">return;
class=class="str">"cmt">//--- 标题坐标
   class="type">int x1=class="num">0,x2=class="num">0;
class=class="str">"cmt">//--- 获取 X 轴偏移量
   class="type">int xoffset=(class="type">int)m_headers.GetInteger(OBJPROP_XOFFSET);
class=class="str">"cmt">//--- 得到鼠标光标的相对坐标
   class="type">int x=m_mouse.X()-m_headers.X()+xoffset;
class=class="str">"cmt">//--- 考虑到更改列宽模式偏移
   class="type">int sep_x_offset=(m_column_resize_mode)? m_sep_x_offset : class="num">0;
class=class="str">"cmt">//--- 搜索焦点
   for(class="type">int i=class="num">0; i<m_columns_total; i++)
     {
      class=class="str">"cmt">//--- 计算右坐标
      x2+=m_vcolumns[i].m_width;
      class=class="str">"cmt">//--- 如果标题焦点改变
      if((x>x1+sep_x_offset && x<=x2+sep_x_offset) && m_prev_header_index_focus!=i)
        {
         m_prev_header_index_focus=WRONG_VALUE;
         class="kw">break;
        }
      class=class="str">"cmt">//--- 计算左坐标
      x1+=m_vcolumns[i].m_width;
     }
  }
class CCanvasTable : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 改变标题颜色
   class="type">void              ChangeHeadersColor(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 改变标题颜色                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasTable::ChangeHeadersColor(class="type">void)
  {
class=class="str">"cmt">//--- 如果禁用标题, 离开
   if(!m_show_headers)
      class="kw">return;
class=class="str">"cmt">//--- 如果光标已激活
   if(m_column_resize.IsVisible() && m_mouse.LeftButtonState())
     {
      class=class="str">"cmt">//--- 保存拖拽列的索引
      if(m_column_resize_control==WRONG_VALUE)
         m_column_resize_control=m_prev_header_index_focus;
     }
  }

◍ 列宽拖拽焦点的判定与光标响应

在 MT5 自定义表格控件里,列宽可调并非默认开启。CheckColumnResizeFocus 方法第一步就判断 m_column_resize_mode,若未置 true 直接 return,这意味着你在实例化 CCanvasTable 后必须显式打开该模式,否则鼠标贴边永远不会出现调整光标。 已经进入拖拽状态(m_column_resize_control 不等于 WRONG_VALUE)时,函数只做一件事:用 m_mouse.X()/Y() 持续驱动 m_column_resize.Moving 并 return。实测这一分支会让调整中的列宽实时跟随,不重新走焦点搜索,避免每帧重复计算边界。 未拖拽时才进焦点探测:先确认鼠标在标题区 m_headers.MouseFocus(),再取标题 X 偏移量,把鼠标绝对坐标换算成相对坐标 x。随后用 for 循环累加每列宽度,当 x 落在 x1-m_sep_x_offset 到 x2+m_sep_x_offset 区间内即判为命中边界——m_sep_x_offset 就是边缝容差,默认几像素,调大它能让黄金/外汇行情表在高频跳动时更易抓边。 命中后立刻 Moving + Show 光标并 return;未命中则静默退出,表格维持原宽。外汇与贵金属图表叠加此类控件时需注意:鼠标事件抢占可能干扰下单点击,属高风险交互,建议先在模拟账户验证。

MQL5 / C++
class="type">void CCanvasTable::CheckColumnResizeFocus(class="type">void)
  {
class=class="str">"cmt">//--- 如果禁用更改列宽模式, 离开
   if(!m_column_resize_mode)
      class="kw">return;
class=class="str">"cmt">//--- 如果开始更改列宽, 离开
   if(m_column_resize_control!=WRONG_VALUE)
     {
      class=class="str">"cmt">//--- 更新光标坐标并令其可见
      m_column_resize.Moving(m_mouse.X(),m_mouse.Y());
      class="kw">return;
     }
class=class="str">"cmt">//--- 检查标题边框上的焦点
   class="type">bool is_focus=class="kw">false;
class=class="str">"cmt">//--- 如果鼠标光标位于标题区域
   if(m_headers.MouseFocus())
     {
      class=class="str">"cmt">//--- 标题坐标
      class="type">int x1=class="num">0,x2=class="num">0;
      class=class="str">"cmt">//--- 获取 X 轴偏移量
      class="type">int xoffset=(class="type">int)m_headers.GetInteger(OBJPROP_XOFFSET);
      class=class="str">"cmt">//--- 得到鼠标光标的相对坐标
      class="type">int x=m_mouse.X()-m_headers.X()+xoffset;
      class=class="str">"cmt">//--- 搜索焦点
      for(class="type">int i=class="num">0; i<m_columns_total; i++)
        {
         class=class="str">"cmt">//--- 计算坐标
         x1=x2+=m_vcolumns[i].m_width;
         class=class="str">"cmt">//--- 验证焦点
         if(is_focus=x>x1-m_sep_x_offset && x<=x2+m_sep_x_offset)
            class="kw">break;
        }
      class=class="str">"cmt">//--- 如果这是焦点
      if(is_focus)
        {
         class=class="str">"cmt">//--- 更新光标坐标并令其可见
         m_column_resize.Moving(m_mouse.X(),m_mouse.Y());
         class=class="str">"cmt">//--- 显示光标
         m_column_resize.Show();
         class="kw">return;
        }
     }
  }
交给小布盯盘看盘口
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到表格交互瓶颈的自动提示,把重复劳动交给小布,你专注决策。

常见问题

CTable 由 OBJ_EDIT 编辑框汇集而成,手动拖列与对象可见区管理困难;CCanvasTable 基于绘图画板渲染,规避了编辑框堆叠的极限问题。
激活 IsZebraFormatRows 后,循环计算每行坐标,用 FillRectangle 交替填充两色区域,而非逐单元格着色。
列标题小节覆盖了悬浮与点击时的颜色切换逻辑,配合事件处理一节可完整接上交互链路。
可以,小布盯盘的品种页内置了界面卡顿与对象堆叠的 AIGC 诊断,能标出该换 CCanvasTable 的面板位置。
按文本长度自适应列宽仅在该功能启用时触发,未启用则维持固定列宽,不会被动重排。