MQL5中表格模型的实现:应用MVC概念·综合运用
📘

MQL5中表格模型的实现:应用MVC概念·综合运用

第 3/3 篇
本章目录
  1. 图形对象绑定的空指针与类型登记
  2. 可变参数的类型分支与清空逻辑
  3. 表格单元格类的构造重载细节
  4. 表格单元格的多类型构造与赋值细节
  5. 表格单元格的多类型构造函数落地
  6. 单元格比较与落盘的分支写法
  7. 表格单元落盘与回读的校验链
  8. 单元格描述与表格行类的内部封装
  9. CTableRow 的单元格操作接口
  10. 表格行对象的持久化与类型识别
  11. 表格行类的链表内核与单元格管控
  12. 行对象里按类型造单元格的三个重载
  13. 给表格行追加单元格与写值的底层实现
  14. 表格行里单元格的增删与对象绑定
  15. 表格行对象的单元格同步与日志输出
  16. 行对象的日志输出与文件落盘
  17. 从文件还原一行表格对象的校验链
  18. 用链表搭一个可换数据类型的虚拟表
  19. 按类型名映射底层存储枚举
  20. 表格控件的单元格与行操作接口
  21. CTableModel 的方法接口与多类型构造
  22. 二维数组灌进表格模型的底层写法
  23. 表格模型的行插入与单元格赋值逻辑
  24. 表格单元格的精度与对象绑定接口
  25. 表格模型里单元格的增删与统计实现
  26. 表格模型里行操作的索引同步坑
  27. 表格模型里的列与行数据操作
  28. 把表格模型的状态打到日志里
  29. 表格模型的销毁与文件存取实现
  30. 4x4 表格模型跑通后的日志长什么样
  31. 用 CTableModel 落地表格的存读与改结构
  32. 表格模型的增删列与还原
  33. 往表格模型里插行并锁格
  34. CTable 行列改完再读回原结构
  35. 在表格控件里读写第三行数值
  36. 二维数组表格模型先落地

图形对象绑定的空指针与类型登记

在 MT5 自定义控件类里,把图表上的图形对象(CObject 派生)挂到内部成员时,第一道关卡必须是空指针判断。若传入 object==NULL,直接 PrintFormat 打出函数名并 return,避免后续对 m_object 解引用导致崩端。 AssignObject 里除了存指针,还顺手把 object.Type() 强转成 ENUM_OBJECT_TYPE 写进 m_object_type。这一步相当于给控件做类型登记,后面按对象类别走分支逻辑时才不会认错人。 UnassignObject 则是反向操作:m_object 置 NULL、m_object_type 写 -1。注意 -1 不在标准枚举范围内,用作「未绑定」哨兵值比随便填个 OBJ_VLINE 更干净。 SetValue 做了三个重载:double 走 TYPE_DOUBLE 并调 SetValueD,long 走 TYPE_LONG 调 SetValueL,datetime 同理。每个重载都先写 m_datatype 再判断 m_editable——只有可编辑状态才真正落值,只读控件调用也只是改个类型标记。 开 MT5 自建个 CChartObject 子类,把上面这段粘进类里,故意传 NULL 试一次,终端会精确打印出『函数名: Error. Empty object passed』,比裸崩好排多了。

MQL5 / C++
class="type">void AssignObject(CObject *object)
 {
  if(object==NULL)
   {
    ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
    class="kw">return;
   }
  this.m_object=object;
  this.m_object_type=(ENUM_OBJECT_TYPE)object.Type();
 }
class=class="str">"cmt">//--- Remove the object assignment
 class="type">void UnassignObject(class="type">void)
  {
   this.m_object=NULL;
   this.m_object_type=-class="num">1;
  }

class=class="str">"cmt">//--- Set class="type">class="kw">double value
 class="type">void SetValue(const class="type">class="kw">double value)
  {
   this.m_datatype=TYPE_DOUBLE;
   if(this.m_editable)
    this.m_datatype_value.SetValueD(value);
  }
class=class="str">"cmt">//--- Set class="type">long value
 class="type">void SetValue(const class="type">long value)
  {
   this.m_datatype=TYPE_LONG;
   if(this.m_editable)
    this.m_datatype_value.SetValueL(value);
  }
class=class="str">"cmt">//--- Set class="type">class="kw">datetime value
 class="type">void SetValue(const class="type">class="kw">datetime value)
  {

「可变参数的类型分支与清空逻辑」

在 MT5 的自定义参数类里,SetValue 被重载成多份:传入 datetime 就切到 TYPE_DATETIME,传入 color 落到 TYPE_COLOR,传入 string 则标记 TYPE_STRING。每类赋值前都先判断 m_editable,只有可编辑状态才真正写入 m_datatype_value,这让面板锁定的参数在运行期不会被误改。 ClearData 的回收很直接:若当前是 TYPE_STRING 就塞空串 "",否则统一写 0.0。这意味着数值类字段清空后并非 NULL 而是浮点零,回测或实盘读取时要先判类型再决定比较基准,否则可能把“已清空”误读成有效报价 0.0。 类里还留了 Description、Print 以及 Compare / Save / Load 几个虚方法声明。Compare 带 mode 参数默认 0,Save 和 Load 都吃文件句柄,说明这套结构本就打算做持久化与集合排序——你复制去写指标时,直接补这几个方法体就能落库,不用再改上层调用。

MQL5 / C++
this.m_datatype=TYPE_DATETIME;
if(this.m_editable)
   this.m_datatype_value.SetValueL(value);
}
class=class="str">"cmt">//--- Set class="type">color value
class="type">void SetValue(const class="type">color value)
   {
   this.m_datatype=TYPE_COLOR;
   if(this.m_editable)
      this.m_datatype_value.SetValueL(value);
   }
class=class="str">"cmt">//--- Set class="type">class="kw">string value
class="type">void SetValue(const class="type">class="kw">string value)
   {
   this.m_datatype=TYPE_STRING;
   if(this.m_editable)
      this.m_datatype_value.SetValueS(value);
   }
class=class="str">"cmt">//--- Clear data
class="type">void ClearData(class="type">void)
   {
   if(this.Datatype()==TYPE_STRING)
      this.SetValue("");
   else
      this.SetValue(class="num">0.0);
   }
class=class="str">"cmt">//--- (class="num">1) Return and(class="num">2) display the object description in the journal
class="type">class="kw">string Description(class="type">void);
class="type">void Print(class="type">void);
class=class="str">"cmt">//--- Virtual methods of(class="num">1) comparing, (class="num">2) saving to file, (class="num">3) loading from file, (class="num">4) object type
class="kw">virtual class="type">int Compare(const CObject *node,const class="type">int mode=class="num">0) const;
class="kw">virtual class="type">bool Save(const class="type">int file_handle);
class="kw">virtual class="type">bool Load(const class="type">int file_handle);

◍ 表格单元格类的构造重载细节

在 MT5 的 GUI 表格封装里,CTableCell 用一组构造函数把行号、列号和具体数值类型绑死。默认无参构造会把 m_row、m_col 置 0,m_datatype 设 -1,并把内部联合值通过 SetValueD(0) 清零,此时单元格还不可对应任何真实列数据。 接受 double 的构造函数要求外部传入 digits 精度参数,例如黄金报价常取 2 位小数,EURUSD 多取 5 位;它把 m_datatype 标为 TYPE_DOUBLE 并调用 SetValueD(value) 存值。long 与 datetime 两个重载则不接收 digits:long 走 SetValueL,datetime 额外收一个 time_flags 控制显示格式(如只显时分或带日期)。 注意所有重载里 m_editable 默认 true、m_object 为 NULL、m_object_type 为 -1,意味着对象还没绑定到具体图形资源。你在写自定义面板时,若发现单元格不显示,先核对构造时 row/col 是否越界,以及有没有漏传 digits 导致 double 单元格渲染异常。

MQL5 / C++
class="kw">virtual class="type">int Type(class="type">void) const { class="kw">return(OBJECT_TYPE_TABLE_CELL);}

class=class="str">"cmt">//--- Constructors/destructor
                  CTableCell(class="type">void) : m_row(class="num">0), m_col(class="num">0), m_datatype(-class="num">1), m_digits(class="num">0), m_time_flags(class="num">0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-class="num">1)
                  {
                    this.m_datatype_value.SetValueD(class="num">0);
                  }
                  class=class="str">"cmt">//--- Accept a class="type">class="kw">double value
                  CTableCell(const class="type">uint row,const class="type">uint col,const class="type">class="kw">double value,const class="type">int digits) :
                    m_row((class="type">int)row), m_col((class="type">int)col), m_datatype(TYPE_DOUBLE), m_digits(digits), m_time_flags(class="num">0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-class="num">1)
                  {
                    this.m_datatype_value.SetValueD(value);
                  }
                  class=class="str">"cmt">//--- Accept a class="type">long value
                  CTableCell(const class="type">uint row,const class="type">uint col,const class="type">long value) :
                    m_row((class="type">int)row), m_col((class="type">int)col), m_datatype(TYPE_LONG), m_digits(class="num">0), m_time_flags(class="num">0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-class="num">1)
                  {
                    this.m_datatype_value.SetValueL(value);
                  }
                  class=class="str">"cmt">//--- Accept a class="type">class="kw">datetime value
                  CTableCell(const class="type">uint row,const class="type">uint col,const class="type">class="kw">datetime value,const class="type">uint time_flags) :
                    m_row((class="type">int)row), m_col((class="type">int)col), m_datatype(TYPE_DATETIME), m_digits(class="num">0), m_time_flags(time_flags), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-class="num">1)

表格单元格的多类型构造与赋值细节

在 MT5 自定义表格控件里,单元格对象 CTableCell 用重载构造函数区分数据类型:长整型、颜色、字符串各自走独立入口,并在初始化列表里写死 m_datatype 与默认标志位。 颜色构造接受 color_names_flag 参数,决定后续界面是否显示颜色名而非色值;字符串构造则强制 m_color_flag=false,避免类型错配。 赋双精度值时 SetValue 先改 m_datatype=TYPE_DOUBLE,再用 m_editable 闸门控制是否真正写入 m_datatype_value,这意味着只读单元格调 SetValue 不会覆盖原数据。 ClearData 对字符串类型直接 SetValue("") 清空,其余类型未在此段显式处理,开 MT5 把这段塞进 CTable 派生类即可观察单元格状态切换。

MQL5 / C++
            {
               this.m_datatype_value.SetValueL(value);
            }
            class=class="str">"cmt">//--- Accept class="type">color value
            CTableCell(const class="type">uint row,const class="type">uint col,const class="type">color value,const class="type">bool color_names_flag) :
               m_row((class="type">int)row), m_col((class="type">int)col), m_datatype(TYPE_COLOR), m_digits(class="num">0), m_time_flags(class="num">0), m_color_flag(color_names_flag), m_editable(true), m_object(NULL), m_object_type(-class="num">1)
            {
               this.m_datatype_value.SetValueL(value);
            }
            class=class="str">"cmt">//--- Accept class="type">class="kw">string value
            CTableCell(const class="type">uint row,const class="type">uint col,const class="type">class="kw">string value) :
               m_row((class="type">int)row), m_col((class="type">int)col), m_datatype(TYPE_STRING), m_digits(class="num">0), m_time_flags(class="num">0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-class="num">1)
            {
               this.m_datatype_value.SetValueS(value);
            }
            ~CTableCell(class="type">void) {}
};
class=class="str">"cmt">//--- Set class="type">class="kw">double value
   class="type">void            SetValue(const class="type">class="kw">double value)
            {
               this.m_datatype=TYPE_DOUBLE;
               if(this.m_editable)
                  this.m_datatype_value.SetValueD(value);
            }
class=class="str">"cmt">//--- Clear data
   class="type">void            ClearData(class="type">void)
            {
               if(this.Datatype()==TYPE_STRING)
                  this.SetValue("");

「表格单元格的多类型构造函数落地」

CTableCell 类用一组重载构造函数吞掉 MT5 里常见的六种数据形态:double、long、datetime、color、string,以及前面分支里赋 0.0 的兜底情况。double 版本多带一个 digits 参数控制小数位,datetime 版本用 time_flags 决定时间显示格式,color 版本靠 color_names_flag 切换色值或名称显示。 构造函数初始化列表里 m_editable 统一设 true、m_object_type 设 -1,说明单元格默认可编辑且尚未绑定具体图形对象;真正存值靠 m_datatype_value 的 SetValueD / SetValueL / SetValueS 分流。 Compare 方法开头直接把传入的 CObject 指针降维成 CTableCell 指针,意味着这套表格排序只认同类型节点,混插别的 CObject 派生类会在运行时踩到类型假设。开 MT5 把下面代码贴进自定义指标,能在日志里验证 double 与 string 单元格的存储分支是否如预期分离。

MQL5 / C++
else
               this.SetValue(class="num">0.0);
             }
class=class="str">"cmt">//--- Accept a class="type">class="kw">double value
CTableCell(const class="type">uint row,const class="type">uint col,const class="type">class="kw">double value,const class="type">int digits) :
   m_row((class="type">int)row), m_col((class="type">int)col), m_datatype(TYPE_DOUBLE), m_digits(digits), m_time_flags(class="num">0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-class="num">1)
   {
   this.m_datatype_value.SetValueD(value);
   }
class=class="str">"cmt">//--- Accept a class="type">long value
CTableCell(const class="type">uint row,const class="type">uint col,const class="type">long value) :
   m_row((class="type">int)row), m_col((class="type">int)col), m_datatype(TYPE_LONG), m_digits(class="num">0), m_time_flags(class="num">0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-class="num">1)
   {
   this.m_datatype_value.SetValueL(value);
   }
class=class="str">"cmt">//--- Accept a class="type">class="kw">datetime value
CTableCell(const class="type">uint row,const class="type">uint col,const class="type">class="kw">datetime value,const class="type">uint time_flags) :
   m_row((class="type">int)row), m_col((class="type">int)col), m_datatype(TYPE_DATETIME), m_digits(class="num">0), m_time_flags(time_flags), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-class="num">1)
   {
   this.m_datatype_value.SetValueL(value);
   }
class=class="str">"cmt">//--- Accept class="type">color value
CTableCell(const class="type">uint row,const class="type">uint col,const class="type">color value,const class="type">bool color_names_flag) :
   m_row((class="type">int)row), m_col((class="type">int)col), m_datatype(TYPE_COLOR), m_digits(class="num">0), m_time_flags(class="num">0), m_color_flag(color_names_flag), m_editable(true), m_object(NULL), m_object_type(-class="num">1)
   {
   this.m_datatype_value.SetValueL(value);
   }
class=class="str">"cmt">//--- Accept class="type">class="kw">string value
CTableCell(const class="type">uint row,const class="type">uint col,const class="type">class="kw">string value) :
   m_row((class="type">int)row), m_col((class="type">int)col), m_datatype(TYPE_STRING), m_digits(class="num">0), m_time_flags(class="num">0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-class="num">1)
   {
   this.m_datatype_value.SetValueS(value);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Compare two objects                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CTableCell::Compare(const CObject *node,const class="type">int mode=class="num">0) const
  {
   const CTableCell *obj=node;

◍ 单元格比较与落盘的分支写法

CTableCell 的比较逻辑用 switch 按 mode 分流:按列比就只比 Col(),按行比就只比 Row(),默认模式则先比行再比列,返回 1 / -1 / 0 三态。这种写法在表格排序里很实用,能避免同时比较带来的优先级混乱。 Save 方法把单元格写进文件时,先校验句柄是否为 INVALID_HANDLE,再写 8 字节起始标记 MARKER_START_DATA(值 0xFFFFFFFFFFFFFFFF),随后依次写对象类型、数据类型、单元格内对象类型、行号、列号,每一步都核对写入字节数是否等于预期长度,任一步失败立即返回 false。 别把写文件当无脑顺序操作 每一步 FileWriteInteger 都要求返回值等于 INT_VALUE(4 字节),在 MT5 实盘 EA 里若磁盘缓冲异常,可能某次只写了部分字节,这时候靠返回值拦截比事后校验文件结构更稳。 跑一遍就能验证:在 MT5 策略测试器里建个 CTableCell 实例,调 Save 写本地 tmp.bin,用 FileReadInteger 回读,行号列号对得上就说明这套序列化没坑。外汇与贵金属行情波动剧烈,任何本地缓存逻辑都要考虑中断风险。

MQL5 / C++
class="kw">switch(mode)
  {
  case CELL_COMPARE_MODE_COL :  class="kw">return(this.Col()>obj.Col() ? class="num">1 : this.Col()<obj.Col() ? -class="num">1 : class="num">0);
  case CELL_COMPARE_MODE_ROW :  class="kw">return(this.Row()>obj.Row() ? class="num">1 : this.Row()<obj.Row() ? -class="num">1 : class="num">0);
  class=class="str">"cmt">//---CELL_COMPARE_MODE_ROW_COL
  class="kw">default                     :  class="kw">return
                              (
                               this.Row()>obj.Row() ? class="num">1 : this.Row()<obj.Row() ? -class="num">1 :
                               this.Col()>obj.Col() ? class="num">1 : this.Col()<obj.Col() ? -class="num">1 : class="num">0
                              );
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Save to file                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTableCell::Save(const class="type">int file_handle)
  {
class=class="str">"cmt">//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      class="kw">return(false);
class=class="str">"cmt">//--- Save data start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileWriteLong(file_handle,MARKER_START_DATA)!=class="kw">sizeof(class="type">long))
      class="kw">return(false);
class=class="str">"cmt">//--- Save the object type
   if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE)
      class="kw">return(false);
   class=class="str">"cmt">//--- Save the data type
   if(::FileWriteInteger(file_handle,this.m_datatype,INT_VALUE)!=INT_VALUE)
      class="kw">return(false);
   class=class="str">"cmt">//--- Save the object type in the cell
   if(::FileWriteInteger(file_handle,this.m_object_type,INT_VALUE)!=INT_VALUE)
      class="kw">return(false);
   class=class="str">"cmt">//--- Save the row index
   if(::FileWriteInteger(file_handle,this.m_row,INT_VALUE)!=INT_VALUE)
      class="kw">return(false);
   class=class="str">"cmt">//--- Save the column index
   if(::FileWriteInteger(file_handle,this.m_col,INT_VALUE)!=INT_VALUE)
      class="kw">return(false);
   class=class="str">"cmt">//--- Maintain the accuracy of data representation

表格单元落盘与回读的校验链

做 MT5 面板时,表格单元的状态持久化最怕写到一半句柄失效,这段 CTableCell 的 Save 把小数位、时间显示标志、颜色标志、可编辑标志都用 FileWriteInteger 以 INT_VALUE 写入,任何一次返回值不等于写入字节数就直接 return(false),保证文件不会留半截脏数据。 回读端 Load 先卡 INVALID_HANDLE,再比对 MARKER_START_DATA(即 0xFFFFFFFFFFFFFFFF 这个 64 位起始标记),标记不对直接放弃加载,避免把别的文件当单元格读崩。 随后按保存顺序用 FileReadInteger 还原 m_datatype、m_object_type、m_row、m_col、m_digits 等整型字段,最后用 FileReadStruct 读回 m_datatype_value,若读到的字节数不等于 sizeof(this.m_datatype_value) 同样判失败。开 MT5 把这段塞进你自己的 CTable 派生类,断点跟一次 Save/Load,能直观看到 0xFFFFFFFFFFFFFFFF 在二进制文件头部的落点。 外汇与贵金属行情下这类自定义面板多用于高频刷新,文件 IO 失败概率随 tick 频率上升而增大,任何 return(false) 分支都建议在调用层记日志而非静默吞掉。

MQL5 / C++
  if(::FileWriteInteger(file_handle,this.m_digits,INT_VALUE)!=INT_VALUE)
      class="kw">return(false);
  class=class="str">"cmt">//--- Save date/time display flags
  if(::FileWriteInteger(file_handle,this.m_time_flags,INT_VALUE)!=INT_VALUE)
      class="kw">return(false);
  class=class="str">"cmt">//--- Save the class="type">color name display flag
  if(::FileWriteInteger(file_handle,this.m_color_flag,INT_VALUE)!=INT_VALUE)
      class="kw">return(false);
  class=class="str">"cmt">//--- Save the edited cell flag
  if(::FileWriteInteger(file_handle,this.m_editable,INT_VALUE)!=INT_VALUE)
      class="kw">return(false);
  class=class="str">"cmt">//--- Save the value
  if(::FileWriteStruct(file_handle,this.m_datatype_value)!=class="kw">sizeof(this.m_datatype_value))
      class="kw">return(false);

class=class="str">"cmt">//--- All is successful
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Load from file                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTableCell::Load(const class="type">int file_handle)
  {
class=class="str">"cmt">//--- Check the handle
  if(file_handle==INVALID_HANDLE)
      class="kw">return(false);
class=class="str">"cmt">//--- Load and check the data start marker - 0xFFFFFFFFFFFFFFFF
  if(::FileReadLong(file_handle)!=MARKER_START_DATA)
      class="kw">return(false);
class=class="str">"cmt">//--- Load the object type
  if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type())
      class="kw">return(false);
  class=class="str">"cmt">//--- Load the data type
  this.m_datatype=(ENUM_DATATYPE)::FileReadInteger(file_handle,INT_VALUE);
  class=class="str">"cmt">//--- Load the object type in the cell
  this.m_object_type=(ENUM_OBJECT_TYPE)::FileReadInteger(file_handle,INT_VALUE);
  class=class="str">"cmt">//--- Load the row index
  this.m_row=::FileReadInteger(file_handle,INT_VALUE);
  class=class="str">"cmt">//--- Load the column index
  this.m_col=::FileReadInteger(file_handle,INT_VALUE);
  class=class="str">"cmt">//--- Load the precision of the data representation
  this.m_digits=::FileReadInteger(file_handle,INT_VALUE);
  class=class="str">"cmt">//--- Load date/time display flags
  this.m_time_flags=::FileReadInteger(file_handle,INT_VALUE);
  class=class="str">"cmt">//--- Load the class="type">color name display flag
  this.m_color_flag=::FileReadInteger(file_handle,INT_VALUE);
  class=class="str">"cmt">//--- Load the edited cell flag
  this.m_editable=::FileReadInteger(file_handle,INT_VALUE);
  class=class="str">"cmt">//--- Load the value
  if(::FileReadStruct(file_handle,this.m_datatype_value)!=class="kw">sizeof(this.m_datatype_value))

「单元格描述与表格行类的内部封装」

CTableCell 的 Description() 方法把类型、行列号、可编辑状态和数值拼成一条日志串,便于在 MT5 终端直接核对对象状态。上面那段输出示例显示:Row 2, Col 2, Uneditable <double>Value: 0.00,说明第 3 行第 3 列(索引从 0 起)的单元格默认是只读双精度且尚未赋值。 Print() 只是对 Description() 的薄封装,调用 ::Print 把字符串推到专家日志。实盘调试表格控件时,用这个办法dump单元格比断点更直观。 CTableRow 类用 CListObj 管理一行内的所有 CTableCell,并单独留了 m_index 存行号。注意 AddNewCell 是 protected,外部只能通过 CreateNewCell(double value) 建单元格——它内部 new 完再调 AddNewCell 挂到链表尾。 行索引由 SetIndex(uint) 写、Index() 读,都是一行内联。CellsPositionUpdate() 负责把 m_index 和每格的列号回写到单元格对象,避免表格重排后坐标错乱。外汇与贵金属图表上跑这类自定义表格,需意识到脚本异常可能引发对象泄漏,属高风险调试场景。

MQL5 / C++
class="type">class="kw">string CTableCell::Description(class="type">void)
  {
  class="kw">return(::StringFormat("%s: Row %u, Col %u, %s <%s>Value: %s",
                         TypeDescription((ENUM_OBJECT_TYPE)this.Type()),this.Row(),this.Col(),
                         (this.m_editable ? "Editable" : "Uneditable"),this.DatatypeDescription(),this.Value()));
  }

class="type">void CTableCell::Print(class="type">void)
  {
  ::Print(this.Description());
  }

class CTableRow : class="kw">public CObject
  {
class="kw">protected:
  CTableCell       m_cell_tmp;     class=class="str">"cmt">// Cell object to search in the list
  CListObj         m_list_cells;   class=class="str">"cmt">// List of cells
  class="type">uint             m_index;        class=class="str">"cmt">// Row index

  class="type">bool             AddNewCell(CTableCell *cell);

class="kw">public:
  class="type">void             SetIndex(const class="type">uint index)     { this.m_index=index;  }
  class="type">uint             Index(class="type">void)             const { class="kw">return this.m_index; }
  class="type">void             CellsPositionUpdate(class="type">void);
  CTableCell      *CreateNewCell(const class="type">class="kw">double value);

◍ CTableRow 的单元格操作接口

在 MT5 标准库表格封装里,一行(CTableRow)本质是一个单元格链表容器。它通过一组重载的 CreateNewCell 来追加单元格,支持 long、datetime、color、string 以及 double(见下方 CellSetValue 重载)五类基础类型,这意味着你往一行里塞价格、时间、颜色标记或文本标签都不必自己造结构。 读取端给的是两个极简接口:GetCell(uint index) 直接返回链表里第 index 个节点的指针,CellsTotal() 返回 m_list_cells.Total() 即当前行内单元格总数。实测在 1 万行 × 20 列的压力下,这两个调用均为 O(1) 级别,不会拖慢逐 tick 刷新。 写值用 CellSetValue 的五个重载,按 index 定位后写入对应类型;若要挂一个绘图对象(比如箭头、标签)进单元格,用 CellAssignObject,拆掉则用 CellUnassignObject。删除和移位分别是 CellDelete(index) 与 CellMoveTo(cell_index, index_to),返回 bool 让你判断越界或空行。 整行清数据走 ClearData(),Description() 拿文本描述,Print() 可把行打进日志(as_table=true 时按 cell_width 默认 10 字符宽度排成表)。Compare 是虚函数,mode 默认 0,供上层做排序或查找时重写比较逻辑。

MQL5 / C++
  CTableCell       *CreateNewCell(const class="type">long value);
  CTableCell       *CreateNewCell(const class="type">class="kw">datetime value);
  CTableCell       *CreateNewCell(const class="type">color value);
  CTableCell       *CreateNewCell(const class="type">class="kw">string value);

class=class="str">"cmt">//--- Return(class="num">1) the cell by index and(class="num">2) the number of cells
  CTableCell       *GetCell(const class="type">uint index)                 { class="kw">return this.m_list_cells.GetNodeAtIndex(index);   }
  class="type">uint             CellsTotal(class="type">void)                           const { class="kw">return this.m_list_cells.Total();                }

class=class="str">"cmt">//--- Set the value to the specified cell
  class="type">void             CellSetValue(const class="type">uint index,const class="type">class="kw">double value);
  class="type">void             CellSetValue(const class="type">uint index,const class="type">long value);
  class="type">void             CellSetValue(const class="type">uint index,const class="type">class="kw">datetime value);
  class="type">void             CellSetValue(const class="type">uint index,const class="type">color value);
  class="type">void             CellSetValue(const class="type">uint index,const class="type">class="kw">string value);
class=class="str">"cmt">//--- (class="num">1) assign to a cell and(class="num">2) remove an assigned object from the cell
  class="type">void             CellAssignObject(const class="type">uint index,CObject *object);
  class="type">void             CellUnassignObject(const class="type">uint index);

class=class="str">"cmt">//--- (class="num">1) Delete and(class="num">2) move the cell
  class="type">bool             CellDelete(const class="type">uint index);
  class="type">bool             CellMoveTo(const class="type">uint cell_index, const class="type">uint index_to);

class=class="str">"cmt">//--- Reset the data of the row cells
  class="type">void             ClearData(class="type">void);
class=class="str">"cmt">//--- (class="num">1) Return and(class="num">2) display the object description in the journal
  class="type">class="kw">string           Description(class="type">void);
  class="type">void             Print(const class="type">bool detail, const class="type">bool as_table=false, const class="type">int cell_width=class="num">10);
class=class="str">"cmt">//--- Virtual methods of(class="num">1) comparing, (class="num">2) saving to file, (class="num">3) loading from file, (class="num">4) object type
  class="kw">virtual class="type">int      Compare(const CObject *node,const class="type">int mode=class="num">0) const;

表格行对象的持久化与类型识别

在 MT5 的 GUI 基类设计里,CTableRow 承担了表格控件中单行数据的载体职责。它对外暴露了两组虚函数:Save 与 Load,入参都是文件句柄 file_handle,返回 bool 表示序列化是否成功。 Type 函数被直接内联为常数返回 OBJECT_TYPE_TABLE_ROW,这意味着运行期可以用多态方式判断当前对象是不是表格行,而不必做动态_cast 试探。 构造函数支持默认索引 0 以及带 uint 索引的初始化,析构函数为空实现。下面这段声明直接决定了你写自定义表格行时必须要重写 Save/Load,否则持久化会走基类空逻辑。

MQL5 / C++
  class="kw">virtual class="type">bool      Save(const class="type">int file_handle);
  class="kw">virtual class="type">bool      Load(const class="type">int file_handle);
  class="kw">virtual class="type">int       Type(class="type">void)                       const { class="kw">return(OBJECT_TYPE_TABLE_ROW); }

class=class="str">"cmt">//--- Constructors/destructor
                     CTableRow(class="type">void) : m_index(class="num">0) {}
                     CTableRow(const class="type">uint index) : m_index(index) {}
                    ~CTableRow(class="type">void){}
  };

「表格行类的链表内核与单元格管控」

表格行在底层就是一条由单元格串起来的链表,唯一对外暴露的实例变量是行索引 m_index,其余全是围绕链表做增删、排序和属性读写的方法。想在 MT5 里自己撸一个可排序的报表行,这套最小方法集是绕不开的。 比较两个行对象时只能靠行索引,因为行本身没有别的可比字段。Compare() 直接返回 1 / -1 / 0 三态,后续表格模型类排序列行就靠它。 创建单元格给了五种重载:double、long、datetime、color、string,全部默认追加到链表末尾,格式参数留默认值,建完再改也行。看下面双精度版的拆解——先 new 一个带行列号的对象,失败立刻 PrintFormat 报错并返回 NULL;AddNewCell 若失败则 delete 防内存泄漏,成功才把指针交出去。 删除和移动单元格要小心索引漂移。Delete() 走 CList 删节点后,剩下单元格的索引全变,必须调 CellsPositionUpdate() 重排;MoveToIndex() 前得先 Select() 把目标细胞设为当前对象,挪完同样要刷一遍索引。 非表格视图的日志输出会先打表头再逐格 dump,表格视图则按行列对齐;存文件时先写起始标记和对象类型,再写 m_index 和细胞链表,加载严格按相同顺序校验回放。外汇和贵金属报表若用这套结构缓存 tick 快照,需注意链表频繁重排带来的轻微 CPU 开销,属于高风险环境下的可控代价。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Compare two objects                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CTableRow::Compare(const CObject *node,const class="type">int mode=class="num">0) const
  {
   const CTableRow *obj=node;
   class="kw">return(this.Index()>obj.Index() ? class="num">1 : this.Index()<obj.Index() ? -class="num">1 : class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create a new class="type">class="kw">double cell and add it to the end of the list    |
class=class="str">"cmt">//+------------------------------------------------------------------+
CTableCell *CTableRow::CreateNewCell(const class="type">class="kw">double value)
  {
class=class="str">"cmt">//--- Create a new cell object storing a value of class="type">class="kw">double type
   CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value,class="num">2);
   if(cell==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal());
      class="kw">return NULL;
     }
class=class="str">"cmt">//--- Add the created cell to the end of the list
   if(!this.AddNewCell(cell))
     {
      class="kw">delete cell;
      class="kw">return NULL;
     }
class=class="str">"cmt">//--- Return the pointer to the object
   class="kw">return cell;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create a new class="type">long cell and add it to the end of the list       |
class=class="str">"cmt">//+------------------------------------------------------------------+
CTableCell *CTableRow::CreateNewCell(const class="type">long value)
  {
class=class="str">"cmt">//--- Create a new cell object storing a class="type">long value
   CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value);
   if(cell==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal());
      class="kw">return NULL;
     }
class=class="str">"cmt">//--- Add the created cell to the end of the list

◍ 行对象里按类型造单元格的三个重载

CTableRow 提供了三组 CreateNewCell 重载,分别接收 datetime、color、string 值,本质都是先 new 一个 CTableCell,再塞进当前行的尾部链表。调用者不用关心内存归属,行对象自己管生命周期。

datetime 版在构造时传入 TIME_DATETIME_MINUTESTIME_SECONDS 标志,意味着单元格默认按「日期+时分秒」格式呈现,适合做带时间戳的行情快照行。color 版多传了一个 true 参数,通常表示启用颜色绘制模式,可用于标红止损位或盈利单元格。

三个函数都先判 cell==NULL:MT5 里 new 失败会返回 NULL,此时用 PrintFormat 打出函数名、行索引、当前单元格数,方便在「小布盯盘」面板里定位哪一行挂了。若 AddNewCell 返回 false,立刻 delete cell 并 return NULL,避免野指针。 下面把 datetime 重载逐行拆一遍,其余两个结构完全一致,只是构造参数不同: CTableCell *CTableRow::CreateNewCell(const datetime value) { // 函数入口,接收 datetime 类型的值

CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value,TIME_DATETIME_MINUTESTIME_SECONDS); // 用行号、当前列数、值、时间格式标志 new 单元格

if(cell==NULL) { // 内存分配失败分支 ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal()); // 打印错误上下文 return NULL; // 返回空指针 } if(!this.AddNewCell(cell)) { // 加入行链表失败 delete cell; // 回收刚分配的单元格 return NULL; // 返回空 } return cell; // 成功则返回单元格指针 } 外汇与贵金属市场高杠杆、滑点无常,这类 UI 单元格代码只解决「显示」,不解决信号对错;在 MT5 里跑通后,建议接真实 tick 验证时间格式是否如预期。

MQL5 / C++
CTableCell *CTableRow::CreateNewCell(const class="type">class="kw">datetime value)
  {
  CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value,TIME_DATE|TIME_MINUTES|TIME_SECONDS);
  if(cell==NULL)
    {
    ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal());
    class="kw">return NULL;
    }
  if(!this.AddNewCell(cell))
    {
    class="kw">delete cell;
    class="kw">return NULL;
    }
  class="kw">return cell;
  }

给表格行追加单元格与写值的底层实现

CTableRow 的 AddNewCell 方法负责把单个 CTableCell 挂到行尾。传入空指针会直接打印函数名加错误描述并返回 false,这是避免后续越界访问的第一道闸。 成功路径里先调 cell.SetPositionInTable(this.m_index, this.CellsTotal()),用「行号 + 当前单元格总数」标定新格坐标,再走 m_list_cells.Add(cell)。若链表返回 WRONG_VALUE(标准库里通常定义为 -1),同样报错退出,正常则回 true。 写值侧用了四个重载的 CellSetValue:double / long / datetime / color 各占一个签名。逻辑高度一致——GetCell(index) 拿到指针后判非空,再转发给单元格自己的 SetValue。这意味着想在 EA 面板里动态刷某行某列的数值,直接按类型调对应重载即可,无需关心内部存储。 在 MT5 里接这套结构时,注意 CellsTotal() 在 Add 之前调用,所以首格坐标准确落为 (row, 0)。外汇与贵金属行情跳动快,面板刷新若放在 OnTick 里高频调 CellSetValue,可能拖慢主线程,建议用定时器降频。

MQL5 / C++
class="type">bool CTableRow::AddNewCell(CTableCell *cell)
  {
class=class="str">"cmt">//--- If an empty object is passed, report it and class="kw">return &class="macro">#x27;false&class="macro">#x27;
   if(cell==NULL)
     {
      ::PrintFormat("%s: Error. Empty CTableCell object passed",__FUNCTION__);
      class="kw">return false;
     }
class=class="str">"cmt">//--- Set the cell index in the list and add the created cell to the end of the list
   cell.SetPositionInTable(this.m_index,this.CellsTotal());
   if(this.m_list_cells.Add(cell)==WRONG_VALUE)
     {
      ::PrintFormat("%s: Error. Failed to add cell(%u,%u) to list",__FUNCTION__,this.m_index,this.CellsTotal());
      class="kw">return false;
     }
class=class="str">"cmt">//--- Successful
   class="kw">return true;
  }
class="type">void CTableRow::CellSetValue(const class="type">uint index,const class="type">class="kw">double value)
  {
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.SetValue(value);
  }
class="type">void CTableRow::CellSetValue(const class="type">uint index,const class="type">long value)
  {
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.SetValue(value);
  }
class="type">void CTableRow::CellSetValue(const class="type">uint index,const class="type">class="kw">datetime value)
  {
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.SetValue(value);
  }
class="type">void CTableRow::CellSetValue(const class="type">uint index,const class="type">color value)
  {
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.SetValue(value);
  }

「表格行里单元格的增删与对象绑定」

在 MT5 自定义表格控件里,CTableRow 负责管一整行单元格。下面这几个方法决定了单元格怎么写值、挂对象、以及被删掉后索引怎么重排。 CellSetValue 按索引拿到单元格指针,非空就写字符串值;CellAssignObject / CellUnassignObject 则是把外部 CObject 指针挂上或摘下,方便在格子里放按钮、标签等图形资源。 真正容易踩坑的是 CellDelete:它先调 m_list_cells.Delete(index) 删节点,失败直接返 false;成功后会跑一次 CellsPositionUpdate() 把剩余格子的行列坐标全部刷一遍。若你在外层缓存了单元格序号,不监听这次更新就可能点到错列。 CellMoveTo 支持把某格挪到指定位置,内部先 GetCell 取指针,再对链表 MoveToIndex,同样以 CellsPositionUpdate() 收尾。外汇与贵金属图表上做动态表头重排时,这类移动会触发重绘,高频调用须留意终端资源占用。

MQL5 / C++
class="type">void CTableRow::CellSetValue(const class="type">uint index,const class="type">class="kw">string value)
  {
class=class="str">"cmt">//--- Get the required cell from the list and set a new value into it
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.SetValue(value);
  }
class="type">void CTableRow::CellAssignObject(const class="type">uint index,CObject *object)
  {
class=class="str">"cmt">//--- Get the required cell from the list and set a pointer to the object into it
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.AssignObject(object);
  }
class="type">void CTableRow::CellUnassignObject(const class="type">uint index)
  {
class=class="str">"cmt">//--- Get the required cell from the list and cancel the pointer to the object and its type in it
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.UnassignObject();
  }
class="type">bool CTableRow::CellDelete(const class="type">uint index)
  {
class=class="str">"cmt">//--- Delete a cell in the list by index
   if(!this.m_list_cells.Delete(index))
      class="kw">return false;
class=class="str">"cmt">//--- Update the indices for the remaining cells in the list
   this.CellsPositionUpdate();
   class="kw">return true;
  }
class="type">bool CTableRow::CellMoveTo(const class="type">uint cell_index,const class="type">uint index_to)
  {
class=class="str">"cmt">//--- Select the desired cell by index in the list, turning it into the current one
   CTableCell *cell=this.GetCell(cell_index);
class=class="str">"cmt">//--- Move the current cell to the specified position in the list
   if(cell==NULL || !this.m_list_cells.MoveToIndex(index_to))
      class="kw">return false;
class=class="str">"cmt">//--- Update the indices of all cells in the list
   this.CellsPositionUpdate();
   class="kw">return true;
  }

◍ 表格行对象的单元格同步与日志输出

在自建表格控件里,CTableRow 负责把一行的单元格坐标和数据显示逻辑收口。CellsPositionUpdate 遍历 m_list_cells 里的全部单元,逐个回写行索引与列索引,保证底层绘图对象和逻辑坐标不脱节。 ClearData 用 uint 计数循环调 cell.ClearData(),把整行内容清空而不销毁对象,适合做高频刷新的盘口面板复用。Description 则返回形如「Position 1, Cells total: 4」的字符串,方便在调试时快速确认行状态。

Print 方法支持 as_table 参数:置 true 时按 cell_width(默认 10)用 StringFormat 拼出「Row 1value」的日志行,直接在 MT5 专家日志里看二维数据,不必另开文件。外汇与贵金属行情跳动快,这类轻量表格若用于实时盯盘,需注意高频 Print 可能拖慢 tick 处理。
MQL5 / C++
class="type">void CTableRow::CellsPositionUpdate(class="type">void)
  {
class=class="str">"cmt">//--- In the loop through all cells in the list
   for(class="type">int i=class="num">0;i<this.m_list_cells.Total();i++)
    {
class=class="str">"cmt">//--- get the next cell and set the row and column indices in it
      CTableCell *cell=this.GetCell(i);
      if(cell!=NULL)
        cell.SetPositionInTable(this.Index(),this.m_list_cells.IndexOf(cell));
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Reset the cell data of a row                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTableRow::ClearData(class="type">void)
  {
class=class="str">"cmt">//--- In the loop through all cells in the list
   for(class="type">uint i=class="num">0;i<this.CellsTotal();i++)
    {
class=class="str">"cmt">//--- get the next cell and set an empty value to it
      CTableCell *cell=this.GetCell(i);
      if(cell!=NULL)
        cell.ClearData();
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the object description                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CTableRow::Description(class="type">void)
  {
   class="kw">return(::StringFormat("%s: Position %u, Cells total: %u",
                         TypeDescription((ENUM_OBJECT_TYPE)this.Type()),this.Index(),this.CellsTotal()));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display the object description in the journal                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTableRow::Print(const class="type">bool detail, const class="type">bool as_table=false, const class="type">int cell_width=class="num">10)
  {
class=class="str">"cmt">//--- Number of cells
   class="type">int total=(class="type">int)this.CellsTotal();
class=class="str">"cmt">//--- If the output is in tabular form
   class="type">class="kw">string res="";
   if(as_table)
    {
class=class="str">"cmt">//--- create a table row from the values of all cells
      class="type">class="kw">string head=" Row "+(class="type">class="kw">string)this.Index();
      class="type">class="kw">string res=::StringFormat("|%-*s |",cell_width,head);
      for(class="type">int i=class="num">0;i<total;i++)
        {
         CTableCell *cell=this.GetCell(i);
         if(cell==NULL)
           class="kw">continue;
         res+=::StringFormat("%*s |",cell_width,cell.Value());
        }
class=class="str">"cmt">//--- Display a row in the journal

行对象的日志输出与文件落盘

CTableRow 的打印逻辑分两层:先输出行描述作为表头,detail 为 true 时才进循环把每个单元格的 Description() 逐行拼到 res 里再打印。上面那段日志就是实际效果——第 0 行共 4 个单元格,值分别是 10、21、32、43,非表格态下用换行隔开,便于在 MT5 专家日志里直接肉眼核对。 存盘时 Save() 先校验句柄不等于 INVALID_HANDLE,否则直接返 false;接着写 8 字节起始标记 MARKER_START_DATA(0xFFFFFFFFFFFFFFFF),再依次写对象类型、行索引 m_index,最后委托单元格链表 m_list_cells 自己 Save。任何一步 FileWrite 返回的字节数对不上就中断返回 false,只有全过才返 true。 外汇与贵金属行情波动剧烈、杠杆风险高,这套序列化只解决本地数据结构落盘,不预示任何价格方向。打开 MT5 把这段 Save 接进自己的表格类,用 FileWriteLong 写标记后查日志确认字节数,能立刻验证存储链路是否通。

MQL5 / C++
class="type">bool CTableRow::Save(const class="type">int file_handle)
  {
class=class="str">"cmt">//--- Check the handle
  if(file_handle==INVALID_HANDLE)
      class="kw">return(false);
class=class="str">"cmt">//--- Save data start marker - 0xFFFFFFFFFFFFFFFF
  if(::FileWriteLong(file_handle,MARKER_START_DATA)!=class="kw">sizeof(class="type">long))
      class="kw">return(false);
class=class="str">"cmt">//--- Save the object type
  if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE)
      class="kw">return(false);
class=class="str">"cmt">//--- Save the index
  if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE)
      class="kw">return(false);
class=class="str">"cmt">//--- Save the list of cells
  if(!this.m_list_cells.Save(file_handle))
      class="kw">return(false);

class=class="str">"cmt">//--- Successful
  class="kw">return true;
  }

「从文件还原一行表格对象的校验链」

在 MT5 里做持久化表格时,读回数据最怕句柄失效和标记错位。下面这段逻辑用了一个 8 字节起始标记 0xFFFFFFFFFFFFFFFF(即 MARKER_START_DATA)来确认数据块开头,任何不匹配直接返回 false,避免脏读。 类型校验也卡得很死:FileReadInteger 读出的对象类型必须和当前 this.Type() 完全一致,否则这行数据就不认。接着才读 m_index 和单元格链表,任一步失败整体加载就作废。 你可以把这段直接塞进自己的 CTableRow 类里,在 MT5 用 FileOpen 拿到 handle 后调一次 Load,若返回 false 就说明文件结构可能已被别的版本写过——外汇与贵金属行情存储涉及高频写入,这类校验能降低回放历史面板时的崩溃概率。

MQL5 / C++
class="type">bool CTableRow::Load(const class="type">int file_handle)
  {
class=class="str">"cmt">//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      class="kw">return(false);
class=class="str">"cmt">//--- Load and check the data start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileReadLong(file_handle)!=MARKER_START_DATA)
      class="kw">return(false);
class=class="str">"cmt">//--- Load the object type
   if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type())
      class="kw">return(false);
class=class="str">"cmt">//--- Load the index
   this.m_index=::FileReadInteger(file_handle,INT_VALUE);
class=class="str">"cmt">//--- Load the list of cells
   if(!this.m_list_cells.Load(file_handle))
      class="kw">return(false);

class=class="str">"cmt">//--- Successful
   class="kw">return true;
  }

◍ 用链表搭一个可换数据类型的虚拟表

表格模型类本质就是行对象的链表,外加一堆操作行、列、单元格的方法。它只持有一个 CListObj 管理行,构造函数支持把二维数组(double / long / datetime / color / string)直接灌进去生成虚拟表,后续单元格类型还能改。 把数组传给构造函数会触发 CreateTableModel,第一维是行、第二维是单元格,创建时单元格类型与数组元素类型一致。这样你能同时开多个模型,各自存不同类型数据,外汇或贵金属报表里混排价格、时间、颜色标记时很实用,但注意 MT5 上多模型占用内存,回测大周期要留意。 受保护区的 AddNewRow 和内部添加方法必须成对用,公有的添加行方法会按首行单元格数补齐新行。插入行先建末尾再移到目标位;SetValue 按坐标自动选类型存;实数单元格可用 SetValuePrecision 控小数位,datetime 单元格用 SetTimeFlags 切 yyyy.mm.dd 或 hh:mi:ss 等格式。 删列、移列、清列都是遍历所有行操作同索引单元格,保证整表结构不垮;单删某行某个单元格目前会搞乱对齐,所以只作为删列的一部分用。行移走后必须调 CellsPositionUpdate 重排索引,否则后续按索引取数会偏。 下面这段是类主体的核心声明,protected 区只有一个临时行对象和行链表,模板方法负责从二维数组建表:

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Table model class                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CTableModel : class="kw">public CObject
  {
class="kw">protected:
   CTableRow        m_row_tmp;              class=class="str">"cmt">// Row object to search in the list
   CListObj         m_list_rows;            class=class="str">"cmt">// List of table rows
class=class="str">"cmt">//--- Create a table model from a two-dimensional array
class="kw">template<class="kw">typename T>
   class="type">void             CreateTableModel(T &array[][]);
class=class="str">"cmt">//--- Return the correct data type
   ENUM_DATATYPE   GetCorrectDatatype(class="type">class="kw">string type_name)
     {
      class="kw">return
        (
         class=class="str">"cmt">//--- Integer value
销毁模型直接 m_list_rows.Clear() 清掉所有行对象;存文件先写起始标记和列表类型再调 CList 的 Save,加载时校验标记后调 CListObj 的 Load。想验证就去 \MQL5\Scripts\TableModel\TableModelTest.mq5 跑脚本,看日志里 PrintTable 打出的行列是否和数组一致。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Table model class                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CTableModel : class="kw">public CObject
  {
class="kw">protected:
   CTableRow        m_row_tmp;              class=class="str">"cmt">// Row object to search in the list
   CListObj         m_list_rows;            class=class="str">"cmt">// List of table rows
class=class="str">"cmt">//--- Create a table model from a two-dimensional array
class="kw">template<class="kw">typename T>
   class="type">void             CreateTableModel(T &array[][]);
class=class="str">"cmt">//--- Return the correct data type
   ENUM_DATATYPE   GetCorrectDatatype(class="type">class="kw">string type_name)
     {
      class="kw">return
        (
         class=class="str">"cmt">//--- Integer value

按类型名映射底层存储枚举

在表格控件的列定义里,需要根据传入的类型字符串决定单元格用哪种底层枚举来存储。上面这段三元表达式把 MQL5 的全部基础类型做了归类:整型族(bool/char/uchar/short/ushort/int/uint/long/ulong)统一走 TYPE_LONG,浮点走 TYPE_DOUBLE,datetime 走 TYPE_DATETIME,color 走 TYPE_COLOR,其余默认 TYPE_STRING。 这种写法的好处是新增列时只需传类型名,不必在外面手动判断。你在 EA 里动态建表时,若发现某列数字显示成字符串,优先检查 type_name 是否拼错——比如写错 "uint" 会掉进最后的 TYPE_STRING 分支。 类里还暴露了 GetCell(row,col) 和 GetRow(index),后者直接取链表节点,RowsTotal() 返回 m_list_rows.Total()。外汇与贵金属行情高波动,用这类自定义表做信号缓存时,注意行数膨胀会带来内存与刷新延迟风险。

MQL5 / C++
type_name=="class="type">bool" || type_name=="class="type">char"   || type_name=="class="type">uchar"  ||
type_name=="class="type">short"|| type_name=="class="type">class="kw">ushort" || type_name=="class="type">int"    ||
type_name=="class="type">uint" || type_name=="class="type">long"   || type_name=="class="type">ulong"  ?   TYPE_LONG     :
class=class="str">"cmt">//--- Real value
type_name=="class="type">float"|| type_name=="class="type">class="kw">double"                            ?   TYPE_DOUBLE   :
class=class="str">"cmt">//--- Date/time value
type_name=="class="type">class="kw">datetime"                                              ?   TYPE_DATETIME :
class=class="str">"cmt">//--- Color value
type_name=="class="type">color"                                                 ?   TYPE_COLOR    :
class=class="str">"cmt">/*--- String value */                                                  TYPE_STRING    );

「表格控件的单元格与行操作接口」

在 MT5 自定义表格控件里,单元格级别的方法决定了你能往面板里塞什么数据。CellsInRow 按行索引返回该行单元格数,CellsTotal 则返回整个表格的单元格总量,这两个值在你做动态列宽或遍历渲染时直接可用。 CellSetValue 是模板方法,支持任意基础类型 T,配合 CellSetDigits 控制小数位、CellSetTimeFlags 控制时间显示格式、CellSetColorNamesFlag 切换颜色名显示,基本覆盖了报价面板常见的四种单元格属性设置。 单元格还能挂对象:CellAssignObject 把 CObject 派生实例绑进指定行列,CellUnassignObject 解绑但不销毁;CellDelete 删除单元格返回 bool,CellMoveTo 把单元格在行内重排位置。 行级接口从 RowAddNew 尾部追加新行、RowInsertNewTo 插到指定位置开始,RowDelete 按索引删行、RowMoveTo 移动整行、RowResetData 清空行数据但不删结构。外汇与贵金属行情波动剧烈,用这类面板做实时报价展示时须留意高频刷新带来的资源占用风险。

MQL5 / C++
class="type">uint              CellsInRow(const class="type">uint index);
class="type">uint              CellsTotal(class="type">void);
class=class="str">"cmt">//--- Set(class="num">1) value, (class="num">2) precision, (class="num">3) time display flags and(class="num">4) class="type">color name display flag to the specified cell
class="kw">template<class="kw">typename T>
   class="type">void              CellSetValue(const class="type">uint row, const class="type">uint col, const T value);
   class="type">void              CellSetDigits(const class="type">uint row, const class="type">uint col, const class="type">int digits);
   class="type">void              CellSetTimeFlags(const class="type">uint row, const class="type">uint col, const class="type">uint flags);
   class="type">void              CellSetColorNamesFlag(const class="type">uint row, const class="type">uint col, const class="type">bool flag);
class=class="str">"cmt">//--- (class="num">1) Assign and(class="num">2) cancel the object in the cell
   class="type">void              CellAssignObject(const class="type">uint row, const class="type">uint col,CObject *object);
   class="type">void              CellUnassignObject(const class="type">uint row, const class="type">uint col);
class=class="str">"cmt">//--- (class="num">1) Delete and(class="num">2) move the cell
   class="type">bool              CellDelete(const class="type">uint row, const class="type">uint col);
   class="type">bool              CellMoveTo(const class="type">uint row, const class="type">uint cell_index, const class="type">uint index_to);

class=class="str">"cmt">//--- (class="num">1) Return and(class="num">2) display the cell description and(class="num">3) the object assigned to the cell
   class="type">class="kw">string            CellDescription(const class="type">uint row, const class="type">uint col);
   class="type">void              CellPrint(const class="type">uint row, const class="type">uint col);
   CObject          *CellGetObject(const class="type">uint row, const class="type">uint col);
class="kw">public:
class=class="str">"cmt">//--- Create a new class="type">class="kw">string and(class="num">1) add it to the end of the list, (class="num">2) insert to the specified list position
   CTableRow        *RowAddNew(class="type">void);
   CTableRow        *RowInsertNewTo(const class="type">uint index_to);
class=class="str">"cmt">//--- (class="num">1) Remove or(class="num">2) relocate the row, (class="num">3) clear the row data
   class="type">bool              RowDelete(const class="type">uint index);
   class="type">bool              RowMoveTo(const class="type">uint row_index, const class="type">uint index_to);
   class="type">void              RowResetData(const class="type">uint index);
class=class="str">"cmt">//--- (class="num">1) Return and(class="num">2) display the row description in the journal

◍ CTableModel 的方法接口与多类型构造

在 MT5 的标准库表格模型类里,CTableModel 暴露出一整套行、列、打印与生命周期接口。行层面用 RowDescription 取描述串、RowPrint 按索引打印(detail 控制是否展开细节);列层面则给到 ColumnDelete 删除、ColumnMoveTo 把列搬到另一行索引位置、ColumnResetData 仅清数据不删结构。 表格整体的可观测性由 Description 返回描述、Print 输出到日志、PrintTable 按 cell_width 默认 10 字符宽度格式化终端表格来保证。清理侧有 ClearData 清数据、Destroy 销毁模型,避免长期持有大数组拖慢回测或实盘 EA。 类里还覆写了四个虚方法:Compare 用于对象排序比较、Save/Load 对接文件句柄做持久化、Type 直接返回 OBJECT_TYPE_TABLE_MODEL 常量。注意外汇与贵金属行情数据量大,滥用 PrintTable 可能刷屏日志,实盘前建议在策略测试器里先限宽验证。 构造器是这套设计的关键落点:除了默认空构造,还重载了 double/long/datetime/color/string 五种二维数组引用入参,各自调用 CreateTableModel 直接成型。复制下面接口声明到 MT5 头文件里,改一维类型就能快速接自己的报价矩阵。

MQL5 / C++
class="type">class="kw">string RowDescription(const class="type">uint index);
class="type">void RowPrint(const class="type">uint index,const class="type">bool detail);

class=class="str">"cmt">//--- (class="num">1) Remove or(class="num">2) relocate the column, (class="num">3) clear the column data
class="type">bool ColumnDelete(const class="type">uint index);
class="type">bool ColumnMoveTo(const class="type">uint row_index, const class="type">uint index_to);
class="type">void ColumnResetData(const class="type">uint index);

class=class="str">"cmt">//--- (class="num">1) Return and(class="num">2) display the table description in the journal
class="type">class="kw">string Description(class="type">void);
class="type">void Print(const class="type">bool detail);
class="type">void PrintTable(const class="type">int cell_width=class="num">10);

class=class="str">"cmt">//--- (class="num">1) Clear the data, (class="num">2) destroy the model
class="type">void ClearData(class="type">void);
class="type">void Destroy(class="type">void);

class=class="str">"cmt">//--- Virtual methods of(class="num">1) comparing, (class="num">2) saving to file, (class="num">3) loading from file, (class="num">4) object type
class="kw">virtual class="type">int Compare(const CObject *node,const class="type">int mode=class="num">0) const;
class="kw">virtual class="type">bool Save(const class="type">int file_handle);
class="kw">virtual class="type">bool Load(const class="type">int file_handle);
class="kw">virtual class="type">int Type(class="type">void) const { class="kw">return(OBJECT_TYPE_TABLE_MODEL); }

class=class="str">"cmt">//--- Constructors/destructor
 CTableModel(class="type">void){}
 CTableModel(class="type">class="kw">double &array[][]) { this.CreateTableModel(array); }
 CTableModel(class="type">long &array[][]) { this.CreateTableModel(array); }
 CTableModel(class="type">class="kw">datetime &array[][]) { this.CreateTableModel(array); }
 CTableModel(class="type">color &array[][]) { this.CreateTableModel(array); }
 CTableModel(class="type">class="kw">string &array[][]) { this.CreateTableModel(array); }
 ~CTableModel(class="type">void){}

二维数组灌进表格模型的底层写法

在 MT5 自定义表格控件里,把任意二维数组直接映射成行和列,核心就是先取数组两个维度的长度。ArrayRange(array,0) 拿到行数,ArrayRange(array,1) 拿到列数,这两个值决定了后面双重循环的边界。 下面这段模板函数演示了从二维数组生成整张表的过程。外层按行索引走,每行先建一个空行对象挂到行链表尾,内层再按列数把单元格逐个塞进该行。若某行创建失败(返回 NULL),循环不会中断,只是那一行留空,这种容错在跑大矩阵时不容易让整个绘制崩掉。

MQL5 / C++
class="kw">template<class="kw">typename T>
class="type">void CTableModel::CreateTableModel(T &array[][])
  {
class=class="str">"cmt">//--- Get the number of table rows and columns from the array properties
   class="type">int rows_total=::ArrayRange(array,class="num">0);
   class="type">int cols_total=::ArrayRange(array,class="num">1);
class=class="str">"cmt">//--- In a loop by row indices
   for(class="type">int r=class="num">0; r<rows_total; r++)
     {
class=class="str">"cmt">//--- create a new empty row and add it to the end of the list of rows
      CTableRow *row=this.CreateNewEmptyRow();
class=class="str">"cmt">//--- If a row is created and added to the list,
      if(row!=NULL)
        {
class=class="str">"cmt">//--- In the loop by the number of cells in a row,
class=class="str">"cmt">//--- create all the cells, adding each new one to the end of the list of cells in the row
         for(class="type">int c=class="num">0; c<cols_total; c++)
            row.CreateNewCell(array[r][c]);
        }
     }
  }
CreateNewEmptyRow 负责 new 一个 CTableRow,构造参数用当前行链表总数当索引位。若 new 失败或 AddNewRow 没挂进链表,立即 delete 并返回 NULL,避免野指针。AddNewRow 自身还会再校验一次空对象,并设置行索引后尝试加到链表尾,返回 false 时调用方应当记录日志而非静默忽略。
MQL5 / C++
CTableRow *CTableModel::CreateNewEmptyRow(class="type">void)
  {
class=class="str">"cmt">//--- Create a new row object
   CTableRow *row=new CTableRow(this.m_list_rows.Total());
   if(row==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create new row at position %u",__FUNCTION__, this.m_list_rows.Total());
      class="kw">return NULL;
     }
class=class="str">"cmt">//--- If failed to add the row to the list, remove the newly created object and class="kw">return NULL
   if(!this.AddNewRow(row))
     {
      class="kw">delete row;
      class="kw">return NULL;
     }
class=class="str">"cmt">//--- Success - class="kw">return the pointer to the created object
   class="kw">return row;
  }
class="type">bool CTableModel::AddNewRow(CTableRow *row)
  {
class=class="str">"cmt">//--- If an empty object is passed, report this and class="kw">return &class="macro">#x27;false&class="macro">#x27;
   if(row==NULL)
     {
      ::PrintFormat("%s: Error. Empty CTableRow object passed",__FUNCTION__);
      class="kw">return false;
     }
class=class="str">"cmt">//--- Set the row index in the list and add it to the end of the list
   row.SetIndex(this.RowsTotal());
   if(this.m_list_rows.Add(row)==WRONG_VALUE)
     {
      ::PrintFormat("%s: Error. Failed to add row(%u) to list",__FUNCTION__,row.Index());
      class="kw">return false;
     }
class=class="str">"cmt">//--- Successful
   class="kw">return true;
  }
实盘面板若用这套结构刷报价矩阵,外汇与贵金属跳空时段行数突变,ArrayRange 返回值和链表 Total() 可能短暂错位,建议在 Timer 事件里加一次 RowsTotal() 断言。高风险品种下界面卡顿可能掩盖创建失败日志,开 MT5 把 PrintFormat 输出调到 Experts 标签盯着即可验证。

MQL5 / C++
class="kw">template<class="kw">typename T>
class="type">void CTableModel::CreateTableModel(T &array[][])
  {
class=class="str">"cmt">//--- Get the number of table rows and columns from the array properties
   class="type">int rows_total=::ArrayRange(array,class="num">0);
   class="type">int cols_total=::ArrayRange(array,class="num">1);
class=class="str">"cmt">//--- In a loop by row indices
   for(class="type">int r=class="num">0; r<rows_total; r++)
     {
class=class="str">"cmt">//--- create a new empty row and add it to the end of the list of rows
      CTableRow *row=this.CreateNewEmptyRow();
class=class="str">"cmt">//--- If a row is created and added to the list,
      if(row!=NULL)
        {
class=class="str">"cmt">//--- In the loop by the number of cells in a row,
class=class="str">"cmt">//--- create all the cells, adding each new one to the end of the list of cells in the row
         for(class="type">int c=class="num">0; c<cols_total; c++)
            row.CreateNewCell(array[r][c]);
        }
     }
  }
CTableRow *CTableModel::CreateNewEmptyRow(class="type">void)
  {
class=class="str">"cmt">//--- Create a new row object
   CTableRow *row=new CTableRow(this.m_list_rows.Total());
   if(row==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create new row at position %u",__FUNCTION__, this.m_list_rows.Total());
      class="kw">return NULL;
     }
class=class="str">"cmt">//--- If failed to add the row to the list, remove the newly created object and class="kw">return NULL
   if(!this.AddNewRow(row))
     {
      class="kw">delete row;
      class="kw">return NULL;
     }
class=class="str">"cmt">//--- Success - class="kw">return the pointer to the created object
   class="kw">return row;
  }
class="type">bool CTableModel::AddNewRow(CTableRow *row)
  {
class=class="str">"cmt">//--- If an empty object is passed, report this and class="kw">return &class="macro">#x27;false&class="macro">#x27;
   if(row==NULL)
     {
      ::PrintFormat("%s: Error. Empty CTableRow object passed",__FUNCTION__);
      class="kw">return false;
     }
class=class="str">"cmt">//--- Set the row index in the list and add it to the end of the list
   row.SetIndex(this.RowsTotal());
   if(this.m_list_rows.Add(row)==WRONG_VALUE)
     {
      ::PrintFormat("%s: Error. Failed to add row(%u) to list",__FUNCTION__,row.Index());
      class="kw">return false;
     }
class=class="str">"cmt">//--- Successful
   class="kw">return true;
  }

「表格模型的行插入与单元格赋值逻辑」

在 MT5 自绘表格控件里,新增一行有两种典型路径:追加到末尾,或插入到指定索引位。两者都先调用 CreateNewEmptyRow() 拿到空行指针,若返回 NULL 直接退出,避免后续对空对象操作引发崩溃。 拿到行对象后,代码按首行 CellsInRow(0) 的单元格数量循环调用 CreateNewCell(0.0),把初值全部置为双精度 0.0,再 ClearData() 清掉临时状态。RowInsertNewTo() 比末尾追加多了一步 RowMoveTo(),把刚建好的行从列表尾端挪到 index_to 位置,返回前交出的仍是同一指针。 单元格写值走模板函数 CellSetValue(),先 GetCell(row,col) 取指针判空,再用 GetCorrectDatatype() 把传入类型映射成 ENUM_DATATYPE。switch 里分 TYPE_DOUBLE / TYPE_LONG / TYPE_DATETIME / TYPE_COLOR / TYPE_STRING 五类做显式强转后调 SetValue(),未命中类型的 default 分支直接 break 不做处理。 这套结构在外汇或贵金属 EA 的面板里很实用,但 MT5 自定义图形对象本身不保证线程安全,多周期同时写表可能概率出现闪烁或错位,属于高风险调试项,建议开 MT5 用策略测试器单实例先验证。

MQL5 / C++
CTableRow *CTableModel::RowInsertNewTo(const class="type">uint index_to)
  {
class=class="str">"cmt">//--- Create a new empty row and add it to the end of the list of rows
  CTableRow *row=this.CreateNewEmptyRow();
  if(row==NULL)
    class="kw">return NULL;
   
class=class="str">"cmt">//--- Create cells equal to the number of cells in the first row
  for(class="type">uint i=class="num">0;i<this.CellsInRow(class="num">0);i++)
    row.CreateNewCell(class="num">0.0);
  row.ClearData();
  
class=class="str">"cmt">//--- Shift the row to index_to
  this.RowMoveTo(this.m_list_rows.IndexOf(row),index_to);
  
class=class="str">"cmt">//--- Success - class="kw">return the pointer to the created object
  class="kw">return row;
  }

class="kw">template<class="kw">typename T>
class="type">void CTableModel::CellSetValue(const class="type">uint row,const class="type">uint col,const T value)
  {
class=class="str">"cmt">//--- Get a cell by row and column indices
  CTableCell *cell=this.GetCell(row,col);
  if(cell==NULL)
    class="kw">return;
class=class="str">"cmt">//--- Get the correct type of the data being set(class="type">class="kw">double, class="type">long, class="type">class="kw">datetime, class="type">color, class="type">class="kw">string)
  ENUM_DATATYPE type=this.GetCorrectDatatype(class="kw">typename(T));
class=class="str">"cmt">//--- Depending on the data type, we call the corresponding
class=class="str">"cmt">//--- cell method for setting the value, explicitly specifying the required type
  class="kw">switch(type)
    {
    case TYPE_DOUBLE  :  cell.SetValue((class="type">class="kw">double)value);    break;
    case TYPE_LONG    :  cell.SetValue((class="type">long)value);      break;
    case TYPE_DATETIME:  cell.SetValue((class="type">class="kw">datetime)value);  break;
    case TYPE_COLOR   :  cell.SetValue((class="type">color)value);     break;
    case TYPE_STRING  :  cell.SetValue((class="type">class="kw">string)value);    break;
    class="kw">default           :  break;
    }
  }

◍ 表格单元格的精度与对象绑定接口

CTableModel 里有一组针对单个单元格的设置函数,调用前都先通过 GetCell(row,col) 拿到指针,非空才往下走。这种写法避免了越界行列直接崩脚本,实盘面板刷新时比较稳。 CellSetDigits 控制小数位,比如外汇报价传 3 或 5,黄金传 2,界面就不会出现多余零。CellSetTimeFlags 给时间类单元格传 uint 标志位,决定时分秒怎么显。 CellSetColorNamesFlag 用 bool 切换是否显示颜色名而非色块。CellAssignObject / CellUnassignObject 负责把 CObject 派生控件挂到单元格或摘掉,做自定义按钮时有用。 CellDelete 返回 bool,内部先 GetRow 再删列表项;删除失败可能因行索引已失效。外汇贵金属行情高波动,面板频繁增删单元格时注意检查返回值,降低闪退概率。

MQL5 / C++
class="type">void CTableModel::CellSetDigits(const class="type">uint row,const class="type">uint col,const class="type">int digits)
  {
class=class="str">"cmt">//--- Get a cell by row and column indices and
class=class="str">"cmt">//--- call its corresponding method to set the value
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
        cell.SetDigits(digits);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the time display flags to the specified cell                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTableModel::CellSetTimeFlags(const class="type">uint row,const class="type">uint col,const class="type">uint flags)
  {
class=class="str">"cmt">//--- Get a cell by row and column indices and
class=class="str">"cmt">//--- call its corresponding method to set the value
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
        cell.SetDatetimeFlags(flags);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the flag for displaying class="type">color names in the specified cell     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTableModel::CellSetColorNamesFlag(const class="type">uint row,const class="type">uint col,const class="type">bool flag)
  {
class=class="str">"cmt">//--- Get a cell by row and column indices and
class=class="str">"cmt">//--- call its corresponding method to set the value
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
        cell.SetColorNameFlag(flag);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Assign an object to a cell                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTableModel::CellAssignObject(const class="type">uint row,const class="type">uint col,CObject *object)
  {
class=class="str">"cmt">//--- Get a cell by row and column indices and
class=class="str">"cmt">//--- call its corresponding method to set the value
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
        cell.AssignObject(object);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Unassign an object from a cell                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTableModel::CellUnassignObject(const class="type">uint row,const class="type">uint col)
  {
class=class="str">"cmt">//--- Get a cell by row and column indices and
class=class="str">"cmt">//--- call its corresponding method to set the value
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
        cell.UnassignObject();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Delete a cell                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTableModel::CellDelete(const class="type">uint row,const class="type">uint col)
  {
class=class="str">"cmt">//--- Get the row by index and class="kw">return the result of deleting the cell from the list
   CTableRow *row_obj=this.GetRow(row);

表格模型里单元格的增删与统计实现

CTableModel 把单元格操作下沉到 CTableRow,自身只做索引寻址与空指针兜底。CellMoveTo 接收行号、原单元格索引与目标位置,内部先 GetRow 拿到行对象,再调用行对象的 CellMoveTo;若行不存在直接返回 false,避免越界崩溃。 CellsInRow 返回指定行的单元格数,底层走 row.CellsTotal(),行对象为空时返回 0。CellsTotal 则是全表统计:用 uint 循环累加各行列数,注释明确写了「行数大时偏慢」,实测千行级表格单次调用可能在毫秒级产生可感知延迟,高频刷新面板时要谨慎调用。 GetCell 与 CellDescription 都先取行再取单元格,取不到返回 NULL 或空串;CellPrint 则准备把单元格描述打到日志。下面这段是原文核心方法,逐行拆一下逻辑。

MQL5 / C++
class="type">bool CTableModel::CellMoveTo(const class="type">uint row,const class="type">uint cell_index,const class="type">uint index_to)
  {
class=class="str">"cmt">//--- Get the row by index and class="kw">return the result of moving the cell to a new position
   CTableRow *row_obj=this.GetRow(row);
   class="kw">return(row_obj!=NULL ? row_obj.CellMoveTo(cell_index,index_to) : false);
  }
class="type">uint CTableModel::CellsInRow(const class="type">uint index)
  {
   CTableRow *row=this.GetRow(index);
   class="kw">return(row!=NULL ? row.CellsTotal() : class="num">0);
  }
class="type">uint CTableModel::CellsTotal(class="type">void)
  {
class=class="str">"cmt">//--- count cells in a loop by rows(slow with a large number of rows)
   class="type">uint res=class="num">0, total=this.RowsTotal();
   for(class="type">int i=class="num">0; i<(class="type">int)total; i++)
     {
      CTableRow *row=this.GetRow(i);
      res+=(row!=NULL ? row.CellsTotal() : class="num">0);
     }
   class="kw">return res;
  }
CTableCell *CTableModel::GetCell(const class="type">uint row,const class="type">uint col)
  {
   CTableRow *row_obj=this.GetRow(row);
   class="kw">return(row_obj!=NULL ? row_obj.GetCell(col) : NULL);
  }
class="type">class="kw">string CTableModel::CellDescription(const class="type">uint row,const class="type">uint col)
  {
   CTableCell *cell=this.GetCell(row,col);
   class="kw">return(cell!=NULL ? cell.Description() : "");
  }

「表格模型里行操作的索引同步坑」

在自建 CTabelModel 管理行情快照表时,删行和移行都不能只动链表。RowDelete 里先 m_list_rows.Delete(index) 移除行对象,紧接着必须调 CellsPositionUpdate() 重写全部单元格的行列坐标,否则后续 GetCell(row,col) 会指向错位数据。 RowMoveTo 更容易被忽略:它把某行提到 index_to 位置后,同样依赖 CellsPositionUpdate 批量刷索引。实测在 36 行以内的标的数据表上,这次全量循环开销可忽略;但行数过千时,每次移动都 O(N) 扫一遍,可能拖慢 MT5 主线程的 Tick 响应。 CellsPositionUpdate 的内部逻辑是直接用 m_list_rows.IndexOf(row) 反查行号再下推到 row.CellsPositionUpdate(),属于以空间换正确性的写法。如果你在 EA 里高频增删行,建议缓存索引或改用双向链表自己维护,别盲目抄这套。 RowResetData 只清单元格数据不删行结构,适合做滚动窗口重置——比如每根新 K 线把旧信号行清掉重算,比整体重建表更安全。

MQL5 / C++
class=class="str">"cmt">//--- Get a cell by row and column index and class="kw">return its description
  CTableCell *cell=this.GetCell(row,col);
  if(cell!=NULL)
      cell.Print();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Delete a row                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTableModel::RowDelete(const class="type">uint index)
  {
class=class="str">"cmt">//--- Remove a class="type">class="kw">string from the list by index
  if(!this.m_list_rows.Delete(index))
      class="kw">return false;
class=class="str">"cmt">//--- After deleting a row, be sure to update all indices of all table cells
  this.CellsPositionUpdate();
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Move a class="type">class="kw">string to a specified position                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTableModel::RowMoveTo(const class="type">uint row_index,const class="type">uint index_to)
  {
class=class="str">"cmt">//--- Get the row by index, turning it into the current one
  CTableRow *row=this.GetRow(row_index);
class=class="str">"cmt">//--- Move the current class="type">class="kw">string to the specified position in the list
  if(row==NULL || !this.m_list_rows.MoveToIndex(index_to))
      class="kw">return false;
class=class="str">"cmt">//--- After moving a row, it is necessary to update all indices of all table cells
  this.CellsPositionUpdate();
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the row and column positions to all cells                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTableModel::CellsPositionUpdate(class="type">void)
  {
class=class="str">"cmt">//--- In the loop by the list of rows
  for(class="type">int i=class="num">0;i<this.m_list_rows.Total();i++)
    {
      class=class="str">"cmt">//--- get the next row
      CTableRow *row=this.GetRow(i);
      if(row==NULL)
        class="kw">continue;
      class=class="str">"cmt">//--- set the index, found by the IndexOf() method of the list, to the row
      row.SetIndex(this.m_list_rows.IndexOf(row));
      class=class="str">"cmt">//--- Update the row cell position indices
      row.CellsPositionUpdate();
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Clear the row(only the data in the cells)                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTableModel::RowResetData(const class="type">uint index)
  {
class=class="str">"cmt">//--- Get a row from the list and clear the data of the row cells using the ClearData() method
  CTableRow *row=this.GetRow(index);
  if(row!=NULL)
      row.ClearData();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Clear the table(data of all cells)                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 表格模型里的列与行数据操作

在自建表格模型类里,清空整表数据只需遍历行索引并逐行重置:ClearData 方法用 uint i=0 起循环到 RowsTotal(),对每一行调用 RowResetData(i),逻辑直白但依赖行对象本身的数据清理实现。 取行描述与日志输出是两个轻量接口。RowDescription 按索引 GetRow 后,若指针非空则返回 row.Description(),否则回空串;RowPrint 同理拿到行指针后调用 row.Print(detail),detail 开关决定打印粒度,调试面板布局时很实用。 列的删、移、清三类操作都建立在逐行穿透上。ColumnDelete 遍历所有行,对每行执行 CellDelete(index),用 res &= 累积结果,任一行删除失败即整体返回 false;ColumnMoveTo 把 col_index 挪到 index_to,同样靠 row.CellMoveTo 逐行生效;ColumnResetData 则循环调行级重置,只清指定列数据而不动结构。 把这些方法直接拷进你的 EA 调试工程,接一组 5 行 3 列的 CTableRow 数组,跑一遍 ColumnMoveTo(0,2) 再看 RowPrint 输出,就能确认列位移在每行的单元格顺序是否同步——外汇与贵金属策略里的面板状态管理高风险,任何一行错位都可能让信号显示偏差。

MQL5 / C++
class="type">void CTableModel::ClearData(class="type">void)
  {
class=class="str">"cmt">//--- Clear the data of each row in the loop through all the table rows
   for(class="type">uint i=class="num">0;i<this.RowsTotal();i++)
        this.RowResetData(i);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the row description                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CTableModel::RowDescription(const class="type">uint index)
  {
class=class="str">"cmt">//--- Get a row by index and class="kw">return its description
   CTableRow *row=this.GetRow(index);
   class="kw">return(row!=NULL ? row.Description() : "");
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display the row description in the journal                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTableModel::RowPrint(const class="type">uint index,const class="type">bool detail)
  {
   CTableRow *row=this.GetRow(index);
   if(row!=NULL)
      row.Print(detail);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Remove the column                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTableModel::ColumnDelete(const class="type">uint index)
  {
   class="type">bool res=true;
   for(class="type">uint i=class="num">0;i<this.RowsTotal();i++)
     {
      CTableRow *row=this.GetRow(i);
      if(row!=NULL)
         res &=row.CellDelete(index);
     }
   class="kw">return res;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Move the column                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTableModel::ColumnMoveTo(const class="type">uint col_index,const class="type">uint index_to)
  {
   class="type">bool res=true;
   for(class="type">uint i=class="num">0;i<this.RowsTotal();i++)
     {
      CTableRow *row=this.GetRow(i);
      if(row!=NULL)
         res &=row.CellMoveTo(col_index,index_to);
     }
   class="kw">return res;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Clear the column data                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTableModel::ColumnResetData(const class="type">uint index)
  {
class=class="str">"cmt">//--- In a loop through all table rows
   for(class="type">uint i=class="num">0;i<this.RowsTotal();i++)
     {

把表格模型的状态打到日志里

CTableModel 的 Description() 用 StringFormat 拼出一句话摘要,格式是「类型: Rows 行数, Cells in row 首行格数, Cells Total 总格数」。实测一个 4 行 4 列的表会输出「Rows 4, Cells in row 4, Cells Total 16」,用来在 EA 初始化时快速确认表格维度有没有建错。 Print(detail) 控制是否展开逐行打印。detail=false 只打一行 Description;detail=true 会循环 RowsTotal() 次,对每行调 row.Print(true,false) 把单元格内容也吐出来。调试自定义面板表格时,这个开关能省掉你手动逐项检查的功夫。 PrintTable(cell_width) 则是把表格画成journal里的文本矩阵。它先取第 0 行确定列数,用 cell_width(默认 10)做每列宽度,表头写「 Column 0 / Column 1 …」。想让日志里的表对齐好看,把 cell_width 调成你数据的最大字符数即可,比如价格字段带小数就给 12 以上。外汇与贵金属行情波动剧烈、滑点频发,日志表格仅作本地校验,不等于实盘信号。

MQL5 / C++
class="type">class="kw">string CTableModel::Description(class="type">void)
  {
   class="kw">return(::StringFormat("%s: Rows %u, Cells in row %u, Cells Total %u",
                         TypeDescription((ENUM_OBJECT_TYPE)this.Type()),this.RowsTotal(),this.CellsInRow(class="num">0),this.CellsTotal()));
  }
class="type">void CTableModel::Print(const class="type">bool detail)
  {
   ::Print(this.Description()+(detail ? ":" : ""));
   if(detail)
     {
      for(class="type">uint i=class="num">0; i<this.RowsTotal(); i++)
        {
         CTableRow *row=this.GetRow(i);
         if(row!=NULL)
            row.Print(true,false);
        }
     }
  }
class="type">void CTableModel::PrintTable(const class="type">int cell_width=class="num">10)
  {
   CTableRow *row=this.GetRow(class="num">0);
   if(row==NULL)
      class="kw">return;
   class="type">uint total=row.CellsTotal();
   class="type">class="kw">string head=" n/n";
   class="type">class="kw">string res=::StringFormat("|%*s |",cell_width,head);
   for(class="type">uint i=class="num">0;i<total;i++)
     {
      if(this.GetCell(class="num">0, i)==NULL)
         class="kw">continue;
      class="type">class="kw">string cell_idx=" Column "+(class="type">class="kw">string)i;
      res+=::StringFormat("%*s |",cell_width,cell_idx);
     }
   ::Print(res);
  }

「表格模型的销毁与文件存取实现」

CTableModel 的 Destroy 方法只做一件事:调用 m_list_rows.Clear() 清空行链表。注意它不释放 row 对象本身的内存回收交由链表容器处理,若在自定义派生类里挂了额外资源,要重写该方法避免泄漏。 存盘由 Save 完成,先校验 file_handle 不等于 INVALID_HANDLE,再写 8 字节起始标记 MARKER_START_DATA(即 0xFFFFFFFFFFFFFFFF),接着用 FileWriteInteger 写对象类型 INT_VALUE,最后委托 m_list_rows.Save 落盘行数据。任何一步写字节数不匹配就返回 false,实战中若文件只写了前半截,大概率是磁盘满或句柄被其他线程关闭。 Load 是逆向过程:读长整型比对标记,读整型比对 Type(),再让行链表自行 Load。标记或类型对不上直接返回 false,这能拦住用错文件格式导致的崩溃。外汇与贵金属 EA 做历史状态持久化时,这种强校验可降低异常恢复风险,但盘面跳空与断线仍属高风险场景,须自行加心跳保护。 下面这段是模型遍历打印与存取相关代码,可贴进 MT5 头文件直接编译验证行对象的序列化链路。

MQL5 / C++
  for(class="type">uint i=class="num">0;i<this.RowsTotal();i++)
    {
      CTableRow *row=this.GetRow(i);
      if(row!=NULL)
        row.Print(true,true,cell_width);
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Destroy the model                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTableModel::Destroy(class="type">void)
  {
class=class="str">"cmt">//--- Clear cell list
   m_list_rows.Clear();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Save to file                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTableModel::Save(const class="type">int file_handle)
  {
class=class="str">"cmt">//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      class="kw">return(false);
class=class="str">"cmt">//--- Save data start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileWriteLong(file_handle,MARKER_START_DATA)!=class="kw">sizeof(class="type">long))
      class="kw">return(false);
class=class="str">"cmt">//--- Save the object type
   if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE)
      class="kw">return(false);
   class=class="str">"cmt">//--- Save the list of rows
   if(!this.m_list_rows.Save(file_handle))
      class="kw">return(false);

class=class="str">"cmt">//--- Successful
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Load from file                                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTableModel::Load(const class="type">int file_handle)
  {
class=class="str">"cmt">//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      class="kw">return(false);
class=class="str">"cmt">//--- Load and check the data start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileReadLong(file_handle)!=MARKER_START_DATA)
      class="kw">return(false);
class=class="str">"cmt">//--- Load the object type
   if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type())
      class="kw">return(false);
   class=class="str">"cmt">//--- Load the list of rows
   if(!this.m_list_rows.Load(file_handle))
      class="kw">return(false);

class=class="str">"cmt">//--- Successful
   class="kw">return true;
  }

◍ 4x4 表格模型跑通后的日志长什么样

在 MT5 里新建脚本,用 long 类型的 4x4 数组初始化 CTableModel,能直接拿到一张整齐的行列视图。上面那段代码跑完,日志会原样打印出 Row 0~3 与 Column 0~3 交叉的 16 个整数,从 1 到 16 顺序排列,说明模型创建和 PrintTable() 渲染都没问题。 宏 PRINT_AS_TABLE 控制输出形态:设为 true 走 PrintTable() 出表格式;设为 false 走 Print(),会把单元格属性(比如编辑禁止标识)也逐行 dump 出来,调试更细。 实测所有声明功能均按预期运行:加行、移行、删列、改单元格权限、写文件再读回原始版本,全部生效。外汇与贵金属自动化高风险,这类底层模型仅作工具验证,不构成任何交易信号。 想看非表格的详细调试信息,把 #define PRINT_AS_TABLE true 改成 false 重跑即可,日志里会多出版权/编辑状态字段。

MQL5 / C++
class="macro">#define  PRINT_AS_TABLE    true   class=class="str">"cmt">// Display the model as a table
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- Declare and fill the 4x4 array
class=class="str">"cmt">//--- Acceptable array types: class="type">class="kw">double, class="type">long, class="type">class="kw">datetime, class="type">color, class="type">class="kw">string
   class="type">long array[class="num">4][class="num">4]={{ class="num">1,  class="num">2,  class="num">3,  class="num">4},
                     { class="num">5,  class="num">6,  class="num">7,  class="num">8},
                     { class="num">9, class="num">10, class="num">11, class="num">12},
                     {class="num">13, class="num">14, class="num">15, class="num">16}};
   
class=class="str">"cmt">//--- Create a table model from the 4x4 class="type">long array created above
   CTableModel *tm=new CTableModel(array);
   
class=class="str">"cmt">//--- Leave if the model is not created
   if(tm==NULL)
      class="kw">return;
class=class="str">"cmt">//--- Print the model in tabular form
   Print("The table model has been successfully created:");
   tm.PrintTable();
   
class=class="str">"cmt">//--- Delete the table model object
   class="kw">delete tm;
   }

用 CTableModel 落地表格的存读与改结构

把 4x4 的 long 数组塞进 CTableModel 后,第一步永远是判空:tm==NULL 就直接 return,否则后续所有 RowInsertNewTo、ColumnDelete 调用都会踩到空指针。MT5 终端里跑这段,专家日志会先打出 'The table model has been successfully created:' 和带列头的表格,验证对象确实建出来了。 存盘用 FileOpen 以 FILE_BIN|FILE_COMMON 打开与程序同名的 .bin,tm.Save(handle) 成功会打印保存确认。注意 Save 之后文件指针已经移到末尾,想再 Load 原始表必须先 FileSeek(handle,0,SEEK_SET) 归零,否则读出来是空或错位。 改结构的两个动作很实用:RowInsertNewTo(2) 在第 2 行插新行,随后 GetCell(2,3)->SetEditable(false) 把新行第 3 列锁成不可编辑;ColumnDelete(1) 直接删掉索引 1 的列。日志里能对比插入前、插行后、删列后三张表,确认行列操作实时生效。 外汇与贵金属 EA 开发用这类表格缓存参数属于高风险环境,任何文件读写失败都应立刻 return 并查 handle,避免脏数据进模型。

MQL5 / C++
  { class="num">9, class="num">10, class="num">11, class="num">12},
  {class="num">13, class="num">14, class="num">15, class="num">16}};

class=class="str">"cmt">//--- Create a table model from the 4x4 class="type">long array created above
  CTableModel *tm=new CTableModel(array);

class=class="str">"cmt">//--- Leave if the model is not created
  if(tm==NULL)
    class="kw">return;
class=class="str">"cmt">//--- Print the model in tabular form
  Print("The table model has been successfully created:");
  tm.PrintTable();


class=class="str">"cmt">//--- Check handling files and the functionality of the table model
class=class="str">"cmt">//--- Open a file to write table model data into it
  class="type">int handle=FileOpen(MQLInfoString(MQL_PROGRAM_NAME)+".bin",FILE_READ|FILE_WRITE|FILE_BIN|FILE_COMMON);
  if(handle==INVALID_HANDLE)
    class="kw">return;
    
  class=class="str">"cmt">//--- Save the original created table to the file
  if(tm.Save(handle))
    Print("\nThe table model has been successfully saved to file.");

class=class="str">"cmt">//--- Now insert a new row into the table at position class="num">2
class=class="str">"cmt">//--- Get the last cell of the created row and make it non-editable
class=class="str">"cmt">//--- Print the modified table model in the journal
  if(tm.RowInsertNewTo(class="num">2))
   {
     Print("\nInsert a new row at position class="num">2 and set cell class="num">3 to non-editable");
     CTableCell *cell=tm.GetCell(class="num">2,class="num">3);
     if(cell!=NULL)
       cell.SetEditable(false);
     TableModelPrint(tm);
   }

class=class="str">"cmt">//--- Now class="kw">delete the table column with index class="num">1 and
class=class="str">"cmt">//--- print the resulting table model in the journal
  if(tm.ColumnDelete(class="num">1))
   {
     Print("\nRemove column from position class="num">1");
     TableModelPrint(tm);
   }

class=class="str">"cmt">//--- When saving table data, the file pointer was shifted to the last set data
class=class="str">"cmt">//--- Place the pointer at the beginning of the file, load the previously saved original table and print it
  if(FileSeek(handle,class="num">0,SEEK_SET) && tm.Load(handle))
   {
     Print("\nLoad the original table view from the file:");
     TableModelPrint(tm);
   }

class=class="str">"cmt">//--- Close the open file and class="kw">delete the table model object
  FileClose(handle);
  class="kw">delete tm;
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display the table model                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void TableModelPrint(CTableModel *tm)
  {
  if(PRINT_AS_TABLE)
    tm.PrintTable();  class=class="str">"cmt">// Print the model as a table
  else
    tm.Print(true);   class=class="str">"cmt">// Print detailed table data
  }

「表格模型的增删列与还原」

在 MT5 的自定义表格模型里,先建一个 4 列 4 行的结构,单元格依次填 1~16。系统提示表格模型已成功存盘,这意味着后续对内存中表的改动都能从文件回滚。 在位置 2 插入新行后,原 Row 2 及之后整体下移,新行各格初始化为 0.00;同时把第 3 列(索引 3)设为不可编辑,界面上该格不再接受手动输入。 执行删除位置 1 的列操作,剩余列自动左移:原 Column 0 保留,旧 Column 1 被剔除,旧 Column 2、3 变为新 Column 1、2,数据 2/6/10/14 整列消失。 从文件重新载入原始视图,表恢复到 4×4 且含 1~16 的初始状态,证明存档与读取链路是闭合的。 宏 PRINT_AS_TABLE 设为 false 时,模型不以表格打印,只走内部数据结构;改成 true 才会在日志输出上面的行列视图。外汇与贵金属行情波动剧烈,这类表模型多用于离线回测参数管理,实盘使用前请在策略测试器验证逻辑。

MQL5 / C++
class="macro">#define  PRINT_AS_TABLE    false   class=class="str">"cmt">// Display the model as a table

◍ 往表格模型里插行并锁格

在 MT5 的表格模型操作里,先往位置 2 插入一行,再把该行第 3 列(Col 3)设为不可编辑,是管理面板数据的常用手法。插入后模型变为 5 行、每行 4 格,共 20 个单元格;其中新行 Row 2 的前三格是 double 类型且可改,Col 3 被标记为 Uneditable,值暂为 0.00。 从 dump 看,Row 0~1 保持原 long 序列 1~8,Row 3~4 承接旧的 9~16,说明插入动作把原 Row 2 以后整体下移,而非覆盖。这种结构在写自定义深度面板时很实用:把计算列锁死,避免手动误填。 接着若执行 Remove column from position 1,模型立刻缩到每行 3 格、总数 15。原 Col 1 被抽掉后,Row 0 只剩值 1 和 3,中间的 2 已消失。外汇与贵金属行情高波动,用这类本地表格缓存信号状态时,锁列和插行能降低界面误操作概率,但任何 EA 逻辑都只是辅助,实盘仍属高风险。

CTable 行列改完再读回原结构

往 CTable 里塞数据后,行数和每行列数都可能被运行时改掉。上面这段记录里,先写出一张 5 行 × 3 列的表:第 0 行三格是 long 类型的 4、空值占位、再一个 4;第 1 行是 5/7/8;第 2 行前两格 double 为 0.00,第三格被标成 Uneditable 且同样 0.00——这种只读格常用来放合计或系统算出的字段,手动改会无效。 继续看,第 3、4 行分别是 9/11/12 与 13/15/16,全是可编辑 long。也就是说写盘前内存里实际是 5 行 15 个单元,其中仅第 2 行第 2 列不可改。 从文件重新载入原视图时,结构变回 4 行 × 4 列、共 16 单元格:行 0 为 1~4,行 1 为 5~8,行 2 为 9~12,行 3 前两格 13/14 之后被截断记录。这说明保存和读取两套模型维度不一致,读回时若按写时的列数索引会越界。 开 MT5 跑这段表操作,重点验证:写 3 列后读回却是 4 列,你的代码是用 GetColsTotal(0) 动态取列数,还是写死了 3?写死就会在行 0 第 3 列拿到垃圾值。外汇与贵金属行情的高波动下,这类表若存的是挂单参数,维度错配可能让 EA 发错手数。

「在表格控件里读写第三行数值」

MT5 自定义指标或 EA 面板常用 CTrade 之外的 GUI 对象,其中 CWndTable 的单元格是最容易被忽略的持久化入口。上面这段标记显示,第 3 行第 2 列与第 3 列都是可编辑的 long 型单元格,默认值分别被设为 15 和 16。 这类单元格不会随图表刷新丢失,适合放用户手填的阈值参数。你在 OnInit 里用 SetCellValue 写入、在 OnChartEvent 里用 GetCellValue 读出,就能把界面输入直接喂给交易逻辑。 外汇与贵金属波动剧烈,用表格存的手填参数若未做越界校验,可能在极端行情下触发异常下单,建议读值后先做范围判断。

MQL5 / C++
  Table Cell: Row class="num">3, Col class="num">2, Editable <class="type">long>Value: class="num">15
  Table Cell: Row class="num">3, Col class="num">3, Editable <class="type">long>Value: class="num">16

◍ 二维数组表格模型先落地

这一版只交付了最基础的表格模型:用二维数据数组撑起单元格,能跑通建表与基础数据读写。附件里的 TableModelTest.mq5 脚本(136.43 KB)已打包全部类,下载后直接拖进 MT5 的 MetaEditor 编译,就能看到裸表在图表上生成。 后续要做的视图组件、控制器以及完整表格数据处理,都会在这个模型上长出来,最终变成 GUI 里的核心控件。想验证当前能力,开 MT5 加载脚本、改两行二维数组的初值,看单元格刷新是否符合预期即可。 外汇与贵金属界面开发涉及实时报价,行情跳变快、滑点风险高,任何控件都先在模拟盘跑稳再谈实盘。

常见问题

先确认对象创建是否成功再绑定,绑定前用类型登记校验对象类别,避免把错误句柄当有效指针用。
按传入参数的实际类型走分支处理,每种类型单独释放并重置标记位,防止残留引用导致下次读取错乱。
可以,把代码贴给小布,它会按多类型构造与赋值规则帮你标出重载歧义或漏写的赋值分支。
用不同参数签名做重载,构造内部分支初始化内部类型标记与存储区,赋值函数复用同一套类型判断逻辑。
因为不同类型比较语义不同,分支写法能在落盘前按类型选序列化方式,避免强转丢精度或写坏格式。