MQL5中表格模型的实现:应用MVC概念·进阶篇
(2/3)· 从单元格到模型类,逐层拆解MQL5里MVC表格的实现路径与取舍
接上篇对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 字节那一行是否和你自己的对象头格式对得上。
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 出哪一种对象。
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 *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 整体读写,不能逐字段拆。
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 派生类即可验证编译通过。
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() 输出的联动变化。外汇与贵金属图表对象受报价跳空影响,这类读取逻辑在高波动时段可能返回非预期精度,属正常高风险现象。
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 对象若绑实时报价,仍需留意终端卡顿带来的刷新延迟风险。
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