基于MQL5中表模型的表类和表头类:应用MVC概念·进阶篇
🗂️

基于MQL5中表模型的表类和表头类:应用MVC概念·进阶篇

(2/3)·只建好单元格和行还不够,缺了表头类的表连增删列都做不到

偏理论进阶 第 2/3 篇
不少人在 MQL5 里把表模型跑通就停手,以为二维数组能渲染出完整表格。没有表头类的表只是静态死表,想加一列得改代码重编译。本篇要补上的正是这一步能力缺口。

「表格单元格对象的取值接口」

在 MT5 自建表格控件时,每个单元格通常被封装成一个 C++ 风格的类对象,对外只暴露只读的取值方法,避免在渲染层误改数据源。 下面这段接口声明给出了单元格的核心元数据:行号 m_row、列号 m_col 通过 Row() 与 Col() 返回 uint;Datatype() 回传 ENUM_DATATYPE 枚举,Digits() 给出价格小数位(如黄金报价的 2 位或外汇的 5 位)。 IsEditable() 标记该格是否允许终端用户直接改值,ColorNameFlag() 与 DatetimeFlags() 则控制颜色名解析和时间格式标记,这些开关直接影响 CTable 绘制时的表现。 真正取数走三个类型化方法:ValueD() 拿 double(报价、指标值),ValueL() 拿 long(成交量、tick 数),另有字符串通道未在节选内展开。写 EA 时若跨格读数,先用 Datatype() 判断分支再调对应 Value,能少踩一半运行时错误。

MQL5 / C++
class="type">uint Row(class="type">void) const { class="kw">return this.m_row; }
class="type">uint Col(class="type">void) const { class="kw">return this.m_col; }
ENUM_DATATYPE Datatype(class="type">void) const { class="kw">return this.m_datatype; }
class="type">int Digits(class="type">void) const { class="kw">return this.m_digits; }
class="type">uint DatetimeFlags(class="type">void) const { class="kw">return this.m_time_flags; }
class="type">bool ColorNameFlag(class="type">void) const { class="kw">return this.m_color_flag; }
class="type">bool IsEditable(class="type">void) const { class="kw">return this.m_editable; }
class="type">class="kw">double ValueD(class="type">void) const { class="kw">return this.m_datatype_value.ValueD(); }
class="type">long ValueL(class="type">void) const { class="kw">return this.m_datatype_value.ValueL(); }

◍ 通用变量类的取值与类型描述实现

在 MT5 的自定义通用数据容器里,取值不能只靠一个函数硬转,得按内部标记 m_datatype 分流。下面这段实现把 double / long / datetime / color / string 五种类型的输出格式化全收进一个 Value() 里,外部调用时不用关心底层存的是什么。

MQL5 / C++
class="type">class="kw">string ValueS(class="type">void) const { class="kw">return this.m_datatype_value.ValueS(); }
class=class="str">"cmt">//--- Return the value as a formatted class="type">class="kw">string
class="type">class="kw">string Value(class="type">void) const
  {
   class="kw">switch(this.m_datatype)
     {
      case TYPE_DOUBLE  : class="kw">return(this.ValueD()!=DBL_MAX  ? ::DoubleToString(this.ValueD(),this.Digits()) : "");
      case TYPE_LONG    : class="kw">return(this.ValueL()!=LONG_MAX ? ::IntegerToString(this.ValueL()) : "");
      case TYPE_DATETIME: class="kw">return(this.ValueL()!=LONG_MAX ? ::TimeToString(this.ValueL(),this.m_time_flags) : "");
      case TYPE_COLOR   : class="kw">return(this.ValueL()!=LONG_MAX ? ::ColorToString((class="type">class="kw">color)this.ValueL(),this.m_color_flag) : "");
      class="kw">default           : class="kw">return this.ValueS();
     }
  }
class=class="str">"cmt">//--- Return a description of the stored value type
class="type">class="kw">string DatatypeDescription(class="type">void) const
  {
   class="type">class="kw">string type=::StringSubstr(::EnumToString(this.m_datatype),class="num">5);
   type.Lower();
   class="kw">return type;
  }
逐行拆一下关键点:ValueS() 是字符串类型的直出通道,不经过 switch;Value() 里对 double 判断是否为 DBL_MAX(MQL5 中约等于 1.7976931348623158e308 的浮点上限),不是才用 Digits() 给的精度转字符串,否则返空串避免脏数据。 long 系三类(整数、时间、颜色)统一用 LONG_MAX 做“未赋值”哨兵,时间走 TimeToString 并带 m_time_flags 控制格式,颜色强转 color 后走 ColorToString 带 m_color_flag。default 兜底回退到 ValueS(),保证任意未知类型不崩。 DatatypeDescription() 取 EnumToString 生成的 "TYPE_DOUBLE" 这类串,砍掉前 5 个字符得 "DOUBLE" 再 Lower() 成 "double"。你在 EA 里若想打印变量元信息,直接挂这个成员就能拿到小写类型名,省得自己维护映射表。外汇与贵金属行情跳动快,这类容器若用于实时缓存报价,需注意 LONG_MAX / DBL_MAX 哨兵与实际极值冲突的可能风险。

MQL5 / C++
class="type">class="kw">string ValueS(class="type">void) const { class="kw">return this.m_datatype_value.ValueS(); }
class=class="str">"cmt">//--- Return the value as a formatted class="type">class="kw">string
class="type">class="kw">string Value(class="type">void) const
  {
   class="kw">switch(this.m_datatype)
     {
      case TYPE_DOUBLE  : class="kw">return(this.ValueD()!=DBL_MAX  ? ::DoubleToString(this.ValueD(),this.Digits()) : "");
      case TYPE_LONG    : class="kw">return(this.ValueL()!=LONG_MAX ? ::IntegerToString(this.ValueL()) : "");
      case TYPE_DATETIME: class="kw">return(this.ValueL()!=LONG_MAX ? ::TimeToString(this.ValueL(),this.m_time_flags) : "");
      case TYPE_COLOR   : class="kw">return(this.ValueL()!=LONG_MAX ? ::ColorToString((class="type">class="kw">color)this.ValueL(),this.m_color_flag) : "");
      class="kw">default           : class="kw">return this.ValueS();
     }
  }
class=class="str">"cmt">//--- Return a description of the stored value type
class="type">class="kw">string DatatypeDescription(class="type">void) const
  {
   class="type">class="kw">string type=::StringSubstr(::EnumToString(this.m_datatype),class="num">5);
   type.Lower();
   class="kw">return type;
  }

表格行与参数对象的类骨架

在 MT5 自定义表格控件的实现里,CTableRow 继承自 CObject,内部用 CListObj 维护一行中的所有单元格,并用 m_index 记录行号。注意 m_cell_tmp 只是一个用于列表查找的临时 CTableCell 对象,并不参与渲染,纯粹降低遍历时的重复构造开销。 行索引的读写被做成内联方法:SetIndex() 直接赋值 m_index,Index() 加 const 修饰返回,避免在回测或实时刷新时意外篡改行序。CellsPositionUpdate() 则负责把当前行号与列号一次性同步给所有单元格,保证 GUI 重绘时坐标不漂移。 CellAddNew() 提供了 5 个重载版本,分别接收 double / long / datetime / color / string,意味着你往一行里追加数据时可以不用手动 new 再 AddNewCell(),直接丢原始类型进去就能拿到新建的 CTableCell 指针。实际写 EA 面板时,这套重载能省掉大量类型转换样板代码。 CMqlParamObj 紧跟在表格行定义之后,同样继承 CObject,但原文此处只开了 protected 和 public 区段,具体成员被截断。如果你要在自己的指标里序列化 MqlParam 结构,可以先照这个空壳把类建出来,再补 MqlParam 的 type/integer_value/double_value/string_value 字段。

MQL5 / C++
class="type">void ClearData(class="type">void) { this.SetEmptyValue(); }
class="kw">virtual class="type">class="kw">string Description(class="type">void);
class="type">void Print(class="type">void);
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      *CellAddNew(const class="type">class="kw">double value);
  CTableCell      *CellAddNew(const class="type">long value);
  CTableCell      *CellAddNew(const class="type">class="kw">datetime value);
  CTableCell      *CellAddNew(const class="type">class="kw">color value);
  CTableCell      *CellAddNew(const class="type">class="kw">string value);
};
class CMqlParamObj : class="kw">public CObject
{
class="kw">protected:
class="kw">public:

「用包装类接管 MqlParam 的读写」

在 MT5 里直接操作 MqlParam 结构体容易漏字段,写个轻量包装类把 type 与三个值字段一次性接管更稳。下面这段把外部传入的 param 完整拷进成员 m_param,四个字段一个不落。 Description() 里先用 EnumToString 拿到类型名,再从偏移 5 截取掉前缀 'TYPE_' 并转小写,比如 TYPE_DOUBLE 变成 double。随后按 type 分流取值:字符串走 ValueS(),浮点/双精度走 DoubleToString(ValueD()),整型分支在原文截断处未展示但由 ValueL() 兜住。 实盘调用前,建议在 MT5 的 EA 初始化里用一组已知参数跑一次 Description(),肉眼核对输出字符串是否符合 'double: 1.500000' 这种形态,能提前暴露字段赋值错位。外汇与贵金属杠杆高,参数对象若传错类型可能导致下单量异常,验证通过再接信号逻辑。

MQL5 / C++
  class="type">MqlParam         m_param;
class=class="str">"cmt">//--- Set the parameters
   class="type">void             Set(const class="type">MqlParam &param)
                     {
                     this.m_param.type=param.type;
                     this.m_param.double_value=param.double_value;
                     this.m_param.integer_value=param.integer_value;
                     this.m_param.string_value=param.string_value;
                     }
class=class="str">"cmt">//--- Return the parameters
   class="type">MqlParam         Param(class="type">void)     const { class="kw">return this.m_param;         }
   ENUM_DATATYPE   Datatype(class="type">void)   const { class="kw">return this.m_param.type;    }
   class="type">class="kw">double           ValueD(class="type">void)    const { class="kw">return this.m_param.double_value; }
   class="type">long             ValueL(class="type">void)    const { class="kw">return this.m_param.integer_value;}
   class="type">class="kw">string           ValueS(class="type">void)    const { class="kw">return this.m_param.string_value; }
class=class="str">"cmt">//--- Object description
   class="kw">virtual class="type">class="kw">string   Description(class="type">void)
                     {
                     class="type">class="kw">string t=::StringSubstr(::EnumToString(this.m_param.type),class="num">5);
                     t.Lower();
                     class="type">class="kw">string v="";
                     class="kw">switch(this.m_param.type)
                       {
                        case TYPE_STRING  :  v=this.ValueS(); class="kw">break;
                        case TYPE_FLOAT  :   case TYPE_DOUBLE : v=::DoubleToString(this.ValueD()); class="kw">break;

◍ 把 MqlParam 包成可打印的单元格对象

在 MT5 里用 MqlParam 往指标缓冲或自定义结构塞数据时,裸参数无法直接看内容。下面这段把 TYPE_DATETIME 单独格式化成「日期 时分秒」,其余类型强转 string,再用 StringFormat 拼成 <类型>值 的形态,调试时一眼能读出每行数据。 CMqlParamObj 的析构为空,构造支持从已有 MqlParam 引用直接拷贝,说明它只是轻量包装,不持有独立生命周期资源。 DataListCreator 提供两个静态方法:AddNewRowToDataList 在总表 CList 里挂一个新行,失败返回 NULL;AddNewCellParamToRow 把包装好的单元格塞进行,row.Add 返回小于 0 时立刻 delete 防内存泄漏。外汇与贵金属 EA 跑这类链表前,务必在策略测试器开内存泄漏检查,杠杆品种滑点可能让行数暴涨。

MQL5 / C++
case TYPE_DATETIME:  v=::TimeToString(this.ValueL(),TIME_DATE|TIME_MINUTES|TIME_SECONDS); class="kw">break;
    class="kw">default           :  v=(class="type">class="kw">string)this.ValueL(); class="kw">break;
      }
      class="kw">return(::StringFormat("<%s>%s",t,v));
     }

class=class="str">"cmt">//--- Constructors/destructor
     CMqlParamObj(class="type">void){}
     CMqlParamObj(const class="type">MqlParam &param) { this.Set(param);  }
    ~CMqlParamObj(class="type">void){}
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class for creating lists of data                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class DataListCreator
  {
class="kw">public:
class=class="str">"cmt">//--- Add a new row to the CList list_data list
   class="kw">static CList      *AddNewRowToDataList(CList *list_data)
     {
       CList *row=new CList;
       if(row==NULL || list_data.Add(row)<class="num">0)
         class="kw">return NULL;
       class="kw">return row;
     }
class=class="str">"cmt">//--- Create a new CMqlParamObj parameter object and add it to CList
   class="kw">static class="type">bool       AddNewCellParamToRow(CList *row,class="type">MqlParam &param)
     {
       CMqlParamObj *cell=new CMqlParamObj(param);
       if(cell==NULL)
         class="kw">return false;
       if(row.Add(cell)<class="num">0)
         {
          class="kw">delete cell;
          class="kw">return false;

表格模型里的数据类型识别

CTableModel 继承自 CObject,内部用 m_list_rows 维护行对象列表,m_row_tmp 则作为列表检索时的临时行载体。 构造入口被重载了四次:模板方法吃二维数组、两个 uint 指定行列数建空表、matrix 直接灌入行数据、以及从现成 CList 引用构建。这种多源入口在写自定义报表面板时很实用,尤其当你从指标缓冲或 AI 特征矩阵直接塞数据的时候。 GetCorrectDatatype 这个函数把字符串类型名映射回 MQL5 的 ENUM_DATATYPE。逻辑很直白:bool/char/uchar/short/ushort/int/uint/long/ulong 全归 TYPE_LONG;float/double 归 TYPE_DOUBLE;其余走日期等分支。 在 MT5 里新建个 EA 把这段类骨架贴进去,改两行 type_name 测试,能立刻看到返回枚举值的变化,方便后续做单元格渲染分支。外汇与贵金属波动剧烈,任何模型输出都只是概率参考,实盘前务必在策略测试器跑历史数据。

MQL5 / C++
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="type">void            CreateTableModel(const class="type">uint num_rows,const class="type">uint num_columns);
  class="type">void            CreateTableModel(const matrix &row_data);
  class="type">void            CreateTableModel(CList &list_param);
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
         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">class="kw">float"|| type_name=="class="type">class="kw">double"                                      ?   TYPE_DOUBLE     :
         class=class="str">"cmt">//--- Date/time value
把列结构诊断交给小布
这些表类与表头类的初始化差异,小布盯盘的 AIGC 已内置对照示例,打开对应 MQL5 工程页就能直接比对写法,你只管挑适合自己数据流的那种。

常见问题

链表用 CList 嵌套存储,行与单元格可动态增删;二维数组结构固定,适合一次性灌入已知数据集。
表头类只是封装了列操作方法的控制层,底层表模型本身不依赖它,无表头时退化为不可变结构的静态表。
在脚本包含段加 #include <Tables\Tables.mqh> 并补前向声明,旧类声明可移除避免重复定义。
可以,小布盯盘的品种页内置了 MVC 表结构片段,描述行列需求即给出可编译的表类与表头类骨架。
本篇只落地模型与表头,视图与控制器依赖列控制接口,留到系列第 3 篇基于此处扩展。