MQL5中表格模型的实现:应用MVC概念·综合运用
- 图形对象绑定的空指针与类型登记
- 可变参数的类型分支与清空逻辑
- 表格单元格类的构造重载细节
- 表格单元格的多类型构造与赋值细节
- 表格单元格的多类型构造函数落地
- 单元格比较与落盘的分支写法
- 表格单元落盘与回读的校验链
- 单元格描述与表格行类的内部封装
- CTableRow 的单元格操作接口
- 表格行对象的持久化与类型识别
- 表格行类的链表内核与单元格管控
- 行对象里按类型造单元格的三个重载
- 给表格行追加单元格与写值的底层实现
- 表格行里单元格的增删与对象绑定
- 表格行对象的单元格同步与日志输出
- 行对象的日志输出与文件落盘
- 从文件还原一行表格对象的校验链
- 用链表搭一个可换数据类型的虚拟表
- 按类型名映射底层存储枚举
- 表格控件的单元格与行操作接口
- CTableModel 的方法接口与多类型构造
- 二维数组灌进表格模型的底层写法
- 表格模型的行插入与单元格赋值逻辑
- 表格单元格的精度与对象绑定接口
- 表格模型里单元格的增删与统计实现
- 表格模型里行操作的索引同步坑
- 表格模型里的列与行数据操作
- 把表格模型的状态打到日志里
- 表格模型的销毁与文件存取实现
- 4x4 表格模型跑通后的日志长什么样
- 用 CTableModel 落地表格的存读与改结构
- 表格模型的增删列与还原
- 往表格模型里插行并锁格
- CTable 行列改完再读回原结构
- 在表格控件里读写第三行数值
- 二维数组表格模型先落地
图形对象绑定的空指针与类型登记
在 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』,比裸崩好排多了。
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 都吃文件句柄,说明这套结构本就打算做持久化与集合排序——你复制去写指标时,直接补这几个方法体就能落库,不用再改上层调用。
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 单元格渲染异常。
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 派生类即可观察单元格状态切换。
{
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 单元格的存储分支是否如预期分离。
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 回读,行号列号对得上就说明这套序列化没坑。外汇与贵金属行情波动剧烈,任何本地缓存逻辑都要考虑中断风险。
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) 分支都建议在调用层记日志而非静默吞掉。
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 和每格的列号回写到单元格对象,避免表格重排后坐标错乱。外汇与贵金属图表上跑这类自定义表格,需意识到脚本异常可能引发对象泄漏,属高风险调试场景。
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,供上层做排序或查找时重写比较逻辑。
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,否则持久化会走基类空逻辑。
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 开销,属于高风险环境下的可控代价。
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_DATE | TIME_MINUTES | TIME_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_DATE | TIME_MINUTES | TIME_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 验证时间格式是否如预期。
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,可能拖慢主线程,建议用定时器降频。
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() 收尾。外汇与贵金属图表上做动态表头重排时,这类移动会触发重绘,高频调用须留意终端资源占用。
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 1 | value | 」的日志行,直接在 MT5 专家日志里看二维数据,不必另开文件。外汇与贵金属行情跳动快,这类轻量表格若用于实时盯盘,需注意高频 Print 可能拖慢 tick 处理。 |
|---|
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 写标记后查日志确认字节数,能立刻验证存储链路是否通。
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 就说明文件结构可能已被别的版本写过——外汇与贵金属行情存储涉及高频写入,这类校验能降低回放历史面板时的崩溃概率。
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 区只有一个临时行对象和行链表,模板方法负责从二维数组建表:
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
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()。外汇与贵金属行情高波动,用这类自定义表做信号缓存时,注意行数膨胀会带来内存与刷新延迟风险。
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 清空行数据但不删结构。外汇与贵金属行情波动剧烈,用这类面板做实时报价展示时须留意高频刷新带来的资源占用风险。
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 头文件里,改一维类型就能快速接自己的报价矩阵。
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),循环不会中断,只是那一行留空,这种容错在跑大矩阵时不容易让整个绘制崩掉。
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; }
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 用策略测试器单实例先验证。
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 再删列表项;删除失败可能因行索引已失效。外汇贵金属行情高波动,面板频繁增删单元格时注意检查返回值,降低闪退概率。
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 则准备把单元格描述打到日志。下面这段是原文核心方法,逐行拆一下逻辑。
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 线把旧信号行清掉重算,比整体重建表更安全。
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 输出,就能确认列位移在每行的单元格顺序是否同步——外汇与贵金属策略里的面板状态管理高风险,任何一行错位都可能让信号显示偏差。
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 以上。外汇与贵金属行情波动剧烈、滑点频发,日志表格仅作本地校验,不等于实盘信号。
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 头文件直接编译验证行对象的序列化链路。
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 重跑即可,日志里会多出版权/编辑状态字段。
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,避免脏数据进模型。
{ 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 才会在日志输出上面的行列视图。外汇与贵金属行情波动剧烈,这类表模型多用于离线回测参数管理,实盘使用前请在策略测试器验证逻辑。
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 读出,就能把界面输入直接喂给交易逻辑。 外汇与贵金属波动剧烈,用表格存的手填参数若未做越界校验,可能在极端行情下触发异常下单,建议读值后先做范围判断。
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 加载脚本、改两行二维数组的初值,看单元格刷新是否符合预期即可。 外汇与贵金属界面开发涉及实时报价,行情跳变快、滑点风险高,任何控件都先在模拟盘跑稳再谈实盘。