MQL5中表格模型的实现:应用MVC概念·进阶篇
🧱

MQL5中表格模型的实现:应用MVC概念·进阶篇

(2/3)· 从单元格到模型类,逐层拆解MQL5里MVC表格的实现路径与取舍

含代码示例偏理论 第 2/3 篇

接上篇对MVC范式的铺垫,我们继续深挖:在MQL5里把模型、视图、控制器落到具体类时,多数人卡在数据存储选型和对象边界划分上。用数组硬塞行列关系,后期加功能就会牵一发动全身。

◍ 从文件重建对象链表的底层实现

在 MT5 里做价格行为回测或形态缓存时,经常需要把一批图形对象(如趋势线、斐波那契)序列化落盘,再在下次加载时原样恢复。CListObj 这个类就是在标准 CList 之上,补了一层「按类型建节点 + 从文件读回」的能力,核心是两个虚函数:Load 和 CreateElement。 Load 方法先校验文件句柄不是 INVALID_HANDLE,再读一个 8 字节长整型做起始标记,必须是 0xFFFFFFFFFFFFFFFF(即 MARKER_START_DATA),否则直接返回 false。接着用 FileReadInteger 读列表类型并和当前 Type() 比对,读出的 uint num 就是对象个数——这一步决定了后面循环次数。 循环里每读一个元素,都先确认对象数据起始标记,再读 4 字节的对象类型赋给 m_element_type,随后调 CreateElement 生成空节点并 Add 进链表。注意代码里有一处关键偏移:读完标记和类型后文件指针已前进 12 字节(8+4),所以用 FileSeek(file_handle,-12,SEEK_CUR) 把指针拉回对象数据开头,再交给 node.Load 去填属性。外汇与贵金属市场波动剧烈、杠杆风险高,这类本地缓存机制只解决工程效率,不预示任何价格方向。 把下面这段直接贴进 MT5 的 MQH 就能看到结构骨架,验证时重点看 FileSeek 回退 12 字节那一行是否和你自己的对象头格式对得上。

MQL5 / C++
class CListObj : class="kw">public CList
  {
class="kw">protected:
   ENUM_OBJECT_TYPE   m_element_type;   class=class="str">"cmt">// Created object type in CreateElement()
class="kw">public:
class=class="str">"cmt">//--- Virtual method(class="num">1) for loading a list from a file, (class="num">2) for creating a list element
   class="kw">virtual class="type">bool      Load(const class="type">int file_handle);
   class="kw">virtual CObject   *CreateElement(class="type">void);
   };
class="type">bool CListObj::Load(const class="type">int file_handle)
  {
class=class="str">"cmt">//--- Variables
   CObject *node;
   class="type">bool     result=true;
class=class="str">"cmt">//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      class="kw">return(false);
class=class="str">"cmt">//--- Load and check the list start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileReadLong(file_handle)!=MARKER_START_DATA)
      class="kw">return(false);
class=class="str">"cmt">//--- Load and check the list type
   if(::FileReadInteger(file_handle,INT_VALUE)!=Type())
      class="kw">return(false);
class=class="str">"cmt">//--- Read the list size(number of objects)
   class="type">uint num=::FileReadInteger(file_handle,INT_VALUE);

class=class="str">"cmt">//--- Sequentially recreate the list elements by calling the Load() method of node objects
   this.Clear();
   for(class="type">uint i=class="num">0; i<num; i++)
     {
      class=class="str">"cmt">//--- Read and check the object data start marker - 0xFFFFFFFFFFFFFFFF
      if(::FileReadLong(file_handle)!=MARKER_START_DATA)
         class="kw">return false;
      class=class="str">"cmt">//--- Read the object type
      this.m_element_type=(ENUM_OBJECT_TYPE)::FileReadInteger(file_handle,INT_VALUE);
      node=this.CreateElement();
      if(node==NULL)
         class="kw">return false;
      this.Add(node);
      class=class="str">"cmt">//--- Now the file pointer is offset relative to the beginning of the object marker by class="num">12 bytes(class="num">8 - marker, class="num">4 - type)
      class=class="str">"cmt">//--- Set the pointer to the beginning of the object data and load the object properties from the file using the Load() method of the node element.
      if(!::FileSeek(file_handle,-class="num">12,SEEK_CUR))
         class="kw">return false;
      result &=node.Load(file_handle);
     }
class=class="str">"cmt">//--- Result
   class="kw">return result;
   }

按类型动态生成列表元素的工厂方法

在 MT5 自定义控件库里,CListObj 用 CreateElement 充当轻量工厂:它不接收外部类型参数,而是读自身成员变量 m_element_type 来决定 new 出哪一种对象。

MQL5 / C++
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)
     {
      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();
      class="kw">default                       :  class="kw">return NULL;
     }
  }
逐行拆解:函数返回基类指针 CObject*,保证列表能统一持有不同子类。switch 判断 this.m_element_type,三种枚举分别 new 出单元格、行、表模型;都不匹配时走 default 返回 NULL,调用方必须判空。 实盘写面板时,若 m_element_type 没先赋值就调 CreateElement,会稳定拿到 NULL——开 MT5 把这段塞进 EA 的 OnInit 前打印返回值即可验证。外汇与贵金属界面开发属高风险辅助环节,逻辑错误只影响显示,不改动订单。

MQL5 / C++
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)
     {
      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();
      class="kw">default                       :  class="kw">return NULL;
     }
  }

「单元格为何要用联合体存值」

表格单元格一次只装一个值,但类型可能在实数、整数、字符串之间切换。为了不浪费内存又能在同一块地址上换类型读写,这里直接用了 union 而不是 struct——联合体所有字段共享同一内存区域,struct 才给每个字段单独开空间。 类里受保护部分先定义了一个 DataType 联合体,里面塞了 double_value、long_value 和 ushort 数组(字符串拆成宽字符存)。配套写了 SetValueD / SetValueL / SetValueS 以及对应的 ValueD / ValueL / ValueS,字符串通过 StringToShortArray 进数组、ShortArrayToString 出数组并做左右 trim。 新单元格默认按实数类型初始化。设置值的方法会先改存储类型标记,再查“可编辑”标识位:没开编辑权限时,类型会被改但值不动,这能避免误写只读格。 现在的清理逻辑把数值填 0、字符串填空格,但 0 本身也是有效报价,清空显示成 0 不合理。后续打算加一个“空值”标记,让清空真正留白而不是拿 0 顶替。 参数化构造函数除了行列坐标,还要按类型收附加格式:双精度传小数位精度,datetime 传日期/时分/秒显示标识,颜色传是否显示标准色名。比较方法支持按列号、行号或两者同时比,这是后面控制器做行排序的依赖。文件存取时联合体必须用 FileWriteStruct / FileReadStruct 整体读写,不能逐字段拆。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Table cell class                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
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();
            class="kw">return res;
         }
     };
class=class="str">"cmt">//--- Variables

◍ 表格单元格类的私有字段与只读接口

在 MT5 自定义表格控件的底层实现里,一个单元格对象用一组私有成员记录自身状态。下面这段声明直接决定了后续读写、渲染和交互的逻辑边界。 DataType m_datatype_value 存具体数值,ENUM_DATATYPE m_datatype 标记它是整数、双精度还是时间类型;CObject* m_object 指向实际绘制的图形对象,m_object_type 则说明该对象在单元格中的类型。行号和列号由 m_row、m_col 两个 int 固定,m_digits 控制小数位精度,m_time_flags 管时间显示格式。 m_color_flag 决定颜色名是否显示,m_editable 标记单元格是否允许手动编辑——这两个 bool 直接影响用户在面板上的操作权限。外汇与贵金属图表上挂这类控件时,误开编辑可能引发高风险误操作,建议回测阶段默认关掉。 公开的 Row()、Col()、Datatype()、Digits() 都是 const 内联方法,只返回对应私有字段、不做修改。这种只读接口能避免外部逻辑意外篡改坐标,开 MT5 把下列代码塞进你的 CTable 派生类即可验证编译通过。

MQL5 / C++
  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="kw">public:
class=class="str">"cmt">//--- Return cell coordinates and properties
  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;         }

图形对象取值接口的底层封装

在 MT5 自定义指标或 EA 里读取图形对象属性时,很多新手直接调 ObjectGetDouble/ObjectGetString 却忽略了类型分支。下面这段类方法把时间标记、颜色标记、可编辑状态以及三种基础数据类型(double/long/string)的统一取值都收进了成员函数,调用层不用再写一堆 switch。 DatetimeFlags 返回 m_time_flags(控制时间字符串格式,如 TIME_DATE|TIME_MINUTES),ColorNameFlag 返回 m_color_flag(决定是否用颜色名而非十六进制),IsEditable 暴露对象是否允许终端用户拖动修改。这三个布尔/整型标记决定了 Value() 格式化时的表现。 ValueD/ValueL/ValueS 分别取出 double、long、string 类型的裸值;真正好用的是 Value() 这个聚合函数——它按 m_datatype 走 switch:TYPE_DOUBLE 用 DoubleToString 带 Digits() 精度,TYPE_LONG 转整数字符串,TYPE_DATETIME 借 TimeToString 套时间格式标记,TYPE_COLOR 用 ColorToString 套颜色标记,其余一律回退 ValueS()。 开 MT5 新建一个 CSampleObject 类把这段代码贴进去,挂到 OnChartEvent 里打印 Value(),你能直接看到拖一条趋势线时 IsEditable 与 Value() 输出的联动变化。外汇与贵金属图表对象受报价跳空影响,这类读取逻辑在高波动时段可能返回非预期精度,属正常高风险现象。

MQL5 / C++
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=class="str">"cmt">//--- Return(class="num">1) class="type">class="kw">double, (class="num">2) class="type">long and(class="num">3) class="type">class="kw">string value
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(); }
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(::DoubleToString(this.ValueD(),this.Digits()));
      case TYPE_LONG    :  class="kw">return(::IntegerToString(this.ValueL()));
      case TYPE_DATETIME:  class="kw">return(::TimeToString(this.ValueL(),this.m_time_flags));
      case TYPE_COLOR   :  class="kw">return(::ColorToString((class="type">color)this.ValueL(),this.m_color_flag));
      class="kw">default           :  class="kw">return this.ValueS();
     }
  }

「单元格对象的数据类型与定位赋值」

这段 MQL5 片段定义了一个单元格描述类里的几个关键成员,负责把表格里某个格子的位置、数据类型和显示属性固化下来。 DatatypeDescription() 先拿 EnumToString 把 m_datatype 转成字符串,再用 StringSubstr 从第 5 个字符开始截取,最后 Lower() 转小写返回——比如 ENUM_DATATYPE 里带 TYPE_ 前缀的枚举,就会被剥成纯净的小写类型名。 SetRow、SetCol 把 uint 强转 int 存进 m_row / m_col;SetPositionInTable 只是把这两个调用打包,一次写好行列坐标。其余 SetDatatype、SetDigits、SetDatetimeFlags、SetColorNameFlag、SetEditable 都是单行赋值,分别接管精度、时间格式旗标、颜色名开关和是否可编辑。 在 MT5 里建自定义表格面板时,这类封装能让你用 SetPositionInTable(2,3) 一句话把控件钉到第 3 行第 4 列,比散着写坐标更不容易错位。外汇与贵金属行情跳动快,这类 UI 对象若绑实时报价,仍需留意终端卡顿带来的刷新延迟风险。

MQL5 / C++
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;
  }
class=class="str">"cmt">//--- Set variable values
  class="type">void SetRow(const class="type">uint row)          { this.m_row=(class="type">int)row;          }
  class="type">void SetCol(const class="type">uint col)          { this.m_col=(class="type">int)col;          }
  class="type">void SetDatatype(const ENUM_DATATYPE datatype) { this.m_datatype=datatype; }
  class="type">void SetDigits(const class="type">int digits)     { this.m_digits=digits;         }
  class="type">void SetDatetimeFlags(const class="type">uint flags)        { this.m_time_flags=flags; }
  class="type">void SetColorNameFlag(const class="type">bool flag)         { this.m_color_flag=flag;  }
  class="type">void SetEditable(const class="type">bool flag)              { this.m_editable=flag;    }
  class="type">void SetPositionInTable(const class="type">uint row,const class="type">uint col)
    {
      this.SetRow(row);
      this.SetCol(col);
    }
class=class="str">"cmt">//--- Assign an object to a cell
交给小布盯盘看结构
这些MVC类的依赖关系小布盯盘的AIGC已内置,打开对应品种页即可看到调用链路,你专注决策逻辑而非底层拼装。

常见问题

标准库链表在插入删除行或单元格时不需要整体移位,模型层扩容更平滑;动态数组在频繁变更时开销更明显,且边界管理更易出错。
原文实现覆盖实数、整数与字符串三类,并配独立取值设值接口;若后续接入时间序列,可在模型层做类型映射而不动单元格基类。
单文件脚本式指标确实冗余,但涉及多周期表格、交互面板的可扩展EA,分离模型与视图后单独测试组件的概率更高、维护成本倾向更低。
目前小布盯盘内置的是结构诊断与调用视图,不代写完整MVC类;你可把文中类图贴入AIGC面板做差异比对,省去手工追依赖。
典型如点击添加行、编辑单元格后回写模型,控制器拦截ChartEvent并转交模型变更,再通知视图重绘,避免视图直接改数据。