图形界面 VII: 表格控件 (第一章)·综合运用
📘

图形界面 VII: 表格控件 (第一章)·综合运用

第 3/3 篇

图表对象点击与计时器的表格联动

在 MT5 自定义控件里,表格滚动条的交互靠 CHARTEVENT_OBJECT_CLICK 事件驱动。当 sparam 命中标号为垂直或水平滚动条增减按钮的对象时,OnClickScrollInc / OnClickScrollDec 返回 true,随即调用 UpdateTable() 重绘可视区域,这种写法能把 UI 响应压到一次事件内完成。 计时器回调 OnEventTimer 则负责周期性快进。若当前是下拉元件直接 FastSwitching(),否则先判断所属窗口 IsLocked() 状态——未被屏蔽才执行,避免锁定时误触表格跳动。 把这两段接进你自己的 CElement 派生类,开 MT5 用 OnInit 挂 ChartSetInteger(0,CHART_EVENT_OBJECT_CLICK,true) 就能实测点击滚动是否生效;外汇与贵金属图表控件改动存在滑点误触高风险,建议先在模拟盘验证。

MQL5 / C++
  if(id==CHARTEVENT_OBJECT_CLICK)
    {
      class=class="str">"cmt">//--- 如果在表格滚动条按钮上按下按键
     if(m_scrollv.OnClickScrollInc(sparam) || m_scrollv.OnClickScrollDec(sparam) ||
         m_scrollh.OnClickScrollInc(sparam) || m_scrollh.OnClickScrollDec(sparam))
        class=class="str">"cmt">//--- 相对滚动条移动表格
        UpdateTable();
      class=class="str">"cmt">//---
      class="kw">return;
    }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计时器方法                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLabelsTable::OnEventTimer(class="type">void)
  {
class=class="str">"cmt">//--- 如果是下拉元件
   if(CElement::IsDropdown())
      FastSwitching();
class=class="str">"cmt">//--- 如果不是下拉元件,要考虑当前表单是否可用
   else
     {
      class=class="str">"cmt">//--- 只有在表单没有被屏蔽时进行表格的快进
      if(!m_wnd.IsLocked())
         FastSwitching();
     }
  }

「在面板里钉死表头和首列」

给 MT5 的 EA 图形界面加一张文字标签表格,核心类就是 CLabelsTable。声明实例后,真正干活的是 CProgram::CreateLabelsTable(),这里把表格规模定死:21 列、100 行,但只露出 5 列 10 行,其余靠滚动条看。 顶部第一行和左边第一列必须锁住,调用 FixFirstRow(true) 与 FixFirstColumn(true) 即可,这样横向竖向翻动时表头不会跑出视野。表格宽设为 400、列偏移 75、首列偏移 40,这些数字直接决定肉眼观感,调参时建议先在脚本里改一处编译看效果。 填充逻辑分三层:左上角单元写“-”;列头循环写 “SYMBOL n”,行头写 “PARAMETER n” 且右对齐;数据区用 rand()%1000 - rand()%1000 塞入 -1000~1000 的随机整数。正数绿、负数红由控件默认配色处理,但最近一次改动必须手动触发更新才会重绘。 计时器 OnTimerEvent() 里补一段改值代码,就能在图表运行时动态刷新单元格。CreateLabelsTable() 要在 CreateExpertPanel() 主方法内被调用,编译挂图后界面即出图 2 那样的控件,验证通过再写下一种表格类。

MQL5 / C++
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
   class=class="str">"cmt">//--- 文字标签型表格
   CLabelsTable        m_labels_table;
   class=class="str">"cmt">//---
class="kw">private:
   class=class="str">"cmt">//--- 文字标签型表格
class="macro">#define LABELS_TABLE1_GAP_X(class="num">1)
class="macro">#define LABELS_TABLE1_GAP_Y(class="num">42)
   class="type">bool                CreateLabelsTable(class="type">void);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建文字标签型表格                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateLabelsTable(class="type">void)
  {
class="macro">#define COLUMNS1_TOTAL(class="num">21)
class="macro">#define ROWS1_TOTAL(class="num">100)
class=class="str">"cmt">//--- 保存指向表单的指针
   m_labels_table.WindowPointer(m_window1);
class=class="str">"cmt">//--- 坐标
   class="type">int x=m_window1.X()+LABELS_TABLE1_GAP_X;
   class="type">int y=m_window1.Y()+LABELS_TABLE1_GAP_Y;
class=class="str">"cmt">//--- 可见的列数和行数
   class="type">int visible_columns_total =class="num">5;
   class="type">int visible_rows_total    =class="num">10;
class=class="str">"cmt">//--- 设置属性
   m_labels_table.XSize(class="num">400);
   m_labels_table.XOffset(class="num">40);
   m_labels_table.ColumnXOffset(class="num">75);
   m_labels_table.FixFirstRow(true);
   m_labels_table.FixFirstColumn(true);
   m_labels_table.TableSize(COLUMNS1_TOTAL,ROWS1_TOTAL);
   m_labels_table.VisibleTableSize(visible_columns_total,visible_rows_total);
class=class="str">"cmt">//--- 创建表格
   if(!m_labels_table.CreateLabelsTable(m_chart_id,m_subwin,x,y))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 填充表格:
class=class="str">"cmt">//    第一个单元是空的
   m_labels_table.SetValue(class="num">0,class="num">0,"-");
class=class="str">"cmt">//--- 列的表头
   for(class="type">int c=class="num">1; c<COLUMNS1_TOTAL; c++)
     {
       for(class="type">int r=class="num">0; r<class="num">1; r++)
         m_labels_table.SetValue(c,r,"SYMBOL "+class="type">class="kw">string(c));
     }
class=class="str">"cmt">//--- 行的头部,文字是右边对齐的
   for(class="type">int c=class="num">0; c<class="num">1; c++)
     {
       for(class="type">int r=class="num">1; r<ROWS1_TOTAL; r++)
         m_labels_table.SetValue(c,r,"PARAMETER "+class="type">class="kw">string(r));
     }
class=class="str">"cmt">//--- 数据和表格的格式 (背景和单元的颜色)
   for(class="type">int c=class="num">1; c<COLUMNS1_TOTAL; c++)
     {
       for(class="type">int r=class="num">1; r<ROWS1_TOTAL; r++)
         m_labels_table.SetValue(c,r,class="type">class="kw">string(::rand()%class="num">1000-::rand()%class="num">1000));
     }
class=class="str">"cmt">//--- 设置表格单元的文字颜色
   for(class="type">int c=class="num">1; c<m_labels_table.ColumnsTotal(); c++)
      for(class="type">int r=class="num">1; r<m_labels_table.RowsTotal(); r++)

◍ 用定时器驱动面板表格的刷新节奏

在 MT5 的 GUI 程序里,OnTimerEvent 是控制界面刷新频率的核心入口。示例里用了一个静态变量 count 配合 TIMER_STEP_MSC 做累加,只有当累计达到 500 毫秒才执行一次状态行与表格的写入,其余 tick 直接 return,避免每帧都重绘拖慢终端。 表格数据填充部分用双重 for 循环遍历列与行,从第二列、第二行开始(索引 1)写入随机值:string(::rand()%1000 - ::rand()%1000),也就是在 -999 到 999 之间波动。随后立刻按数值正负把文字染成绿或红,再调用 UpdateTable() 推到前端。 CreateExpertPanel 里只负责把 CreateLabelsTable 跑通,失败就返 false,成功才 m_chart.Redraw()。你在自己 EA 里接面板时,把 500 这个阈值改成 200 或 1000,就能直观比对 MT5 在不同刷新周期下的资源占用差异,外汇与贵金属品种因点差跳变快,高频重绘可能放大卡顿风险。

MQL5 / C++
   m_labels_table.TextColor(c,r,((class="type">class="kw">double)m_labels_table.GetValue(c,r)>=class="num">0) ? clrGreen : clrRed);
class=class="str">"cmt">//--- 更新表格
   m_labels_table.UpdateTable();
class=class="str">"cmt">//--- 把元件指针加到库中
   CWndContainer::AddToElementsArray(class="num">0,m_labels_table);
   class="kw">return(true);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计时器方法                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::OnTimerEvent(class="type">void)
  {
  CWndEvents::OnTimerEvent();
class=class="str">"cmt">//--- 每隔500毫秒更新状态行的第二部分
  class="kw">static class="type">int count=class="num">0;
  if(count<class="num">500)
    {
     count+=TIMER_STEP_MSC;
     class="kw">return;
    }
class=class="str">"cmt">//--- 重置计时器
  count=class="num">0;
class=class="str">"cmt">//--- 改变状态行第二部分的数值
  m_status_bar.ValueToItem(class="num">1,::TimeToString(::TimeLocal(),TIME_DATE|TIME_SECONDS));
class=class="str">"cmt">//--- 在表格中填入数据
  for(class="type">int c=class="num">1; c<m_labels_table.ColumnsTotal(); c++)
    for(class="type">int r=class="num">1; r<m_labels_table.RowsTotal(); r++)
      m_labels_table.SetValue(c,r,class="type">class="kw">string(::rand()%class="num">1000-::rand()%class="num">1000));
class=class="str">"cmt">//--- 设置表格单元的文字颜色
  for(class="type">int c=class="num">1; c<m_labels_table.ColumnsTotal(); c++)
    for(class="type">int r=class="num">1; r<m_labels_table.RowsTotal(); r++)
      m_labels_table.TextColor(c,r,((class="type">class="kw">double)m_labels_table.GetValue(c,r)>=class="num">0) ? clrGreen : clrRed);
class=class="str">"cmt">//--- 更新表格
  m_labels_table.UpdateTable();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建EA交易面板                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateExpertPanel(class="type">void)
  {
class=class="str">"cmt">//--- 创建控件表单1
class=class="str">"cmt">//--- 创建控件:
class=class="str">"cmt">//   主菜单
class=class="str">"cmt">//--- 上下文菜单
class=class="str">"cmt">//--- 创建状态行
class=class="str">"cmt">//--- 文字标签型表格
  if(!CreateLabelsTable())
     class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 重绘图表
  m_chart.Redraw();
  class="kw">return(true);
  }

编辑框型表格比标签型多出哪些可控项

编辑框型表格和纯文字标签型的最大差别,在于它把单元格当成了可交互的输入区。除了改文字颜色,你还能单独设某个单元格的对齐方式(左/中/右),重设背景色与边框色,并在开启编辑模式后由用户直接改数值。 这种控件在 MT5 自定义面板里由四个部分拼成:背景层、编辑框、垂直滚动条、水平滚动条。多出来的滚动条和编辑框意味着你要在资源里多声明控件 ID,否则运行时表格只显示一半。 实际写代码时,和标签型表格的区别主要集中在 CTable 的构造参数和后续 SetCellText 之外的 SetCellColor / SetCellAlign 调用。下一段直接看声明差异。

「用编辑框搭出可交互表格」

CTable 类和之前用静态文字标签的表格最大区别,是可见区域每个单元格挂的是 CEdit 编辑框对象,而不是只读 Label。这意味着单元格能改对齐、换背景色、改字色,交互维度比纯文字表多出一截。 这类表格多出一串文字标签表没有的属性:可编辑模式、鼠标悬停高亮、整行选中、行高、网格色、表头背景与文字色、悬停单元色、默认字色与对齐、选中行背景与文字色。还支持锁定首行首列为表头,滚动时原地不动。 设置完属性必须调 CTable::UpdateTable() 才能刷到界面上,否则改了也白改。表格管理方法都是 private,外部碰不到:处理行点击、单元输入、从对象名解析 ID/列号/行号、高亮选中行、悬停变色、数据快滚。 OnClickTableRow 开局就拦几种情况:可编辑模式开着的、滚动条在拖的、点的不是表格单元、控件 ID 不对。通过后按当前表头锁定和垂直滚动位置遍历找命中单元,存下索引和值;点表头就直接退,否则发自定义消息带图表 ID、ON_CLICK_LIST_ITEM、控件 ID、行索引。 OnEndEditCell 只在可编辑模式下干活,禁用了或名称/ID 不匹配就退。用 ColumnIndexFromObjectName 和 RowIndexFromObjectName 拿可见区索引,再加滚动偏移得到数据数组索引;值真变了才写数组并发 ON_END_EDIT 消息,行内用“_”拼列行索引和值。 HighlightSelectedItem 在开启行高亮时把整行背景和字色换成特殊色,编辑模式开或高亮模式关就退。RowColorByHover 管悬停高亮,编辑中、表单屏蔽、滚动中都退,已高亮行不重复染。注意垂直滚动处理后也会再调一次 HighlightSelectedItem。

MQL5 / C++
class CTable : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 表格可见部分的对象数组
   class="kw">struct TEdits
     {
      CEdit            m_rows[];
     };
   TEdits            m_columns[];
  };
class CTable : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 表格值和属性的数组
   class="kw">struct TOptions
     {
      class="type">class="kw">string           m_vrows[];
      ENUM_ALIGN_MODE  m_text_align[];
      class="type">color            m_text_color[];
      class="type">color            m_cell_color[];
     };
   TOptions         m_vcolumns[];
  };
class CTable : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 表格中行的高度
   class="type">int              m_row_y_size;
   class=class="str">"cmt">//--- (class="num">1) 背景颜色 和(class="num">2)表格的背景边框颜色
   class="type">color            m_area_color;
   class="type">color            m_area_border_color;
   class=class="str">"cmt">//--- 网格颜色
   class="type">color            m_grid_color;
   class=class="str">"cmt">//--- 表头背景颜色
   class="type">color            m_headers_color;
   class=class="str">"cmt">//--- 表头文字颜色
   class="type">color            m_headers_text_color;
   class=class="str">"cmt">//--- 不同状态下单元的颜色
   class="type">color            m_cell_color;
   class="type">color            m_cell_color_hover;
   class=class="str">"cmt">//--- 单元文字的默认颜色
   class="type">color            m_cell_text_color;
   class=class="str">"cmt">//--- 被选中行的 (class="num">1)背景颜色 和(class="num">2)文字颜色
   class="type">color            m_selected_row_color;

◍ 表格控件的私有状态与公共接口

在 MT5 自定义表格控件里,私有成员决定了渲染与交互的底层行为。比如 m_fix_first_row 和 m_fix_first_column 这两个 bool 变量,直接控制首行、首列是否在滚动时保持锁定;m_read_only 切换可编辑与只读,m_lights_hover 控制鼠标掠过时的高亮反馈。 公共接口则是一对对 getter/setter。FixFirstRow(void) 返回当前锁定状态,FixFirstRow(const bool flag) 接收新值写入 m_fix_first_row;FixFirstColumn 同理操作首列。AreaColor 与 BorderColor 分别设置背景与边框色,HeadersColor、HeadersTextColor、GridColor 管表头底色、表头字色和网格线颜色。 下面这段声明与实现可直接贴进你的 CWndTable 派生类头文件,编译后就能在面板里实时改样式。外汇与贵金属图表上挂这类控件需注意:高频刷新可能拖慢老机型 MT5 终端,属于高风险环境下的性能权衡。 别把私有变量当摆设 m_selectable_row 若设为 true,用户点哪行哪行反色,配合 m_selected_row_text_color 能做出订单列表选中效果;但忘记初始化这个颜色值,默认可能是黑底黑字——什么也看不见。

MQL5 / C++
  class="type">color                m_selected_row_text_color;
  class=class="str">"cmt">//--- 可编辑表格模式
  class="type">bool                 m_read_only;
  class=class="str">"cmt">//--- 当鼠标掠过时突出显示的模式
  class="type">bool                 m_lights_hover;
  class=class="str">"cmt">//--- 可选择行模式
  class="type">bool                 m_selectable_row;
  class=class="str">"cmt">//--- 第一行的锁定模式
  class="type">bool                 m_fix_first_row;
  class=class="str">"cmt">//--- 第一列的锁定模式
  class="type">bool                 m_fix_first_column;
  class=class="str">"cmt">//--- 编辑框中默认的文字对齐方式
  ENUM_ALIGN_MODE      m_align_mode;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- (class="num">1) 背景色 和 (class="num">2) 表格边框颜色
  class="type">void                 AreaColor(const class="type">color clr)                       { m_area_color=clr;                }
  class="type">void                 BorderColor(const class="type">color clr)                     { m_area_border_color=clr;         }
  class=class="str">"cmt">//--- (class="num">1) 读取 和 (class="num">2) 设置第一行的锁定模式
  class="type">bool                 FixFirstRow(class="type">void)                        const    { class="kw">return(m_fix_first_row);         }
  class="type">void                 FixFirstRow(const class="type">bool flag)                      { m_fix_first_row=flag;            }
  class=class="str">"cmt">//--- (class="num">1) 读取 和 (class="num">2) 设置第一列的锁定模式
  class="type">bool                 FixFirstColumn(class="type">void)                      const    { class="kw">return(m_fix_first_column);      }
  class="type">void                 FixFirstColumn(const class="type">bool flag)                    { m_fix_first_column=flag;         }
  class=class="str">"cmt">//--- (class="num">1) 表头背景色, (class="num">2) 表头文字颜色 (class="num">3) 表格网格颜色
  class="type">void                 HeadersColor(const class="type">color clr)                     { m_headers_color=clr;             }
  class="type">void                 HeadersTextColor(const class="type">color clr)                  { m_headers_text_color=clr;        }
  class="type">void                 GridColor(const class="type">color clr)                         { m_grid_color=clr;                }

表格控件的 setter 接口怎么写

在 MT5 自定义面板里做报价表或信号矩阵时,CTable 和内部行单元类靠一批 setter 控制外观与交互。下面这段接口声明把单元格高度、配色、悬停高亮、只读模式全部暴露出来,调参不用碰绘制逻辑。 行单元类里 RowYSize 接收 int 设定行高,CellColor 与 CellColorHover 分别接管常态和鼠标掠过时的背景色;注释标了三种交互模式——只读、悬停突出、整行可选,对应 ReadOnly / LightsHover / SelectableRow 三个 bool 开关。 CTable 自身则在行列二维坐标上做精细控制:TableSize 与 VisibleTableSize 分离总表尺寸和可视区域,TextAlign、TextColor、CellColor 都带 column_index 与 row_index,意味着单格级覆盖是支持的;SetValue 按坐标写字符串,取数接口紧随其后。 实盘面板若要在黄金 15 分钟周期上做多品种波动率速览,先把 LightsHover 设为 true,行高给到 18~22 像素,鼠标扫过才能快速定位异动格。外汇与贵金属波动剧烈,面板只是辅助,任何信号都只是概率倾向,不等于方向确认。

MQL5 / C++
class="type">void                RowYSize(const class="type">int y_size)              { m_row_y_size=y_size;             }
class="type">void                CellColor(const class="type">color clr)              { m_cell_color=clr;                }
class="type">void                CellColorHover(const class="type">color clr)          { m_cell_color_hover=clr;          }
class=class="str">"cmt">//--- (class="num">1) "只读", (class="num">2) 当鼠标掠过时突出显示, (class="num">3) 可选择行 模式 
class="type">void                ReadOnly(const class="type">bool flag)               { m_read_only=flag;               }
class="type">void                LightsHover(const class="type">bool flag)            { m_lights_hover=flag;            }
class="type">void                SelectableRow(const class="type">bool flag)          { m_selectable_row=flag;          }
class=class="str">"cmt">//--- 单元文字对齐方式
class="type">void                TextAlign(const ENUM_ALIGN_MODE align_mode)   { m_align_mode=align_mode;     }
};
class CTable : class="kw">public CElement
  {
class="kw">public:
  class=class="str">"cmt">//--- 设置 (class="num">1) 表格的大小 和 (class="num">2) 可见部份的大小
  class="type">void                TableSize(const class="type">int columns_total,const class="type">int rows_total);
  class="type">void                VisibleTableSize(const class="type">int visible_columns_total,const class="type">int visible_rows_total);
  class=class="str">"cmt">//--- 设置 (class="num">1)文字的对齐模式, (class="num">2)文字颜色, (class="num">3)单元背景色
  class="type">void                TextAlign(const class="type">int column_index,const class="type">int row_index,const ENUM_ALIGN_MODE mode);
  class="type">void                TextColor(const class="type">int column_index,const class="type">int row_index,const class="type">color clr);
  class="type">void                CellColor(const class="type">int column_index,const class="type">int row_index,const class="type">color clr);
  class=class="str">"cmt">//--- 设置指定表格单元的值
  class="type">void                SetValue(const class="type">int column_index,const class="type">int row_index,const class="type">class="kw">string value);
  class=class="str">"cmt">//--- 取得指定表格单元的值

「表格行点击的事件拦截与坐标反查」

在 MT5 自定义表格控件里,行点击不是直接透传的,CTable 先靠 OnClickTableRow 做几道闸门:可编辑模式开启、垂直/水平滚动条处于拖动态、 clicked_object 名称里找不到 '_table_edit_' 前缀,任一命中就直接 return false,避免误触发行选中。 真正定位行号时,代码用 IdFromObjectName 从图形对象名反解控件 ID,再双重比对 CElement::Id() 防止跨表串扰;若 ID 不符同样退出。这里有个容易踩的坑:固定表头开启时,循环起点 t 会被置为 1,否则为 0,行索引计算要跟着偏移。 定位循环里,列层取 m_scrollv.CurrentPos()+t 作为可视区垂直基准,行层从 t 扫到 m_visible_rows_total,命中 m_columns[c].m_rows[r].Name()==clicked_object 后,把 row_index 与单元描述分别写进 m_selected_item / m_selected_item_text,随后跳出。注意 v 的自增受 v>=t && v<m_rows_total 约束,否则滚动到底部会越界。 最后一道判断:固定表头且 row_index<1 视为点了表头,不派发消息;过关才调用 EventChartCustom 以 ON_CLICK_LIST_ITEM 事件把选中行号推给图表脚本。外汇与贵金属面板高频刷新时,这种事件拦截能明显降低误选概率,但自定义控件本身仍属高风险扩展,建议先在模拟盘验证。

MQL5 / C++
class="type">bool CTable::OnClickTableRow(const class="type">class="kw">string clicked_object)
  {
  class=class="str">"cmt">//--- 如果启用了可编辑表格模式,就退出
   if(!m_read_only)
      class="kw">return(class="kw">false);
  class=class="str">"cmt">//--- 如果滚动条是活动的,就退出
   if(m_scrollv.ScrollState() || m_scrollh.ScrollState())
      class="kw">return(class="kw">false);
  class=class="str">"cmt">//--- 如果没有按在表格单元上,就退出
   if(::StringFind(clicked_object,CElement::ProgramName()+"_table_edit_",class="num">0)<class="num">0)
      class="kw">return(class="kw">false);
  class=class="str">"cmt">//--- 从对象名称中取得ID
   class="type">int id=IdFromObjectName(clicked_object);
  class=class="str">"cmt">//--- 如果ID不匹配就退出
   if(id!=CElement::Id())
      class="kw">return(class="kw">false);
  class=class="str">"cmt">//--- 搜索行的索引
   class="type">int row_index=class="num">0;
  class=class="str">"cmt">//--- 如果启用了固定表头模式,偏移一个索引
   class="type">int t=(m_fix_first_row) ? class="num">1 : class="num">0;
  class=class="str">"cmt">//--- 列
   for(class="type">int c=class="num">0; c<m_visible_columns_total; c++)
     {
     class=class="str">"cmt">//--- 取得垂直滚动条滑块的当前位置
      class="type">int v=m_scrollv.CurrentPos()+t;
     class=class="str">"cmt">//--- 行
      for(class="type">int r=t; r<m_visible_rows_total; r++)
        {
        class=class="str">"cmt">//--- 如果没有按在此单元上
         if(m_columns[c].m_rows[r].Name()==clicked_object)
           {
           class=class="str">"cmt">//--- 保存行的索引
            m_selected_item=row_index=v;
           class=class="str">"cmt">//--- 保存单元内容
            m_selected_item_text=m_columns[c].m_rows[r].Description();
            class="kw">break;
           }
        class=class="str">"cmt">//--- 增加行计数器
         if(v>=t && v<m_rows_total)
            v++;
        }
     }
  class=class="str">"cmt">//--- 如果按在表头上,就退出
   if(m_fix_first_row && row_index<class="num">1)
      class="kw">return(class="kw">false);
  class=class="str">"cmt">//--- 发送相关消息
   ::EventChartCustom(m_chart_id,ON_CLICK_LIST_ITEM,CElement::Id(),m_selected_item,"");
  }

◍ 表格单元编辑的事件回调怎么写

CTable 继承自 CElement,私有方法里挂了三个关键函数:OnEndEditCell 处理单元格输入事件,ColumnIndexFromObjectName 和 RowIndexFromObjectName 从 GUI 对象名反解行列坐标。对象名约定带前缀 ProgramName()+"_table_edit_",靠 StringFind 判存在性,不匹配直接 return(false)。 OnEndEditCell 里先判 m_read_only,若只读表格直接退出;再用 IdFromObjectName 比对控件 ID,防止误触其他元素。列行可视索引 c、r 通过那两个私有方法拿到后,要叠加横向/纵向滚动偏移:vc=c+m_scrollh.CurrentPos()、vr=r+m_scrollv.CurrentPos(),这才是数据数组里的真实下标。 若开启了首行固定(m_fix_first_row)且 r==0,强制 vr=0 避免表头被滚动逻辑错位。单元文本取自 m_columns[c].m_rows[r].Description(),与 m_vcolumns[vc].m_vrows[vr] 不等才写回 SetValue 并抛 EventChartCustom(...,ON_END_EDIT,...),消息体拼成 "vc_vr_text"。外汇/贵金属面板做可编辑参数表时,这种回调漏了滚动偏移会改错行,概率极高。 ColumnIndexFromObjectName 用 '_'(ushort 0x5F)做分隔符,StringSplit 后取 ArraySize(result)-1 再减 3 做越界保护,不足就 Print(PREVENTING_OUT_OF_RANGE)。对象名格式必须稳定,否则 array_size-3<0 会静默失败。

MQL5 / C++
class="type">bool CTable::OnEndEditCell(const class="type">class="kw">string edited_object)
  {
  class=class="str">"cmt">//--- 如果禁用了可编辑表格模式,就退出
   if(m_read_only)
      class="kw">return(class="kw">false);
  class=class="str">"cmt">//--- 如果没有按在表格单元上,就退出
   if(::StringFind(edited_object,CElement::ProgramName()+"_table_edit_",class="num">0)<class="num">0)
      class="kw">return(class="kw">false);
  class=class="str">"cmt">//--- 从对象名称中取得ID
   class="type">int id=IdFromObjectName(edited_object);
  class=class="str">"cmt">//--- 如果ID不匹配就退出
   if(id!=CElement::Id())
      class="kw">return(class="kw">false);
  class=class="str">"cmt">//--- 取得单元的列和行的索引
   class="type">int c =ColumnIndexFromObjectName(edited_object);
   class="type">int r =RowIndexFromObjectName(edited_object);
  class=class="str">"cmt">//--- 取得数据数组中列和行的索引
   class="type">int vc =c+m_scrollh.CurrentPos();
   class="type">int vr =r+m_scrollv.CurrentPos();
  class=class="str">"cmt">//--- 如果点击了表头,调整行的索引
   if(m_fix_first_row && r==class="num">0)
      vr=class="num">0;
  class=class="str">"cmt">//--- 取得输入的值
   class="type">class="kw">string cell_text=m_columns[c].m_rows[r].Description();
  class=class="str">"cmt">//--- 如果单元的值已经有改变
   if(cell_text!=m_vcolumns[vc].m_vrows[vr])
     {
      class=class="str">"cmt">//--- 在数组中保存值
      SetValue(vc,vr,cell_text);
      class=class="str">"cmt">//--- 发送相关消息
      ::EventChartCustom(m_chart_id,ON_END_EDIT,CElement::Id(),class="num">0,class="type">class="kw">string(vc)+"_"+class="type">class="kw">string(vr)+"_"+cell_text);
     }
  class=class="str">"cmt">//---
   class="kw">return(true);
  }

class="type">int CTable::ColumnIndexFromObjectName(const class="type">class="kw">string object_name)
  {
   class="type">class="kw">ushort u_sep=class="num">0;
   class="type">class="kw">string result[];
   class="type">int    array_size=class="num">0;
  class=class="str">"cmt">//--- 获取分隔符的代码
   u_sep=::StringGetCharacter("_",class="num">0);
  class=class="str">"cmt">//--- 分割字符串
   ::StringSplit(object_name,u_sep,result);
   array_size=::ArraySize(result)-class="num">1;
  class=class="str">"cmt">//--- 检查是否超出数组范围
   if(array_size-class="num">3<class="num">0)
     {
      ::Print(PREVENTING_OUT_OF_RANGE);

从对象名反解行列索引与选中行高亮

CTable 里有两个私有方法负责把图形对象名还原成行列坐标。RowIndexFromObjectName 用下划线作分隔符切分对象名,取倒数第二项作为行索引:若切分后数组长度减 1 再减 2 小于 0,说明字符串格式不对,直接打印 PREVENTING_OUT_OF_RANGE 并返回 WRONG_VALUE,避免越界读数组。 对应地,前文还有个取列索引的逻辑,取的是 result[array_size-3],也就是倒数第三项。也就是说对象命名约定大概是「前缀_列_行_其他」,列在行前面两位,解析时靠数组尾偏移定位,写 EA 调用表格对象时要严格遵守这个命名结构,否则索引会错。 HighlightSelectedItem 控制选中行的视觉反馈。它先判断 m_read_only 与 m_selectable_row 是否同时为真,任一为假就直接 return,不会画高亮。固定表头或首列时,纵向偏移 t 和横向偏移 l 取 1,否则取 0,用来跳过被冻结的那一行/列。 双循环里,h 和 v 分别叠加了滚动条 CurrentPos(),再按 m_selected_item==v 决定用选中色还是原单元格色。实测若 m_rows_total 为 20、当前选中第 5 行,则只有 v==5 的那些单元格会刷成 m_selected_row_color,其余保持原色——开 MT5 挂个表格对象改 m_selected_item 就能肉眼验证高亮是否跟手。外汇与贵金属图表上做这类自定义表格控件,需注意图形对象频繁重绘带来的 CPU 占用风险。

MQL5 / C++
class="type">int CTable::RowIndexFromObjectName(const class="type">class="kw">string object_name)
  {
   class="type">class="kw">ushort u_sep=class="num">0;
   class="type">class="kw">string result[];
   class="type">int    array_size=class="num">0;
class=class="str">"cmt">//--- 获取分隔符的代码
   u_sep=::StringGetCharacter("_",class="num">0);
class=class="str">"cmt">//--- 分割字符串
   ::StringSplit(object_name,u_sep,result);
   array_size=::ArraySize(result)-class="num">1;
class=class="str">"cmt">//--- 检查是否超出数组范围
   if(array_size-class="num">2<class="num">0)
     {
      ::Print(PREVENTING_OUT_OF_RANGE);
      class="kw">return(WRONG_VALUE);
     }
class=class="str">"cmt">//--- 返回项目索引
   class="kw">return((class="type">int)result[array_size-class="num">2]);
  }

class="type">void CTable::HighlightSelectedItem(class="type">void)
  {
class=class="str">"cmt">//--- 如果禁用了其中一个模式 ("只读", "行可被选中"),就退出
   if(!m_read_only || !m_selectable_row)
      class="kw">return;
class=class="str">"cmt">//--- 如果启用了固定表头模式,偏移一个索引
   class="type">int t=(m_fix_first_row) ? class="num">1 : class="num">0;
   class="type">int l=(m_fix_first_column) ? class="num">1 : class="num">0;
class=class="str">"cmt">//--- 取得水平滚动条滑块的当前位置
   class="type">int h=m_scrollh.CurrentPos()+l;
class=class="str">"cmt">//--- 列
   for(class="type">int c=l; c<m_visible_columns_total; c++)
     {
      class=class="str">"cmt">//--- 取得垂直滚动条滑块的当前位置
      class="type">int v=m_scrollv.CurrentPos()+t;
      class=class="str">"cmt">//--- 行
      for(class="type">int r=t; r<m_visible_rows_total; r++)
        {
         class=class="str">"cmt">//--- 移动表格数据
         if(v>=t && v<m_rows_total)
           {
            class=class="str">"cmt">//--- 考虑到被选中的行而做出调整
            class="type">color back_color=(m_selected_item==v) ? m_selected_row_color : m_vcolumns[h].m_cell_color[v];
            class="type">color text_color=(m_selected_item==v) ? m_selected_row_text_color : m_vcolumns[h].m_text_color[v];
            class=class="str">"cmt">//--- 修改单元的文字颜色和背景色
            m_columns[c].m_rows[r].Color(text_color);
            m_columns[c].m_rows[r].BackColor(back_color);
            v++;
           }
        }
      class=class="str">"cmt">//---
      h++;
     }
  }

「鼠标掠过时的表格行高亮逻辑」

在 MT5 自定义表格控件里,光标掠过行高亮不是简单换色,而要先过三道拦截:悬停高亮开关关闭、表单只读标志未置位、或窗口被锁,函数直接 return;滚动条处于拖动状态时也同样放弃重绘,避免闪烁。 固定表头或首列开启时,行列索引要分别偏移 1(t 和 l),否则会把标题行当数据行染色。水平、垂直滚动条 CurrentPos() 加上偏移量,才是当前可视区映射到数据数组的真实起点。 双重 for 循环按列、行遍历可视单元,用鼠标 x、y 与单元格 X()/X2()/Y()/Y2() 边界做命中判断。命中就刷 m_cell_color_hover,移出则按 m_vcolumns[h].m_cell_color[v] 还原——这里 h、v 随循环自增,若你发现 hover 后颜色串列,优先查滚动偏移是否漏加。 被选中行在只读且允许行选模式下会被 continue 跳过,不抢 hover 色。外汇面板写这类控件时,MT5 图表交互的高频重绘本身不杠杆风险,但误染会让盯盘者读错报价行,实盘贵金属跳空时段尤其扰人。

MQL5 / C++
class="type">void CTable::RowColorByHover(const class="type">int x,const class="type">int y)
  {
class=class="str">"cmt">//--- 如果光标掠过则突出显示的模式被禁止或者表单被屏蔽,则退出
   if(!m_lights_hover || !m_read_only || m_wnd.IsLocked())
      class="kw">return;
class=class="str">"cmt">//--- 如果滚动条在移动过程中,则退出
   if(m_scrollv.ScrollState() || m_scrollh.ScrollState())
      class="kw">return;
class=class="str">"cmt">//--- 如果启用了固定表头模式,偏移一个索引
   class="type">int t=(m_fix_first_row) ? class="num">1 : class="num">0;
   class="type">int l=(m_fix_first_column) ? class="num">1 : class="num">0;
class=class="str">"cmt">//--- 取得水平滚动条滑块的当前位置
   class="type">int h=m_scrollh.CurrentPos()+l;
class=class="str">"cmt">//--- 列
   for(class="type">int c=l; c<m_visible_columns_total; c++)
     {
      class=class="str">"cmt">//--- 取得垂直滚动条滑块的当前位置
      class="type">int v=m_scrollv.CurrentPos()+t;
      class=class="str">"cmt">//--- 行
      for(class="type">int r=t; r<m_visible_rows_total; r++)
        {
         class=class="str">"cmt">//--- 检查以免超出数组范围
         if(v>=t && v<m_rows_total)
           {
            class=class="str">"cmt">//--- 如果是"只读"模式,启用了行选择的模式,而到达了被选中的行,就跳过
            if(m_selected_item==v && m_read_only && m_selectable_row)
              {
               v++;
               class="kw">continue;
              }
            class=class="str">"cmt">//--- 如果是光标掠过的行,把它突出显示
            if(x>m_columns[class="num">0].m_rows[r].X() && x<m_columns[m_visible_columns_total-class="num">1].m_rows[r].X2() &&
                y>m_columns[c].m_rows[r].Y() && y<m_columns[c].m_rows[r].Y2())
              {
               m_columns[c].m_rows[r].BackColor(m_cell_color_hover);
              }
            class=class="str">"cmt">//--- 如果光标到了范围之外,恢复默认颜色
            else
              {
               if(v>=t && v<m_rows_total)
                 m_columns[c].m_rows[r].BackColor(m_vcolumns[h].m_cell_color[v]);
              }
            class=class="str">"cmt">//---
            v++;
           }
        }
      class=class="str">"cmt">//---
      h++;
     }
  }

◍ 表格控件的鼠标与点击事件分发

在 MT5 自定义表格类里,OnEvent 是图表事件的总入口。它接住三种常用事件:鼠标移动、对象点击、输入框结束编辑,分别对应 CHARTEVENT_MOUSE_MOVE、CHARTEVENT_OBJECT_CLICK、CHARTEVENT_OBJECT_ENDEDIT。 鼠标移动时先判断元件是否可见,不可见直接 return,避免无谓重绘。lparam 和 dparam 拆出 x、y 坐标,sparam 转成鼠标按键状态;再用 X()/X2()/Y()/Y2() 围成的矩形做命中测试,决定 MouseFocus 是否生效。 若竖向或横向滚动条捕获到操作,就调 UpdateTable 重排内容,随后 HighlightSelectedItem 保持选中行高亮,RowColorByHover 按光标位置刷行色。点击分支里,OnClickTableRow 优先处理行选中;否则看四个滚动条按钮增量/减量事件,命中同样走 UpdateTable + 高亮。 输入框结束编辑走 CHARTEVENT_OBJECT_ENDEDIT,由 OnEndEditCell 落值后 ResetColors 清掉临时着色。外汇与贵金属图表挂这类控件时,高频鼠标事件可能拖累tick响应,建议在可见性判断前加节流。

MQL5 / C++
class="type">void CTable::OnEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- 处理鼠标光标移动事件
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      class=class="str">"cmt">//--- 如果元件是隐藏的就退出
      if(!CElement::IsVisible())
         class="kw">return;
      class=class="str">"cmt">//--- 鼠标左键的坐标和状态
      class="type">int x=(class="type">int)lparam;
      class="type">int y=(class="type">int)dparam;
      m_mouse_state=(class="type">bool)class="type">int(sparam);
      CElement::MouseFocus(x>X() && x<X2() && y>Y() && y<Y2());
      class=class="str">"cmt">//--- 如果滚动条是活动的
      if(m_scrollv.ScrollBarControl(x,y,m_mouse_state) || m_scrollh.ScrollBarControl(x,y,m_mouse_state))
         class=class="str">"cmt">//--- 移动表格
         UpdateTable();
      class=class="str">"cmt">//--- 突出显示被选中的行
      HighlightSelectedItem();
      class=class="str">"cmt">//--- 当鼠标光标掠过时改变表格行的颜色
      RowColorByHover(x,y);
      class="kw">return;
     }
class=class="str">"cmt">//--- 处理对象的点击
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      class=class="str">"cmt">//--- 如果按下表格行
      if(OnClickTableRow(sparam))
        {
         class=class="str">"cmt">//--- 突出显示被选中的行
         HighlightSelectedItem();
         class="kw">return;
        }
      class=class="str">"cmt">//--- 如果按下滚动条按钮
      if(m_scrollv.OnClickScrollInc(sparam) || m_scrollv.OnClickScrollDec(sparam) ||
         m_scrollh.OnClickScrollInc(sparam) || m_scrollh.OnClickScrollDec(sparam))
        {
         class=class="str">"cmt">//--- 根据最近的变化更新表格数据
         UpdateTable();
         class=class="str">"cmt">//--- 突出显示被选中的行
         HighlightSelectedItem();
         class="kw">return;
        }
      class="kw">return;
     }
class=class="str">"cmt">//--- 处理输入栏位中数值改变的事件
   if(id==CHARTEVENT_OBJECT_ENDEDIT)
     {
      OnEndEditCell(sparam);
      class=class="str">"cmt">//--- 重置表格颜色
      ResetColors();
      class="kw">return;
     }
  }

编辑框型表格的 63 字符硬限制

在 CProgram 里挂一个 CTable 实例,把前面文本标签表格的元件清掉,就能单独测编辑框型表格。建表时设 100 列 1000 行,可见区只给 6 列 15 行,表头首行首列锁死,开行选中与悬停高亮,填充后必须调更新才会出图。 代码里 TABLE1_GAP_X 是 1、TABLE1_GAP_Y 是 42,表格 XSize 写死 600、行高 20;FixFirstRow / FixFirstColumn 都置 true,LightsHover 和 SelectableRow 也开。第一列用 ALIGN_RIGHT,其余居中,表头底色 C'255,244,213'、悬停金 clrGold。 实测跑通后界面正常,但单个单元格塞超过 63 个字符就显示不全。MT5 所有图形对象文字都卡在 63 字符上限,要破这个限得用 CCanvas 自己绘图——前面画分割线、右键菜单和信息界面时已经这么干过。

  • 字符在真实行情数据里经常不够用,所以下一版表格类会直接基于 CCanvas 重画,不再吃系统图形对象的文字长度亏。
MQL5 / C++
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
   class=class="str">"cmt">//--- 编辑框型表格
   CTable            m_table;
   class=class="str">"cmt">//---
class="kw">private:
   class=class="str">"cmt">//--- 编辑框型表格
class="macro">#define TABLE1_GAP_X(class="num">1)
class="macro">#define TABLE1_GAP_Y(class="num">42)
   class="type">bool              CreateTable(class="type">void);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建表格                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateTable(class="type">void)
  {
class="macro">#define COLUMNS1_TOTAL(class="num">100)
class="macro">#define ROWS1_TOTAL(class="num">1000)
class=class="str">"cmt">//--- 保存表单的指针
   m_table.WindowPointer(m_window1);
class=class="str">"cmt">//--- 坐标
   class="type">int x=m_window1.X()+TABLE1_GAP_X;
   class="type">int y=m_window1.Y()+TABLE1_GAP_Y;
class=class="str">"cmt">//--- 可见的列数和行数
   class="type">int visible_columns_total =class="num">6;
   class="type">int visible_rows_total    =class="num">15;
class=class="str">"cmt">//--- 在创建之前设置属性
   m_table.XSize(class="num">600);
   m_table.RowYSize(class="num">20);
   m_table.FixFirstRow(true);
   m_table.FixFirstColumn(true);
   m_table.LightsHover(true);
   m_table.SelectableRow(true);
   m_table.TextAlign(ALIGN_CENTER);
   m_table.HeadersColor(C&class="macro">#x27;class="num">255,class="num">244,class="num">213&class="macro">#x27;);
   m_table.HeadersTextColor(clrBlack);
   m_table.GridColor(clrLightGray);
   m_table.CellColorHover(clrGold);
   m_table.TableSize(COLUMNS1_TOTAL,ROWS1_TOTAL);
   m_table.VisibleTableSize(visible_columns_total,visible_rows_total);
class=class="str">"cmt">//--- 创建控件元件
   if(!m_table.CreateTable(m_chart_id,m_subwin,x,y))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 填充表格:
class=class="str">"cmt">//    第一个单元是空的
   m_table.SetValue(class="num">0,class="num">0,"-");
class=class="str">"cmt">//--- 列的表头
   for(class="type">int c=class="num">1; c<COLUMNS1_TOTAL; c++)
     {
      for(class="type">int r=class="num">0; r<class="num">1; r++)
         m_table.SetValue(c,r,"SYMBOL "+class="type">class="kw">string(c));
     }
class=class="str">"cmt">//--- 行的头部,文字是右边对齐的
   for(class="type">int c=class="num">0; c<class="num">1; c++)
     {
      for(class="type">int r=class="num">1; r<ROWS1_TOTAL; r++)
        {
         m_table.SetValue(c,r,"PARAMETER "+class="type">class="kw">string(r));
         m_table.TextAlign(c,r,ALIGN_RIGHT);
        }
     }
class=class="str">"cmt">//---  数据和表格的格式(背景和单元颜色)
   for(class="type">int c=class="num">1; c<COLUMNS1_TOTAL; c++)
     {

「用循环给表格上色并挂进对象树」

在 MQL5 自定义面板里,表格视觉区分靠行列双奇偶判断最省事。下面这段把列号 c 与行号 r 拼成单元格文本,同时按列奇偶切换文字红/蓝,按行奇偶切换底色白烟/纯白,肉眼一眼能分出奇偶区。 循环从 r=1 跑到 ROWS1_TOTAL-1,跳过了表头行;若你表头占两行,这里就要改成 r<ROWS1_TOTAL-1 或调整起始值,否则会盖掉表头。 表格改完必须调 UpdateTable() 才会重绘,漏掉这行 MT5 上看到的就是旧数据。最后用 AddToElementsArray(0,m_table) 把表格塞进窗口容器数组,EA 面板才会在图表上真正显示出来。 CreateExpertPanel() 里只判了 CreateTable() 的返回值,返回 false 就直接退出,说明表格创建失败会阻断后续菜单和状态行挂载——调试时若面板空白,先查这个函数返回。

MQL5 / C++
for(class="type">int r=class="num">1; r<ROWS1_TOTAL; r++)
  {
   m_table.SetValue(c,r,class="type">class="kw">string(c)+":"+class="type">class="kw">string(r));
   m_table.TextColor(c,r,(c%class="num">2==class="num">0)? clrRed : clrRoyalBlue);
   m_table.CellColor(c,r,(r%class="num">2==class="num">0)? clrWhiteSmoke : clrWhite);
  }
 }
class=class="str">"cmt">//--- 更新表格以显示改变
 m_table.UpdateTable();
class=class="str">"cmt">//--- 把对象加到对象组的总的数组中
 CWndContainer::AddToElementsArray(class="num">0,m_table);
 class="kw">return(true);
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建EA交易面板                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateExpertPanel(class="type">void)
  {
class=class="str">"cmt">//--- 创建控件表单1
class=class="str">"cmt">//--- 创建控件:
class=class="str">"cmt">//   主菜单
class=class="str">"cmt">//--- 上下文菜单
class=class="str">"cmt">//--- 创建状态行
class=class="str">"cmt">//--- 编辑框型表格
   if(!CreateTable())
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 重绘图表
 m_chart.Redraw();
 class="kw">return(true);
 }

◍ 用位图标签搭出可拉伸的表格

绘制型表格和前面几种靠文字标签、编辑框拼出来的表格不同,它每个单元格能塞任意长度字符,列宽也能在不改变表格可见区域总宽的前提下单独调。这一点在 OBJ_LABEL 或 OBJ_EDIT 堆出来的表格里基本做不到。 实现上它只依赖一种对象:OBJ_BITMAP_LABEL。比起混用多种 GUI 对象,维护起来简单很多,背景、表格本体、垂直滚动条、水平滚动条都画在同一位图标签上。 从组件构成看,绘制型表格包含背景层、绘制表格层、垂直滚动条和水平滚动条四部分。想要验证,开 MT5 新建一个 OBJ_BITMAP_LABEL,用 ResourceCreate 建好位图后直接往上面绘制行列与滚动区即可。外汇与贵金属图表叠加此类控件时需注意,高频刷新可能拖慢老旧终端,属高风险环境下的性能权衡。

在画布上搭一个可滚动的表格控件

做绘制型表格前,先准备一个 CTOptions 结构存每列的宽度与文字对齐方式。调用 TableSize 设总行列数时,代码里强制列数最少 1、行数最少 2,并把每列默认宽度写成 100 像素、对齐方式 ALIGN_CENTER,这决定了你不开参数时表格的长相。 列宽和对齐后续都能改,不用重建对象:传一个 ENUM_ALIGN_MODE 数组给 TextAlign,或传 int 数组给 ColumnsWidth 即可逐列覆盖默认值。适合在运行时根据品种报价位数动态拉宽某一列。 真正排版发生在 CalculateTableSize:依据所有列宽、行高和是否带滚动条算出整图尺寸,随后 CreateCells 建好控件。网格由 DrawGrid 两个循环画出——先水平线后垂直线;DrawText 更复杂,要同时看当前列和前一列的对齐来算缩进,左右边距硬编码 10 像素、上边距 3 像素,错一个就可能文字压线。 滚动不靠挪标签对象,而是在原图上移一个可见矩形:原图尺寸 = 全部列宽和全部行高,用 OBJPROP_XSIZE/YSIZE 设可见范围、OBJPROP_XOFFSET/YOFFSET 设偏移。ShiftTable 按滚动条位置移它,垂直步长等于行高、水平按像素走。外汇和贵金属行情跳动快,这种自绘表在高频刷新时比标准控件更稳,但 MT5 图形对象权限配置错会直接不显示,属高风险调试项。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 用于创建绘制型表格的类                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CCanvasTable : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 表格值和属性的数组
  class="kw">struct CTOptions
    {
      class="type">class="kw">string                m_vrows[];
      class="type">int                   m_width;
      ENUM_ALIGN_MODE       m_text_align;
    };
  CTOptions                m_vcolumns[];
  };
class CCanvasTable : class="kw">public CElement
  {
class="kw">public:
  class=class="str">"cmt">//--- 设置表格的大小
  class="type">void                TableSize(const class="type">int columns_total,const class="type">int rows_total);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置表格的大小                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasTable::TableSize(const class="type">int columns_total,const class="type">int rows_total)
  {
class=class="str">"cmt">//--- 必须至少有一列
  m_columns_total=(columns_total<class="num">1) ? class="num">1 : columns_total;
class=class="str">"cmt">//--- 必须至少有两行
  m_rows_total=(rows_total<class="num">2) ? class="num">2 : rows_total;
class=class="str">"cmt">//--- 设置列数组的大小
  ::ArrayResize(m_vcolumns,m_columns_total);
class=class="str">"cmt">//--- 设置行数组的大小
  for(class="type">int i=class="num">0; i<m_columns_total; i++)
    {
      ::ArrayResize(m_vcolumns[i].m_vrows,m_rows_total);
      class=class="str">"cmt">//--- 使用默认值初始化列的属性
      m_vcolumns[i].m_width      =class="num">100;
      m_vcolumns[i].m_text_align =ALIGN_CENTER;
    }
  }
class CCanvasTable : class="kw">public CElement
  {
class="kw">public:
  class=class="str">"cmt">//--- 设置每一列的 (class="num">1) 文字对齐方式 和 (class="num">2) 宽度
  class="type">void                TextAlign(const ENUM_ALIGN_MODE &array[]);
  class="type">void                ColumnsWidth(const class="type">int &array[]);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 填充文字对齐方式的数组                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasTable::TextAlign(const ENUM_ALIGN_MODE &array[])
  {

「表格尺寸与列属性的底层绑定」

在 MT5 自定义表格控件里,列对齐方式和列宽度都靠两个独立方法灌入数组。ColumnsWidth 和前面的列对齐方法逻辑一致:先取 ArraySize,若数组为空直接 return,再用三元运算把写入量截断在 m_columns_total 以内,避免越界写结构体。 这两个方法都只负责把外部 int 数组搬到 m_vcolumns[c] 的成员里,真正的排版压力在 CalculateTableSize。该方法先累加所有列宽得 m_table_x_size;当总列宽小于控件宽时,若有垂直滚动条就扣掉 ScrollWidth(),否则只减 2 像素边框。 高度计算更直接:m_table_y_size = m_cell_y_size * m_rows_total - (m_rows_total - 1),也就是每个单元格高度乘行数再减去行间隙。若横向溢出出现水平滚动条,m_y_size 还会加上 ScrollWidth() - 1。外汇与贵金属图表挂这类自绘控件时,需注意高分屏下 cell_y_size 设太小可能导致文字截断,属高风险界面适配点。 想验证截断逻辑,把 m_columns_total 设成 3,却传入 5 个元素的宽度数组,调试里看 m_vcolumns[3] 和 [4] 应保持未初始化——这能确认边界保护生效。

MQL5 / C++
class="type">int total=class="num">0;
class="type">int array_size=::ArraySize(array);
class=class="str">"cmt">//--- 如果传入的数组大小为0,退出
if(array_size<class="num">1)
   class="kw">return;
class=class="str">"cmt">//--- 调整数值以免数组超出范围
total=(array_size<m_columns_total)? array_size : m_columns_total;
class=class="str">"cmt">//--- 在结构中保存数值
for(class="type">int c=class="num">0;c<total;c++)
   m_vcolumns[c].m_text_align=array[c];
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 填充列宽度的数组                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasTable::ColumnsWidth(const class="type">int &array[])
  {
   class="type">int total=class="num">0;
   class="type">int array_size=::ArraySize(array);
class=class="str">"cmt">//--- 如果传入的数组大小为0,退出
   if(array_size<class="num">1)
      class="kw">return;
class=class="str">"cmt">//--- 调整数值以免数组超出范围 
   total=(array_size<m_columns_total)? array_size : m_columns_total;
class=class="str">"cmt">//--- 在结构中保存数值
   for(class="type">int c=class="num">0;c<total;c++)
      m_vcolumns[c].m_width=array[c];
  }
class CCanvasTable : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 表格可见部分的总大小
   class="type">int            m_table_x_size;
   class="type">int            m_table_y_size;
   class="type">int            m_table_visible_x_size;
   class="type">int            m_table_visible_y_size;
class=class="str">"cmt">//---
class="kw">private:
   class=class="str">"cmt">//--- 计算表格大小
   class="type">void           CalculateTableSize(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计算表格大小                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasTable::CalculateTableSize(class="type">void)
  {
class=class="str">"cmt">//--- 计算表格总的宽度
   m_table_x_size=class="num">0;
   for(class="type">int c=class="num">0;c<m_columns_total;c++)
      m_table_x_size=m_table_x_size+m_vcolumns[c].m_width;
class=class="str">"cmt">//--- 带有垂直滚动条的表格宽度
   class="type">int x_size=(m_rows_total>m_visible_rows_total) ? m_x_size-m_scrollh.ScrollWidth() : m_x_size-class="num">2;
class=class="str">"cmt">//--- 如果所有列的宽度小于表格宽度,使用表格宽度
   if(m_table_x_size<m_x_size)
      m_table_x_size=x_size;
class=class="str">"cmt">//--- 计算表格的总高度
   m_table_y_size=m_cell_y_size*m_rows_total-(m_rows_total-class="num">1);
class=class="str">"cmt">//--- 设置显示图形部分(表格的可见部分)的边框大小
   m_table_visible_x_size=x_size;
   m_table_visible_y_size=m_cell_y_size*m_visible_rows_total-(m_visible_rows_total-class="num">1);
class=class="str">"cmt">//--- 如果有水平滚动条,调整控件在Y轴上的大小
   class="type">int y_size=m_cell_y_size*m_visible_rows_total+class="num">2-(m_visible_rows_total-class="num">1);
   m_y_size=(m_table_x_size>m_table_visible_x_size) ? y_size+m_scrollh.ScrollWidth()-class="num">1 : y_size;
  }
class CCanvasTable : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 网格颜色
   class="type">color          m_grid_color;
   class=class="str">"cmt">//--- 单元的大小(高度)
   class="type">int            m_cell_y_size;
class=class="str">"cmt">//---
class="kw">public:
   class=class="str">"cmt">//--- 网格颜色

◍ 网格与文字绘制的底层坐标逻辑

CCanvasTable 的 DrawGrid 把画布可用区定为 XSize()-1 与 YSize()-1,即右下角留 1 像素边,避免线被裁切。水平线按 m_cell_y_size-1 步进,循环 m_rows_total+1 次(含首行 y=0),所以 10 行表会画 11 条横线。 垂直线遍历 m_columns_total 列,每次 x 偏移累加 m_vcolumns[i].m_width,循环结束后再补一条 x=x_size 的右边框线。注意 x2=x1+=width 是后置赋值,线起点用的仍是上一列右沿。 DrawText 里写死了两个偏移:cell_x_offset=10、cell_y_offset=3。也就是说单元格文字恒距左边界 10 像素、距上边界 3 像素,想紧凑显示就把这两个数往下调,但改完要在 MT5 里看是否压线。 颜色统一走 ColorToARGB(clr,255),alpha 写死 255 表示不透明。若你用小布盯盘做贵金属面板,网格和文字色都靠 GridColor / TextColor 设,外汇品种高频刷新时建议浅灰网格降低视觉噪点,但任何可视化改动都不改价格风险本质。

MQL5 / C++
class="type">void GridColor(const class="type">color clr) { m_grid_color=clr; }
class=class="str">"cmt">//---
class="kw">private:
  class=class="str">"cmt">//--- 画网格
  class="type">void DrawGrid(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 画出网格                                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasTable::DrawGrid(class="type">void)
  {
class=class="str">"cmt">//--- 网格颜色
   class="type">uint clr=::ColorToARGB(m_grid_color,class="num">255);
class=class="str">"cmt">//--- 用于绘画的画布大小
   class="type">int x_size =m_canvas.XSize()-class="num">1;
   class="type">int y_size =m_canvas.YSize()-class="num">1;
class=class="str">"cmt">//--- 坐标
   class="type">int x1=class="num">0,x2=class="num">0,y1=class="num">0,y2=class="num">0;
class=class="str">"cmt">//--- 水平线
   x1=class="num">0;
   y1=class="num">0;
   x2=x_size;
   y2=class="num">0;
   for(class="type">int i=class="num">0; i<=m_rows_total; i++)
     {
      m_canvas.Line(x1,y1,x2,y2,clr);
      y2=y1+=m_cell_y_size-class="num">1;
     }
class=class="str">"cmt">//--- 垂直线
   x1=class="num">0;
   y1=class="num">0;
   x2=class="num">0;
   y2=y_size;
   for(class="type">int i=class="num">0; i<m_columns_total; i++)
     {
      m_canvas.Line(x1,y1,x2,y2,clr);
      x2=x1+=m_vcolumns[i].m_width;
     }
class=class="str">"cmt">//--- 向右
   x1=x_size;
   y1=class="num">0;
   x2=x_size;
   y2=y_size;
   m_canvas.Line(x1,y1,x2,y2,clr);
   }
class CCanvasTable : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 文字颜色
  class="type">color          m_cell_text_color;
class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 表格文字的颜色
  class="type">void          TextColor(const class="type">color clr) { m_cell_text_color=clr; }
class=class="str">"cmt">//---
class="kw">private:
  class=class="str">"cmt">//--- 绘制文字
  class="type">void          DrawText(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 绘制文字                                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasTable::DrawText(class="type">void)
  {
class=class="str">"cmt">//--- 用于计算坐标和偏移
   class="type">int  x          =class="num">0;
   class="type">int  y          =class="num">0;
   class="type">uint text_align =class="num">0;
   class="type">int  column_offset =class="num">0;
   class="type">int  cell_x_offset =class="num">10;
   class="type">int  cell_y_offset =class="num">3;
class=class="str">"cmt">//--- 文字颜色
   class="type">uint clr=::ColorToARGB(m_cell_text_color,class="num">255);
class=class="str">"cmt">//--- 字体属性

表头文字对齐的偏移算法

在 MT5 自定义表格绘制里,第一列和后续列的文字横向坐标 x 不能简单累加列宽,必须按每列设定的对齐方式(ALIGN_LEFT / ALIGN_CENTER / ALIGN_RIGHT)分别推算。第一列以 column_offset 为基准,中间对齐就加半个列宽、右对齐直接跳到列尾再减 cell_x_offset、左对齐则加 cell_x_offset 作为文字起点。 从第二列开始,偏移计算要参考前一列的对齐状态。比如前一列是中间对齐、当前列也是中间对齐,column_offset 就累加「前两列半宽之和」;若前一列左对齐,则要先补满前一列整宽,再叠当前列半宽。这种嵌套 switch 的逻辑,直接决定了你在 canvas 上 TextOut 时文字会不会错位重叠。 实盘面板若用 -80 像素高度字体(FONT 大小负值代表像素单位)配合 FW_NORMAL,渲染出的紧凑表格在 1920 宽屏上单列误差可能不到 2 像素;但一旦列数超过 8 且混用对齐,手工算错偏移就会让报价列整体右偏。建议把下面这段原样丢进 MT5 的 OnInit 后调试,改 m_vcolumns[].m_text_align 看 x 的变化。

MQL5 / C++
m_canvas.FontSet(FONT,-class="num">80,FW_NORMAL);
class=class="str">"cmt">//--- 列
for(class="type">int c=class="num">0;c<m_columns_total; c++)
  {
  class=class="str">"cmt">//--- 计算第一列的偏移
  if(c==class="num">0)
    {
    class=class="str">"cmt">//--- 基于每个列设置的单元中的文字对齐方式
    class="kw">switch(m_vcolumns[class="num">0].m_text_align)
      {
      class=class="str">"cmt">//--- 中间对齐
      case ALIGN_CENTER :
        column_offset=column_offset+m_vcolumns[class="num">0].m_width/class="num">2;
        x=column_offset;
        class="kw">break;
      class=class="str">"cmt">//--- 右对齐
      case ALIGN_RIGHT :
        column_offset=column_offset+m_vcolumns[class="num">0].m_width;
        x=column_offset-cell_x_offset;
        class="kw">break;
      class=class="str">"cmt">//--- 左对齐
      case ALIGN_LEFT :
        x=column_offset+cell_x_offset;
        class="kw">break;
      }
    }
  class=class="str">"cmt">//--- 计算除了第一列之外的其他列的偏移
  else
    {
    class=class="str">"cmt">//--- 基于每个列设置的单元中的文字对齐方式
    class="kw">switch(m_vcolumns[c].m_text_align)
      {
      class=class="str">"cmt">//--- 中间对齐
      case ALIGN_CENTER :
        class=class="str">"cmt">//--- 计算相对前一列的偏移
        class="kw">switch(m_vcolumns[c-class="num">1].m_text_align)
          {
          case ALIGN_CENTER :
            column_offset=column_offset+(m_vcolumns[c-class="num">1].m_width/class="num">2)+(m_vcolumns[c].m_width/class="num">2);
            class="kw">break;
          case ALIGN_RIGHT :
            column_offset=column_offset+(m_vcolumns[c].m_width/class="num">2);
            class="kw">break;
          case ALIGN_LEFT :
            column_offset=column_offset+m_vcolumns[c-class="num">1].m_width+(m_vcolumns[c].m_width/class="num">2);
            class="kw">break;
          }
      }
  }

「表格列右对齐与左对齐的偏移递推」

在自绘表格控件里,列的横向坐标 x 不是写死的,而是根据前一列的对齐方式和自身宽度递推算出来的。这段代码处理的是 ALIGN_RIGHT 与 ALIGN_LEFT 两种情形,核心变量是 column_offset(已累积的前列总偏移)和 cell_x_offset(单元格内边距)。 右对齐时若前一列居中,偏移要加「前半列宽 + 当前列宽」:column_offset=column_offset+(m_vcolumns[c-1].m_width/2)+m_vcolumns[c].m_width,再把 x 设为 column_offset-cell_x_offset,文本右缘贴齐列右边界。若前一列也是右对齐,则只加当前列宽;若前一列左对齐,则加「前列宽 + 当前列宽」。 左对齐分支逻辑相反:前一列居中时 column_offset 仅加半列宽,x=column_offset+cell_x_offset 让文本从左边界内缩;前一列右对齐时 x 直接等于 column_offset+cell_x_offset,不额外推进偏移;前一列左对齐时代码在此截断,实际需补上 column_offset=column_offset+m_vcolumns[c-1].m_width 才会连续。 开 MT5 把这段塞进 CTable 类的列布局函数,打印三列(居中/右/左,宽各 100/80/120)的 x 值,应能验证第二列 x 落在约 190-cell_x_offset、第三列落在约 270+cell_x_offset 附近。外汇与贵金属图表叠加自绘控件时滑点敏感,参数改动后务必在 tick 回放里看重绘是否跳变。

MQL5 / C++
class=class="str">"cmt">//---
   x=column_offset;
   class="kw">break;
   class=class="str">"cmt">//--- 右对齐
   case ALIGN_RIGHT :
      class=class="str">"cmt">//--- 计算相对前一列的偏移
      class="kw">switch(m_vcolumns[c-class="num">1].m_text_align)
      {
         case ALIGN_CENTER :
            column_offset=column_offset+(m_vcolumns[c-class="num">1].m_width/class="num">2)+m_vcolumns[c].m_width;
            x=column_offset-cell_x_offset;
            class="kw">break;
         case ALIGN_RIGHT :
            column_offset=column_offset+m_vcolumns[c].m_width;
            x=column_offset-cell_x_offset;
            class="kw">break;
         case ALIGN_LEFT :
            column_offset=column_offset+m_vcolumns[c-class="num">1].m_width+m_vcolumns[c].m_width;
            x=column_offset-cell_x_offset;
            class="kw">break;
      }
      class=class="str">"cmt">//---
      class="kw">break;
   class=class="str">"cmt">//--- 左对齐
   case ALIGN_LEFT :
      class=class="str">"cmt">//--- 计算相对前一列的偏移
      class="kw">switch(m_vcolumns[c-class="num">1].m_text_align)
      {
         case ALIGN_CENTER :
            column_offset=column_offset+(m_vcolumns[c-class="num">1].m_width/class="num">2);
            x=column_offset+cell_x_offset;
            class="kw">break;
         case ALIGN_RIGHT :
            x=column_offset+cell_x_offset;
            class="kw">break;
         case ALIGN_LEFT :

◍ 用滚动条偏移量驱动表格重绘

表格在画布上的实际位置,并不靠重画像素实现,而是借 OBJPROP_XOFFSET / YOFFSET 两个整数偏移把整块对象推走。上面这段逻辑先按列宽累加 column_offset,再用 cell_x_offset 算出单元格 x 基准,命中列后直接 break 退出内层循环,避免无谓遍历。 行方向用 y += (r>0) ? m_cell_y_size-1 : cell_y_offset 控制纵向步进:首行用 cell_y_offset 留白,其余行只补一个像素重叠量,保证行间不裂缝。文字对齐在循环里靠 m_text_align 分支切成 TA_CENTER / TA_RIGHT / TA_LEFT 并统一顶对齐 TA_TOP,最后 m_canvas.TextOut(x,y,...) 落字。 收尾把可见区域尺寸写回对象:OBJPROP_XSIZE 接 m_table_visible_x_size,OBJPROP_YSIZE 接 m_table_visible_y_size,偏移归零。ShiftTable() 则是另一套驱动——读 m_scrollh.CurrentPos() 和 m_scrollv.CurrentPos(),水平偏移直接取滑块位置 h,垂直偏移用 v*(m_cell_y_size-1),把长表相对滚动条滑动起来。 在 MT5 里建个 CCanvasTable 派生类,挂两根滚动条,把 cell_y_size 设成 20 试 ShiftTable:滑块拉到底时 YOFFSET 会等于 (总行数-1)*19,表格底边恰好露头,可据此反推最大可见行数。

MQL5 / C++
      column_offset=column_offset+m_vcolumns[c-class="num">1].m_width;
      x=column_offset+cell_x_offset;
      class="kw">break;
    }
    class=class="str">"cmt">//---
    class="kw">break;
    }
  }
class=class="str">"cmt">//--- 行
for(class="type">int r=class="num">0;r<m_rows_total;r++)
  {
  class=class="str">"cmt">//---
  y+=(r>class="num">0) ? m_cell_y_size-class="num">1 : cell_y_offset;
  class=class="str">"cmt">//---
  class="kw">switch(m_vcolumns[c].m_text_align)
    {
    case ALIGN_CENTER :
      text_align=TA_CENTER|TA_TOP;
      class="kw">break;
    case ALIGN_RIGHT :
      text_align=TA_RIGHT|TA_TOP;
      class="kw">break;
    case ALIGN_LEFT :
      text_align=TA_LEFT|TA_TOP;
      class="kw">break;
    }
  class=class="str">"cmt">//--- 绘制文字
  m_canvas.TextOut(x,y,m_vcolumns[c].m_vrows[r],clr,text_align);
  }
class=class="str">"cmt">//--- 为下一次循环清零Y坐标
   y=class="num">0;
  }
}
class=class="str">"cmt">//--- 设置可见区域的大小
   ::ObjectSetInteger(m_chart_id,name,OBJPROP_XSIZE,m_table_visible_x_size);
   ::ObjectSetInteger(m_chart_id,name,OBJPROP_YSIZE,m_table_visible_y_size);
class=class="str">"cmt">//--- 设置边界在图形中X轴和Y轴上的偏移
   ::ObjectSetInteger(m_chart_id,name,OBJPROP_XOFFSET,class="num">0);
   ::ObjectSetInteger(m_chart_id,name,OBJPROP_YOFFSET,class="num">0);
class CCanvasTable : class="kw">public CElement
  {
class="kw">public:
  class=class="str">"cmt">//--- 相对滚动条位置移动表格
  class="type">void             ShiftTable(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 相对滚动条位置移动表格                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasTable::ShiftTable(class="type">void)
  {
class=class="str">"cmt">//--- 取得垂直和水平滚动条滑块的当前位置
  class="type">int h=m_scrollh.CurrentPos();
  class="type">int v=m_scrollv.CurrentPos();
class=class="str">"cmt">//--- 计算表格相对滚动条滑块的位置
  class="type">long c=h;
  class="type">long r=v*(m_cell_y_size-class="num">1);
class=class="str">"cmt">//--- 移动表格
  ::ObjectSetInteger(m_chart_id,m_canvas.Name(),OBJPROP_XOFFSET,c);
  ::ObjectSetInteger(m_chart_id,m_canvas.Name(),OBJPROP_YOFFSET,r);
  }

表格重绘时的透明背景与偏移处理

在自定义图表控件里,CCanvasTable 继承自 CElement,对外暴露 DrawTable() 负责按最新状态重画整个表格。这个方法没有参数,纯粹依据内部缓存的数据和滚动位置刷新画布。 方法开头先用 m_canvas.Erase(ColorToARGB(clrNONE,0)) 把画布擦成完全透明(alpha 为 0),避免上一次绘制的残影叠在当前帧上。接着依次调用 DrawGrid() 画网格、DrawText() 写文字,再用 m_canvas.Update() 把改动推到屏幕,最后 ShiftTable() 按滚动条偏移表格内容。 你在 MT5 里写自己的 canvas 表格类时,可以直接复用这个调用顺序:先擦成透明、再分层绘制、最后更新并偏移。外汇与贵金属行情跳动快,这类控件重绘频率高,透明背景处理不当会肉眼可见地闪。

MQL5 / C++
class CCanvasTable : class="kw">public CElement
  {
class="kw">public:
   class=class="str">"cmt">//--- 考虑到最近的变化而绘制表格
   class="type">void              DrawTable(class="type">void);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 绘制表格                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvasTable::DrawTable(class="type">void)
  {
class=class="str">"cmt">//--- 使背景透明
   m_canvas.Erase(::ColorToARGB(clrNONE,class="num">0));
class=class="str">"cmt">//--- 画网格
   DrawGrid();
class=class="str">"cmt">//--- 画文字
   DrawText();
class=class="str">"cmt">//--- 显示最近的改变
   m_canvas.Update();
class=class="str">"cmt">//--- 相对滚动条偏移表格
   ShiftTable();
   }

「用 CCanvasTable 搭一个千行表格」

把之前 EA 里和 CTable 相关的元件全部删掉,只在 CProgram 里留一个 CCanvasTable 实例,再补两个方法:CreateCanvasTable() 负责建表,另一定义表单边距(横向 1 像素、纵向 42 像素)。这样界面层就只依赖画布绘制,不再走标准表格控件。 建表时先定规格:15 列、1000 行,可见行数锁在 16 行;横向不按列数而按像素偏移,所以可见区宽度显式写成 601 像素。列宽数组默认 70,第 0、1 列分别覆盖为 100 和 90;对齐方式默认居中,第 0、2 列右对齐,第 1 列左对齐。 下面这段是 CreateCanvasTable() 的完整实现,编译后挂到主面板创建流程里即可在图表看到千行滚动表。外汇与贵金属图表加载此类大表有重绘开销,高频刷新时可能掉帧,建议先在模拟环境验证。 脚本里也能用:把同样代码搬进 CProgram 自定义类,并在 OnEvent() 里每 250 毫秒改一次第 1 列数据,就能看到不带头滚动条的实时绘表格。这类无滚动条形态适合做轻量监控板。

MQL5 / C++
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
  class=class="str">"cmt">//--- 绘制型表格
  CCanvasTable        m_canvas_table;
  class=class="str">"cmt">//---
class="kw">private:
  class=class="str">"cmt">//--- 绘制型表格
class="macro">#define TABLE1_GAP_X(class="num">1)
class="macro">#define TABLE1_GAP_Y(class="num">42)
  class="type">bool                CreateCanvasTable(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建绘制型表格                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateCanvasTable(class="type">void)
  {
class="macro">#define COLUMNS1_TOTAL class="num">15
class="macro">#define ROWS1_TOTAL    class="num">1000
class=class="str">"cmt">//--- 保存表单的指针
  m_canvas_table.WindowPointer(m_window1);
class=class="str">"cmt">//--- 坐标
  class="type">int x=m_window1.X()+TABLE1_GAP_X;
  class="type">int y=m_window1.Y()+TABLE1_GAP_Y;
class=class="str">"cmt">//--- 可见的行数
  class="type">int visible_rows_total=class="num">16;
class=class="str">"cmt">//--- 列宽度数组
  class="type">int width[COLUMNS1_TOTAL];
  ::ArrayInitialize(width,class="num">70);
  width[class="num">0]=class="num">100;
  width[class="num">1]=class="num">90;
class=class="str">"cmt">//--- 列文字对齐方式数组
  ENUM_ALIGN_MODE align[COLUMNS1_TOTAL];
  ::ArrayInitialize(align,ALIGN_CENTER);
  align[class="num">0]=ALIGN_RIGHT;
  align[class="num">1]=ALIGN_LEFT;
  align[class="num">2]=ALIGN_RIGHT;
class=class="str">"cmt">//--- 在创建之前设置属性
  m_canvas_table.XSize(class="num">601);
  m_canvas_table.TableSize(COLUMNS1_TOTAL,ROWS1_TOTAL);
  m_canvas_table.VisibleTableSize(class="num">0,visible_rows_total);
  m_canvas_table.TextAlign(align);
  m_canvas_table.ColumnsWidth(width);
  m_canvas_table.GridColor(clrLightGray);
class=class="str">"cmt">//--- 在表格中填入数据
  for(class="type">int c=class="num">0; c<COLUMNS1_TOTAL; c++)
    for(class="type">int r=class="num">0; r<ROWS1_TOTAL; r++)
       m_canvas_table.SetValue(c,r,class="type">class="kw">string(c)+":"+class="type">class="kw">string(r));
class=class="str">"cmt">//--- 创建控件
  if(!m_canvas_table.CreateTable(m_chart_id,m_subwin,x,y))
     class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 把对象加到对象组的总的数组中
  CWndContainer::AddToElementsArray(class="num">0,m_canvas_table);
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建交易面板                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateExpertPanel(class="type">void)
  {
class=class="str">"cmt">//--- 创建控件表单1
class=class="str">"cmt">//--- 创建控件:
class=class="str">"cmt">//    主菜单
class=class="str">"cmt">//--- 上下文菜单
class=class="str">"cmt">//--- 创建状态行
class=class="str">"cmt">//--- 创建绘制型表格

◍ 用定时器刷新画布表格的实战写法

在 MT5 自定义指标或脚本里,想让面板上的数据动起来,靠的是 OnEvent 这种周期性回调。上面这段把表头做成「脚本面板 / 脚本面板 . / .. / ...」的循环动画,本质就是靠一个 static 计数器在 0~3 之间打转,每帧换一次标题文案。 第一列 13 行数据被 ::rand() 随机重写,再调 DrawTable() 重绘,Redraw() 推到图表。这里有个细节:循环写死了 r<13,说明表格第一列至少有 13 行,改行数时两处要同步,否则会越界或留空行。 别把正态当圣经:rand() 出的是伪随机,回测里看着在跳,实盘多开几个实例可能撞序列。真要模拟行情跳动,建议用 SymbolInfoTick 抓真实卖一买一价差填表。 让小布替你跑这套:把 milliseconds 传 200,面板大约 5 帧/秒,CPU 占用低;设 50 以下容易把图表卡顿,EURUSD 或 XAUUSD 这种高波动品种上验证时尤其明显,外汇贵金属杠杆高、滑点大,仅作界面压力测试用。

MQL5 / C++
  if(!CreateCanvasTable())
        class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 重绘图表
  m_chart.Redraw();
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::OnEvent(const class="type">int milliseconds)
  {
  class="kw">static class="type">int count =class="num">0;   class=class="str">"cmt">// 计数器
  class="type">class="kw">string    str  =""; class=class="str">"cmt">// 表头行
class=class="str">"cmt">//--- 从表头开始显示过程
  class="kw">switch(count)
    {
    case class="num">0 : str="脚本面板";      class="kw">break;
    case class="num">1 : str="脚本面板 .";    class="kw">break;
    case class="num">2 : str="脚本面板 ..";   class="kw">break;
    case class="num">3 : str="脚本面板 ..."; class="kw">break;
    }
class=class="str">"cmt">//--- 更新表头行
  m_window.CaptionText(str);
class=class="str">"cmt">//--- 改变第一列数据
  for(class="type">int r=class="num">0; r<class="num">13; r++)
    m_canvas_table.SetValue(class="num">1,r,class="type">class="kw">string(::rand()));
class=class="str">"cmt">//--- 显示新的表格数据
  m_canvas_table.DrawTable();
class=class="str">"cmt">//--- 重绘图表
  m_chart.Redraw();
class=class="str">"cmt">//--- 增加计数器
  count++;
class=class="str">"cmt">//--- 如果超过3就再设为0
  if(count>class="num">3)
    count=class="num">0;
class=class="str">"cmt">//--- 暂停
  ::Sleep(milliseconds);
  }

别急着下结论

CTable 和 CCanvasTable 这两类表格封装各有侧重:前者带编辑框、可改格式,交互上更贴近用户;后者绕开单元格字符数限制,靠图形可见范围平移,还能逐列设宽。两者都还不是终点,按需求继续改完全可行。 要真在 MT5 里跑通,直接下 easyandfastgui_mql5.zip(313.88 KB)解压进 Indicators/Include 试,MT4 下同名库 313.25 KB 也能用,表格逻辑两端一致。 下一篇会落到页面控件类,那是另一块常用 GUI 拼图。现在先把这两类表格的单元格限制和列宽行为在实盘图表里点开看一眼,比看结论有用。

常见问题

把表头行和首列单独做成锁定层,绘制时优先渲染并屏蔽其滚动偏移量,其余单元格按视口裁剪即可。
用定时器按固定间隔(如每秒一次)重绘,别在每次价格跳动都全量刷新,能明显降低闪烁与卡顿。
可以,小布能接管面板的定时读取,发现越界或突变单元格直接推送提醒,你不用自己一直盯着看。
编辑框型支持双击改值、回车提交、输入校验和失焦回调,标签型只能静态展示不能交互录入。
私有状态暴露后易被外部误改导致错行,应只留公共接口做增删改查,内部坐标与选中态封装起来。