制作仪表板以显示指标和EA中的数据·进阶篇
📊

制作仪表板以显示指标和EA中的数据·进阶篇

(2/3)· 想在图表上直观调试策略却嫌调试器太绕?用可拖拽可折叠的面板把数据摆出来

实战向进阶 第 2/3 篇
不少交易者把EA数值塞进调试器一行行盯,效率低还容易漏掉突变。面板化展示不是锦上添花,而是把隐性状态变成肉眼可扫的常态。外汇贵金属波动快、杠杆高,错看一个读数可能就偏了入场节奏。

◍ 表格行与单元格的插入防重逻辑

在自定义表格类里,往行对象塞单元格之前必须先查重。下面这段用 m_list_cell.Search(cell) 比对已存单元,若返回值不等于 WRONG_VALUE 就说明同列单元已在列表中,直接打印函数名和列号并 return false,避免重复插入导致后续绘图索引错乱。 插入动作本身走 InsertSort(cell) 做有序插入,失败同样回 false 并带出列号日志;只有两步都过才 return true。外汇与贵金属图表上跑这类自定义控件时,对象泄漏或重复创建可能拖慢 MT5 终端,建议直接抄这段防重结构。 取单元格的 GetCell 写法也值得注意:它用 new CTableCell(this.m_row, column) 造一个临时对象去 Search,拿到 index 后立刻 delete obj,再返回 m_list_cell.At(index)。这种「临时对象查索引 + 即删」的手法能把堆上短命对象控制在函数作用域内,回测挂多表时内存波动倾向更平稳。

MQL5 / C++
if(this.m_list_cell.Search(cell)!=WRONG_VALUE)
            {
             ::PrintFormat("%s: Table cell with index %lu is already in the list",__FUNCTION__,cell.Column());
             class="kw">return false;
            }
            if(!this.m_list_cell.InsertSort(cell))
            {
             ::PrintFormat("%s: Failed to add table cell with index %lu to list",__FUNCTION__,cell.Column());
             class="kw">return false;
            }
            class="kw">return true;
class=class="str">"cmt">//--- Return the pointer to the specified cell in the row
   CTableCell      *GetCell(const class="type">int column)
          {
           const CTableCell *obj=new CTableCell(this.m_row,column);
           class="type">int index=this.m_list_cell.Search(obj);
           class="kw">delete obj;
           class="kw">return this.m_list_cell.At(index);
          }

「表格行与单元格的增查逻辑」

在自建表格类里,AddRow 负责把一行塞进内部有序链表,并顺手做去重和插入校验。先调用 Sort() 保证链表有序,再用 Search() 查重——若返回的索引不是 WRONG_VALUE(即 -1),说明这行已存在,往日志打一句带行号的提示后直接 return false。 插入阶段走 InsertSort(row),失败同样打印含行索引的报错并返回 false;两步都过才返回 true。实测中若重复添加同一 CTableRow 指针,日志会出现类似 AddRow: Table row with index 3 is already in the list 的输出,可作为调试表格渲染异常的锚点。 取数侧两个方法很轻量:GetRow(int index) 直接返回 m_list_rows.At(index) 的指针;GetCell(row,column) 先拿行对象,拿不到就返回 NULL,后续由调用方判空。写 EA 面板时,建议对 GetCell 返回值做 NULL 保护,否则贵金属跳空行情下界面刷新可能抛野指针。

MQL5 / C++
class="type">bool AddRow(CTableRow *row)
 {
 class=class="str">"cmt">//--- Set the sorted list flag
 this.m_list_rows.Sort();
 class=class="str">"cmt">//--- If such an object is already in the list(the search returns the object index, not -class="num">1),
 class=class="str">"cmt">//--- inform of that in the journal and class="kw">return &class="macro">#x27;false&class="macro">#x27;
 if(this.m_list_rows.Search(row)!=WRONG_VALUE)
   {
    ::PrintFormat("%s: Table row with index %lu is already in the list",__FUNCTION__,row.Row());
    class="kw">return false;
   }
 class=class="str">"cmt">//--- If failed to add the pointer to the sorted list, inform of that and class="kw">return &class="macro">#x27;false&class="macro">#x27;
 if(!this.m_list_rows.InsertSort(row))
   {
    ::PrintFormat("%s: Failed to add table cell with index %lu to list",__FUNCTION__,row.Row());
    class="kw">return false;
   }
 class=class="str">"cmt">//--- Successful - class="kw">return &class="macro">#x27;true&class="macro">#x27;
 class="kw">return true;
  }
class=class="str">"cmt">//--- Return the pointer to the(class="num">1) specified row and(class="num">2) specified cell in the specified table row
  CTableRow      *GetRow(const class="type">int index) { class="kw">return this.m_list_rows.At(index);  }
  CTableCell     *GetCell(const class="type">int row,const class="type">int column)
   {
   class=class="str">"cmt">//--- Get a pointer to a class="type">class="kw">string object in a list of strings
   CTableRow *row_obj=this.GetRow(row);
   class=class="str">"cmt">//--- If failed to get the object, class="kw">return NULL
   if(row_obj==NULL)

表格单元格坐标与维度的取法

在自定义表格类里,取某个单元格对象不能直接下标访问,得先走 GetCell 拿到指针,空就返回 NULL,这是后续所有坐标读取的前置逻辑。 CellXY 方法把 x、y 先预置为 WRONG_VALUE(通常是 -1),再尝试取 cell;若指针为空直接 return,调用方读到 -1 就能判断该行列越界或尚未绘制。 CellX 与 CellY 是精简版:用三元运算在 cell 非空时返回对应坐标,否则回 WRONG_VALUE。MT5 里表格若未挂载到图表,坐标大概率为 -1,开终端拖一个 CTable 实例就能验证。 行数通过 m_list_rows.Total() 取得,列数原文未在此贴出但同思路走列容器。外汇与贵金属图表上叠加这类 GUI 存在重绘开销,高频 tick 下可能拖慢 EA,属高风险自定义控件用法。

MQL5 / C++
      .class="kw">return NULL;
      class=class="str">"cmt">//--- Get the pointer to the cell object in the row by a column number and
      CTableCell *cell=row_obj.GetCell(column);
      class=class="str">"cmt">//--- class="kw">return the result(object pointer or NULL)
      class="kw">return cell;
       }
class=class="str">"cmt">//--- Write the X and Y coordinates of the specified table cell into the variables passed to the method
   class="type">void        CellXY(const class="type">uint row,const class="type">uint column, class="type">int &x, class="type">int &y)
     {
      x=WRONG_VALUE;
      y=WRONG_VALUE;
      CTableCell *cell=this.GetCell(row,column);
      if(cell==NULL)
        class="kw">return;
      x=cell.X();
      y=cell.Y();
     }
class=class="str">"cmt">//--- Return the X coordinate of the specified table cell
   class="type">int        CellX(const class="type">uint row,const class="type">uint column)
     {
      CTableCell *cell=this.GetCell(row,column);
      class="kw">return(cell!=NULL ? cell.X() : WRONG_VALUE);
     }
class=class="str">"cmt">//--- Return the Y coordinate of the specified table cell
   class="type">int        CellY(const class="type">uint row,const class="type">uint column)
     {
      CTableCell *cell=this.GetCell(row,column);
      class="kw">return(cell!=NULL ? cell.Y() : WRONG_VALUE);
     }
class=class="str">"cmt">//--- Return the number of table(class="num">1) rows and(class="num">2) columns
   class="type">int        RowsTotal(class="type">void)        { class="kw">return this.m_list_rows.Total();  }

◍ 表格行列与清空的底层实现

在 MT5 自定义表格类里,列数不是独立存储的,而是借第一行来推算。ColumnsTotal() 先判断 RowsTotal() 是否为 0,若没有行直接返回 0;否则取第 0 行指针,返回该行的 CellsTotal()。这意味着只要首行结构正常,整表列数就跟着首行走。 CellsTotal() 更粗暴,直接拿 RowsTotal() 乘 ColumnsTotal() 得出单元格总量。假设某表有 12 行、每行 8 列,该函数会返回 96——你可以用 Print() 打出来核对,避免界面渲染和实际存储对不上。 Clear() 的写法值得抄:它不靠析构偷懒,而是先遍历 m_list_rows,对每一行取 GetListCell() 拿单元格列表逐个 Clear(),最后才清 m_list_rows。这样能确定性释放嵌套对象,在 EA 反复重建报价面板时不容易留野指针。 别把首行当恒成立 若你动态删过首行却没重排,ColumnsTotal() 可能返回后续行的列数甚至 0,界面列宽计算会瞬间错乱。开 MT5 在 OnDeinit 前手动调一次 Clear() 验证释放是否干净。

MQL5 / C++
class="type">int ColumnsTotal(class="type">void)
  {
  class=class="str">"cmt">//--- If there is no row in the list, class="kw">return class="num">0
  if(this.RowsTotal()==class="num">0)
     class="kw">return class="num">0;
  class=class="str">"cmt">//--- Get a pointer to the first row and class="kw">return the number of cells in it
  CTableRow *row=this.GetRow(class="num">0);
  class="kw">return(row!=NULL ? row.CellsTotal() : class="num">0);
  }
class=class="str">"cmt">//--- Return the total number of cells in the table
class="type">int CellsTotal(class="type">void){ class="kw">return this.RowsTotal()*this.ColumnsTotal(); }
class=class="str">"cmt">//--- Clear lists of rows and table cells
class="type">void Clear(class="type">void)
  {
  class=class="str">"cmt">//--- In the loop by the number of rows in the list of table rows, 
  for(class="type">int i=class="num">0;i<this.m_list_rows.Total();i++)
    {
    class=class="str">"cmt">//--- get the pointer to the next row
    CTableRow *row=this.m_list_rows.At(i);
    if(row==NULL)
       class="kw">continue;
    class=class="str">"cmt">//--- get cell list from the obtained row object,
    CArrayObj *list_cell=row.GetListCell();
    class=class="str">"cmt">//--- clear cell list
    if(list_cell!=NULL)
       list_cell.Clear();
    }
  class=class="str">"cmt">//--- Clear cell list
  this.m_list_rows.Clear();

「把表格数据打到日志里看结构」

在 MT5 里做自定义表格类时,最忌讳盲写——你以为行数列数对上了,实际某行漏了单元格。下面这段 Print 方法就是用来把 CTableData 的内部行列结构原样吐到专家日志里的,跑一遍就知道哪行哪列有数据、坐标 X/Y 是多少。 Print 先打表头,用 PrintFormat 输出 RowsTotal() 和 ColumnsTotal() 两个值,例如一张 3 行 5 列的表会显示 Table: Rows: 3, Columns: 5。随后双层 for 循环按 r、c 遍历,GetCell(r,c) 拿到单元格指针,非空才打印该行该列的 Row、Column 序号及 Cell X / Cell Y 坐标。 构造函数和析构函数都只做一件事:m_list_rows.Clear() 清空行链表,避免对象复用或销毁时残留上一轮的脏数据。RowsTotal() 直接返回 m_list_rows.Total(),ColumnsTotal() 则先判断 RowsTotal()==0 就直接返 0——空表去查列数会踩坑,这个守卫不能省。 开 MT5 新建 EA 把这段塞进你的表格类,调用 Print() 后切到「专家」标签页,对照日志里的坐标就能确认渲染层画的格子跟逻辑层是不是同一套。外汇与贵金属行情跳动快,这类结构校验最好在模拟盘跑,实盘误用可能放大下单逻辑偏差。

MQL5 / C++
   class=class="str">"cmt">//--- Print the table cell data in the journal
   class="type">void       Print(const class="type">uint indent=class="num">0)
      {
      class=class="str">"cmt">//--- Print the header in the journal
      ::PrintFormat("Table: Rows: %lu, Columns: %lu",this.RowsTotal(),this.ColumnsTotal());
      class=class="str">"cmt">//--- In the loop by table rows
      for(class="type">int r=class="num">0;r<this.RowsTotal();r++)
         class=class="str">"cmt">//--- in the loop by the next row cells,
         for(class="type">int c=class="num">0;c<this.ColumnsTotal();c++)
            {
            class=class="str">"cmt">//--- get the pointer to the next cell and display its data in the journal
            CTableCell *cell=this.GetCell(r,c);
            if(cell!=NULL)
               ::PrintFormat("%*s%-5s %-4lu %-8s %-6lu %-8s %-6lu %-8s %-4lu",indent,"","Row",r,"Column",c,"Cell X:",cell.X(),"Cell Y:",cell.Y());
            }
      }
class=class="str">"cmt">//--- Constructor/destructor
            CTableData(class="type">void)   { this.m_list_rows.Clear();   }
           ~CTableData(class="type">void)   { this.m_list_rows.Clear();   }
   };
    class="type">int       RowsTotal(class="type">void)                { class="kw">return this.m_list_rows.Total();   }
    class="type">int       ColumnsTotal(class="type">void)
      {
      class=class="str">"cmt">//--- If there is no row in the list, class="kw">return class="num">0
      if(this.RowsTotal()==class="num">0)

表格单元格总数的两种取法

在自绘 CTable 控件里,想拿总行数下的单元格数量,有两种写法。一种是直接拿第一行指针,若行存在就返回该行的列数,否则返回 0;另一种更粗暴,用行数乘以列数一次性算总数。 后一种 CellsTotal 写法 return this.RowsTotal()*this.ColumnsTotal(); 假设行列规整,若某行缺列会高估。前一种只认第一行,若后续行不等宽会低估。 在 MT5 里接这段逻辑时,先确认你的表是定宽定高再选乘法,否则遍历每行累加更稳。外汇与贵金属图表上挂这类面板,注意高频刷新下的内存抖动风险。

MQL5 / C++
              class="kw">return class="num">0;
              class=class="str">"cmt">//--- Get a pointer to the first row and class="kw">return the number of cells in it
              CTableRow *row=this.GetRow(class="num">0);
              class="kw">return(row!=NULL ? row.CellsTotal() : class="num">0);
             }
   class="type">int       CellsTotal(class="type">void){ class="kw">return this.RowsTotal()*this.ColumnsTotal(); }
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到指标与EA读数被整理成浮层面板,你只管判断信号强弱。

常见问题

数据窗口是终端固定原型,不能按策略需要增删字段;自定义面板可用表格类自由排布任意行列表格,并随EA事件刷新,调试路径更短。
因为MQL5标准库CArrayObj列表只能容纳CObject及其派生对象,继承后单元格才能被行、表以对象数组统一管理并实现Compare查找。
小布盯盘内置的AIGC看板可呈现同类指标与EA摘要,若你导出自绘面板坐标数据,也能对照品种页浮层做交叉验证,省去手绘代码。
只有开发者清楚按下关闭后该暂停EA还是仅隐藏面板,所以类只发送自定义事件,具体响应写在EA或指标逻辑里更稳妥。
表格类维护行列表与相对坐标,仪表板类在外层记录面板原点并捕获鼠标消息,据此重绘或隐藏表格区域,实现移动与折叠。