基于MQL5中表模型的表类和表头类:应用MVC概念(基础篇)
📘

基于MQL5中表模型的表类和表头类:应用MVC概念(基础篇)

第 1/3 篇

用 MVC 拆开 MQL5 里的表模型

在 MQL5 里做复杂面板时,直接把数据和绘制混在一起会迅速失控。把表(CTable)和表头(CHeader)拆成独立类,再套一层 MVC 思路——模型管数据、视图管绘制、控制器管交互——能让代码在 MT5 里更好维护。 原文给出的示例发布于 2026 年 1 月 7 日 09:30,作者 Artyom Trishkin,示例页面显示 292 次浏览、0 条评论,说明这套表模型示例在发布初期偏冷门但结构完整。 表头类只负责列名、列宽与排序状态,表类持有行数据与单元格引用。两者通过事件ID通信,不在彼此内部写死绘制逻辑,这就是 MVC 在 MQL5 面板里的最小落地形态。

◍ 从静态表到可操控列头

上一篇里我们已经用 MVC 模板在 MQL5 里把单元格、行和表模型的类搭了出来,数据能以结构化方式进表。但那张表是写死的——列不能加、不能删、也不能改名字。 现在要补上表类和表头。表头不只是拿来显示列名的装饰,它是管列的工具:没有表头类,表能跑,但列结构就锁死了,只能做静态表。 要让列能被动态控制,得先给表模型加方法——改结构、增列、删列,这些方法留给表头类调用。这一步是后面视图和控制器的基础,也是做出能真正操作数据的界面的关键节点。

「把表模型拆成可插拔的构件」

原表模型只认二维数组,实战里经常碰见结构先定、数据后灌,或者数据本身就是动态链表的情况。这次给表模型类补了四套初始化入口:二维数组直构、空表预建(指定行列数)、矩阵灌入、以及从 CList 嵌套链表构建。后者用 CList 存行、行内再挂 CList 存单元格,动态增删不费劲。 把上一篇文章里散在脚本里的类收进 Tables.mqh,挪到 MQL5\Scripts\Tables\ 下统一管理。CListObj 的 CreateElement() 要能 new 出这些新类型,所以头里得加前向声明;宏里把单元格最小宽度锁在 19 字符,日期时间文本才不会把右边框顶歪、日志里表格错位。 空值不再写 0,字符串空行、实数 DBL_MAX、整数 LONG_MAX 视作空。CTableCell 受保护区加设置空值的方法,取格式化串时遇空返空行,清数据也调它而非置零。所有返回对象描述的方法改虚函数,CreateNewCell() 系列重命名为以 Cell 开头,行访问以 Row 开头,调起来不混。 CMqlParamObj 包一层 MqlParam,能塞进 CList 当属性单元。主 CList 是行数组,嵌套 CList 是单元格数组,历史交易列表就是三层结构:主表→每行交易→价格/量/时间等属性。DataListCreator 静态类负责快速拼这种列表,拼完丢给 CreateTableModel(CList &list_param) 即可还原表。 新方法里有个细节:double 单元格的显示精度从 CMqlParamObj 的 long 值取。建财务结果列时可顺带传精度,datetime 传显示标志、color 传颜色名标志,写进 integer_value 或 double_value 字段,不传则整列默认零。 列级操作补了三个方法:按索引插新列(先尾插再前移)、设整列数据类型、设整列精度。没表头的表是静态的,列控只在有表头时生效。下面这段是新增的构造与前向声明骨架,可直接抄进包含文件比对。

MQL5 / C++
<span class="keyword">class="type">void</span> CreateTableModel(T &amp;array[][]);
<span class="keyword">class="type">void</span> CreateTableModel(<span class="keyword">const</span> <span class="keyword">class="type">uint</span> num_rows, <span class="keyword">const</span> <span class="keyword">class="type">uint</span> num_columns);
<span class="keyword">class="type">void</span> CreateTableModel(<span class="keyword">const</span> <span class="keyword">matrix</span> &amp;row_data);
<span class="keyword">class="type">void</span> CreateTableModel(CList &amp;list_param);
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Include libraries&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;    |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="preprocessor">class="macro">#include </span>&lt;Arrays\List.mqh&gt;
<span class="comment">class=class="str">"cmt">//--- Forward declaration of classes</span>
<span class="keyword">class</span> CTableCell;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Table cell class</span>
<span class="keyword">class</span> CTableRow;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Table row class</span>
<span class="keyword">class</span> CTableModel;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Table model class</span>

表格对象的类型与常量定义

在 MT5 自定义表格控件的底层,先得把对象类型码和内存布局定清楚。下面这段声明把列头、表头、整表以及基于参数数组的表都抽象成了独立类,方便后续用统一链表管理。 宏里有个关键数字:CELL_WIDTH_IN_CHARS 被写死成 19,意味着每个单元格按 19 个等宽字符渲染,超出的内容在 GUI 上可能被截断,接数据前最好先估一下字段长度。 ENUM_OBJECT_TYPE 里从 10000 起跳,OBJECT_TYPE_TABLE_CELL 是 10000,依次加一,到 OBJECT_TYPE_TABLE_BY_PARAM 为止。这种偏移能避开 MT5 内置图形对象 ID,避免类型冲突。 CListObj::CreateElement 只露出个 switch 入口,实际分支会按 m_element_type 去 new 对应的上面那几个类。你打开 MT5 的 MetaEditor,搜 CListObj 就能看到完整分发逻辑,改表结构从这儿动手最稳。

MQL5 / C++
class CColumnCaption;                 class=class="str">"cmt">// Table column header class
class CTableHeader;                   class=class="str">"cmt">// Table header class
class CTable;                         class=class="str">"cmt">// Table class
class CTableByParam;                 class=class="str">"cmt">// Table class based on parameter array
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Macros                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Macros                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define  MARKER_START_DATA    -class="num">1     class=class="str">"cmt">// Data start marker in a file
class="macro">#define  MAX_STRING_LENGTH    class="num">128   class=class="str">"cmt">// Maximum length of a class="type">class="kw">string in a cell
class="macro">#define  CELL_WIDTH_IN_CHARS  class="num">19     class=class="str">"cmt">// Table cell width in characters
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Enumerations                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Enumerations                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_OBJECT_TYPE                class=class="str">"cmt">// Enumeration of object types
  {
   OBJECT_TYPE_TABLE_CELL=class="num">10000,     class=class="str">"cmt">// Table cell
   OBJECT_TYPE_TABLE_ROW,            class=class="str">"cmt">// Table row
   OBJECT_TYPE_TABLE_MODEL,          class=class="str">"cmt">// Table model
   OBJECT_TYPE_COLUMN_CAPTION,       class=class="str">"cmt">// Table column header
   OBJECT_TYPE_TABLE_HEADER,         class=class="str">"cmt">// Table header
   OBJECT_TYPE_TABLE,                class=class="str">"cmt">// Table
   OBJECT_TYPE_TABLE_BY_PARAM,       class=class="str">"cmt">// Table based on the parameter array data
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| List element creation method                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CObject *CListObj::CreateElement(class="type">void)
  {
class=class="str">"cmt">//--- Create a new object depending on the object type in m_element_type 
   class="kw">switch(this.m_element_type)

◍ 表格对象的工厂分发与单元格联合体存储

在 MT5 标准库的对象体系里,表格相关控件靠一个 switch 工厂来实例化:传入 OBJECT_TYPE_TABLE 这类枚举,就 new 出对应的 CTable、CTableHeader 或 CColumnCaption,未知类型直接返回 NULL,调用方必须判空。 下面这段是工厂里处理表格族的分支,高亮部分就是表格整体与表头、列标题的创建入口: CTableCell 用了一个 union 来塞下三种值:double、long 和定长 ushort 数组(MAX_STRING_LENGTH 上限)。同一块内存按类型解释,省掉继承多态的开销,但也要求写之前明确知道此刻存的是哪一类。 SetValueS 里调 StringToShortArray 把字符串压进 ushort_value,取值时 ShortArrayToString 再转回来,并做 TrimLeft / TrimRight 清掉首尾空白。若你自己在 EA 里复用这个联合体,注意 MAX_STRING_LENGTH 默认 255,超长串会被截断,回测时可能静默丢字符。 外汇与贵金属图表上挂这类自定义表格做信号监控,属于高风险操作环境,任何控件内存异常都可能拖慢 tick 处理,建议先在策略测试器里跑通再上实盘。

MQL5 / C++
   {
      case OBJECT_TYPE_TABLE_CELL      :  class="kw">return new CTableCell();
      case OBJECT_TYPE_TABLE_ROW       :  class="kw">return new CTableRow();
      case OBJECT_TYPE_TABLE_MODEL     :  class="kw">return new CTableModel();
      case OBJECT_TYPE_COLUMN_CAPTION  :  class="kw">return new CColumnCaption();
      case OBJECT_TYPE_TABLE_HEADER    :  class="kw">return new CTableHeader();
      case OBJECT_TYPE_TABLE           :  class="kw">return new CTable();
      case OBJECT_TYPE_TABLE_BY_PARAM  :  class="kw">return new CTableByParam();
      class="kw">default                          :  class="kw">return NULL;
   }
}
class CTableCell : class="kw">public CObject
  {
class="kw">protected:
class=class="str">"cmt">//--- Combining for storing cell values(class="type">class="kw">double, class="type">long, class="type">class="kw">string)
   union DataType
     {
      class="kw">protected:
      class="type">class="kw">double        double_value;
      class="type">long          long_value;
      class="type">class="kw">ushort        ushort_value[MAX_STRING_LENGTH];
      class="kw">public:
      class=class="str">"cmt">//--- Set values
      class="type">void          SetValueD(const class="type">class="kw">double value) { this.double_value=value;                    }
      class="type">void          SetValueL(const class="type">long value)   { this.long_value=value;                      }
      class="type">void          SetValueS(const class="type">class="kw">string value) { ::StringToShortArray(value,ushort_value);   }
      
      class=class="str">"cmt">//--- Return values
      class="type">class="kw">double        ValueD(class="type">void) const { class="kw">return this.double_value; }
      class="type">long          ValueL(class="type">void) const { class="kw">return this.long_value; }
      class="type">class="kw">string        ValueS(class="type">void) const
         {
          class="type">class="kw">string res=::ShortArrayToString(this.ushort_value);
          res.TrimLeft();
          res.TrimRight();

「单元格空值的底层赋值逻辑」

表格控件的单元格类在收尾处给出了成员变量定义,从 m_datatype_value 到 m_editable 共 11 个字段,分别承载数值、类型、绑定对象、行列坐标与显示精度等信息。其中 m_digits 控制浮点展示位数,m_time_flags 管日期格式,m_editable 决定该格是否允许终端内直接改写。 SetEmptyValue 是这套结构的最后一环:它按当前 m_datatype 分支处理“空”语义。整型、时间、颜色三类统一写 LONG_MAX(9223372036854775807),双精度写 DBL_MAX(约 1.7976931348623157e+308),避免和正常业务值撞车。 在 MT5 里自己写自定义表格时,直接复用这个分支能少踩很多坑——尤其别手填 0 或 -1 当空值,后面做排序或条件着色会出诡异结果。外汇与贵金属图表加载此类控件时波动刷新频繁,空值判定错乱可能让面板闪跳,属高风险操作环境,建议先在回测器外的脚本里单步验证。

MQL5 / C++
class=class="str">"cmt">//--- Variables
  DataType        m_datatype_value;                    class=class="str">"cmt">// Value
  ENUM_DATATYPE   m_datatype;                          class=class="str">"cmt">// Data type
  CObject        *m_object;                            class=class="str">"cmt">// Cell object
  ENUM_OBJECT_TYPE m_object_type;                      class=class="str">"cmt">// Object type in the cell
  class="type">int             m_row;                               class=class="str">"cmt">// Row index
  class="type">int             m_col;                               class=class="str">"cmt">// Column index
  class="type">int             m_digits;                            class=class="str">"cmt">// Data representation accuracy
  class="type">uint            m_time_flags;                        class=class="str">"cmt">// Date/time display flags
  class="type">bool            m_color_flag;                        class=class="str">"cmt">// Color name display flag
  class="type">bool            m_editable;                          class=class="str">"cmt">// Editable cell flag

class=class="str">"cmt">//--- Set "empty value"
  class="type">void            SetEmptyValue(class="type">void)
                    {
                      class="kw">switch(this.m_datatype)
                        {
                         case TYPE_LONG     :
                         case TYPE_DATETIME :
                         case TYPE_COLOR    : this.SetValue(LONG_MAX);  class="kw">break;
                         case TYPE_DOUBLE   : this.SetValue(DBL_MAX);    class="kw">break;

常见问题

把列头定义独立成表头类、数据存成表模型类,渲染只负责画,改列不改数据逻辑,面板响应更快。
表头类用可插拔构件存列配置,隐藏时只调列状态标记,表格对象按标记跳过对应单元格绘制。
可以,小布能读你的面板脚本,标出耦合在渲染里的数据逻辑,并给出表头类与表模型类的拆分建议。
用联合体存储时显式写空值常量,别留未初始化内存,绘制前判空再输出占位符。
先定义类型与常量枚举,工厂里用 switch 按常量返回对应对象,避免手填字符串漏拼写出错。