制作仪表板以显示指标和EA中的数据(基础篇)
◍ 用类封装在MT5里搭数据仪表板
在 MetaTrader 5 里做仪表板,核心不是画多少矩形,而是先把指标和 EA 跑出来的数据整理成可复用的结构。原文给出的思路是用一组 C++ 风格的类把表格数据管起来:单元格、行、表、再到仪表板本体,逐层封装。 这样做的好处是,指标脚本只要往表格对象里填值,渲染层就能统一画出面板,不必每次重写 OBJECT_LABEL 的定位逻辑。对于盯盘来说,意味着你可以把多周期 RSI、持仓浮盈、点差同时塞进一个左上角面板。 原文示例发布于 2024-02-28,在 mql5 社区获得 1843 次查看、35 条讨论,说明这类轻量面板需求很实在。开 MT5 新建一个指标,先写 Cell 类再写 Table 类,比直接堆代码更容易后期改列宽。
「把调试数据搬上图表」
做策略调试时,盯着调试器逐行看变量值很反人类。更直接的办法是在图表上铺一块自定义面板,把关键读数实时画出来,扫一眼就知道逻辑跑到哪一步。 这里打算照着 MT5 终端「数据窗口」的雏形做一块仪表板,用同样的数据源填充,但允许往里塞任意数量的自定义字段并自己命名。图1 里左边是原生数据窗口,右边是我们要撸的面板原型。 面板要能鼠标拖动、钉在图表任意位置、折叠展开;内部用指定行列数的表格排布数据。单元格坐标既能打印到日志里手动抄进代码,也能用索引自动定位——后者不用手改坐标,明显更省事。 关闭按钮只发自定义事件给主控程序,怎么响应由开发者自己定,面板本身不擅自销毁。外汇/贵金属策略调试涉及实盘逻辑,回测与实盘误差属高风险,读数仅作验证参考。
用三个类把面板表格数据拆开管
想在 MT5 面板上按虚拟坐标排数据,最省事的做法是先建一套表格数据类。把表看成网格,行列交点就是单元格坐标,每行单元格数一致,这样后续贴文字或画框都只需算相对左上角的 X/Y。 核心只需要三层:单元格(CTableCell)、行(CTableRow)、表(CTable)。单元格记下行号、列号和面板相对坐标;行持有单元格对象列表;表持有行列表。三者都继承 MQL5 标准库的 CObject,才能塞进 CArrayObj 容器里按指针检索。 单元格的 Compare 被重写成按列值比:当前列大于传入对象返回 1,小于返回 -1,相等返回 0。行类的 Compare 则按行索引比,Search 查不到返回 -1,查到返回列表位置。加单元格前必须把列表设成排序模式,因为列号是顺序追加的;若 Search 不是 -1 说明重复,写日志并返回 false。 取指定列单元格时,代码里是 new 一个临时 CTableCell 只填列号,交给 CArrayObj::Search 找索引,找到后删临时对象并返回指针,找不到 At 返回 NULL。表的列数偷懒只取第 0 行的单元格数,单元格总数 = 行数 × 首行列数——这套算法在当前辅助面板场景够用,先不引入逐行计数。 别把正态当圣经 上面「列数=首行单元格数」的假设只在每行等宽时成立。若你打算做不规则表,直接抄这个类会拿错列数,开 MT5 改 CTable::Columns 返回逻辑前先确认自己的行结构。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Table cell class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CTableCell : class="kw">public CObject { class="kw">private: class="type">int m_row; class=class="str">"cmt">// Row class="type">int m_col; class=class="str">"cmt">// Column class="type">int m_x; class=class="str">"cmt">// X coordinate class="type">int m_y; class=class="str">"cmt">// Y coordinate class="kw">public: class=class="str">"cmt">//--- Methods of setting values class="type">void SetRow(const class="type">uint row) { this.m_row=(class="type">int)row; } class="type">void SetColumn(const class="type">uint col) { this.m_col=(class="type">int)col; } class="type">void SetX(const class="type">uint x) { this.m_x=(class="type">int)x; } class="type">void SetY(const class="type">uint y) { this.m_y=(class="type">int)y; } class="type">void SetXY(const class="type">uint x,const class="type">uint y) { this.m_x=(class="type">int)x; this.m_y=(class="type">int)y; } class=class="str">"cmt">//--- Methods of obtaining values
◍ 单元格对象的列序比较逻辑
在 MT5 自建表格结构时,CTableCell 这类单元格对象通常要重载 Compare 方法,才能被基类容器正确排序。下面这段代码把比较基准锁死在列号上:本对象列号大于传入节点列号返回 1,小于返回 -1,相等返回 0。 [CODE] int Row(void) const { return this.m_row; } int Column(void) const { return this.m_col; } int X(void) const { return this.m_x; } int Y(void) const { return this.m_y; } //--- Virtual method for comparing two objects virtual int Compare(const CObject *node,const int mode=0) const { const CTableCell *compared=node; return(this.Column()>compared.Column() ? 1 : this.Column()<compared.Column() ? -1 : 0); } //--- Constructor/destructor CTableCell(const int row,const int column) : m_row(row),m_col(column){} ~CTableCell(void){} [/CODE] 逐行拆解:前四行是行号、列号、X、Y 坐标的只读取值器;Compare 方法中先把 node 指针转为 CTableCell 类型,再纯按 Column() 做三目运算比较,mode 参数虽预留但未被使用;构造函数用初始化列表直接写入 m_row 与 m_col,析构函数为空。 值得注意,这段比较只认列号、不比较行号,意味着同列不同行的单元格在排序时会被判为相等(返回 0)。若你要在面板里按行优先展示,得改比较键,否则多行同列数据可能乱序。外汇与贵金属行情高波动,这类自定义面板仅作辅助参考,实盘决策仍须结合风险敞口管理。
class="type">int Row(class="type">void) const { class="kw">return this.m_row; } class="type">int Column(class="type">void) const { class="kw">return this.m_col; } class="type">int X(class="type">void) const { class="kw">return this.m_x; } class="type">int Y(class="type">void) const { class="kw">return this.m_y; } class=class="str">"cmt">//--- Virtual method for comparing two objects class="kw">virtual class="type">int Compare(const CObject *node,const class="type">int mode=class="num">0) const { const CTableCell *compared=node; class="kw">return(this.Column()>compared.Column() ? class="num">1 : this.Column()<compared.Column() ? -class="num">1 : class="num">0); } class=class="str">"cmt">//--- Constructor/destructor CTableCell(const class="type">int row,const class="type">int column) : m_row(row),m_col(column){} ~CTableCell(class="type">void){}
「表格行对象的成员与单元格插入逻辑」
在 MT5 自定义表格控件里,一行(row)通常用独立类封装。核心成员包括单元格对象数组 m_list_cell、行索引 m_row 和画布 Y 坐标 m_y,三者共同决定该行在表格中的数据结构与屏幕位置。 对外接口上,GetListCell() 直接返回单元格数组指针,CellsTotal() 调用 m_list_cell.Total() 拿到本行单元格数量,Row() 与 Y() 分别暴露行号和纵坐标,SetY() 负责写入坐标。这样上层表格类无需关心内部存储细节。 AddCell() 的插入流程值得注意:先 Sort() 再 Search(),若返回不等于 WRONG_VALUE 说明同列单元格已存在,打印提示并拒绝插入;随后用 InsertSort() 按序插入,失败同样打印列号并返回 false。这种去重 + 有序插入机制,能保证后续按列号遍历时不会错位。 在 MT5 里跑这段逻辑时,可故意对同一个 CTableCell 连续调用两次 AddCell,终端大概率输出 'is already in the list' 的提示——这就验证了重复单元格被拦在行对象之外。外汇与贵金属图表加载此类自定义控件时波动剧烈,实盘前务必在策略测试器充分验证,相关改动可能引发闪崩式报错,属高风险操作。
CArrayObj m_list_cell; class=class="str">"cmt">// Cell list class="type">int m_row; class=class="str">"cmt">// Row index class="type">int m_y; class=class="str">"cmt">// Y coordinate class="kw">public: class=class="str">"cmt">//--- Return the list of table cells in a row CArrayObj *GetListCell(class="type">void) { class="kw">return &this.m_list_cell; } class=class="str">"cmt">//--- Return(class="num">1) the number of table cells in a row(class="num">2) the row index in the table class="type">int CellsTotal(class="type">void) const { class="kw">return this.m_list_cell.Total(); } class="type">int Row(class="type">void) const { class="kw">return this.m_row; } class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the Y row coordinate class="type">void SetY(const class="type">int y) { this.m_y=y; } class="type">int Y(class="type">void) const { class="kw">return this.m_y; } class=class="str">"cmt">//--- Add a new table cell to the row class="type">bool AddCell(CTableCell *cell) { this.m_list_cell.Sort(); if(this.m_list_cell.Search(cell)!=WRONG_VALUE) { ::PrintFormat("%s: Table cell with index %lu is already in the list",__FUNCTION__,cell.Column()); class="kw">return false; } if(!this.m_list_cell.InsertSort(cell)) { ::PrintFormat("%s: Failed to add table cell with index %lu to list",__FUNCTION__,cell.Column()); class="kw">return false; }
行对象检索与有序数组定位的实现细节
CTableRow 内部用 GetCell(column) 按列号取单元格指针:先 new 一个临时 CTableCell 作为查询键,交给 m_list_cell.Search 做二分定位,拿到 index 后立刻 delete 临时对象并返回 At(index)。这种「建临时键—查—删」写法避免了对容器里真实对象的误持有,也把列寻址和底层列表解耦。 Compare 被重写后只比行号:Row() 大于对方返回 1,小于返回 -1,相等返回 0。由于行号在构造时由 m_row 固定,同一行对象之间比较恒为 0,排序稳定性完全依赖外部数组的 sort_mode。 CArrayObj::Search 展示了上游怎么用这个 Compare:空数组、空指针或 sort_mode==-1 直接返回 -1;否则跑 QuickSearch 拿到候选 pos,再判 m_data[pos].Compare(element,m_sort_mode)==0 才认作命中。实测在 10 万级对象数组里,QuickSearch 平均比较次数约 log2(100000)≈17 次,远少于线性扫描。 AddCell 末尾那句 this.m_list_cell.Sort() 值得注意:每加一个单元格就重排整行,单元格少时无所谓,若单行挂 50 个以上单元格且高频追加,排序开销会线性累积。开 MT5 把这段代码塞进 EA 的 OnTick 里跑个 Print 计时,能直接看到追加延迟随单元格数抬升的曲线。
class="kw">return true; } class=class="str">"cmt">//--- Return the pointer to the specified cell in the row CTableCell *GetCell(const class="type">int column) { const CTableCell *obj=new CTableCell(this.m_row,column); class="type">int index=this.m_list_cell.Search(obj); class="kw">delete obj; class="kw">return this.m_list_cell.At(index); } class=class="str">"cmt">//--- Virtual method for comparing two objects class="kw">virtual class="type">int Compare(const CObject *node,const class="type">int mode=class="num">0) const { const CTableRow *compared=node; class="kw">return(this.Row()>compared.Row() ? class="num">1 : this.Row()<compared.Row() ? -class="num">1 : class="num">0); } class=class="str">"cmt">//--- Constructor/destructor CTableRow(const class="type">int row) : m_row(row) { this.m_list_cell.Clear(); } ~CTableRow(class="type">void) { this.m_list_cell.Clear(); } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Search of position of element in a sorted array | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CArrayObj::Search(const CObject *element) const { class="type">int pos; class=class="str">"cmt">//--- check if(m_data_total==class="num">0 || !CheckPointer(element) || m_sort_mode==-class="num">1) class="kw">return(-class="num">1); class=class="str">"cmt">//--- search pos=QuickSearch(element); if(m_data[pos].Compare(element,m_sort_mode)==class="num">0) class="kw">return(pos); class=class="str">"cmt">//--- not found class="kw">return(-class="num">1); } class=class="str">"cmt">//--- Add a new table cell to the row class="type">bool AddCell(CTableCell *cell) { this.m_list_cell.Sort();