制作仪表板以显示指标和EA中的数据·进阶篇
(2/3)· 想在图表上直观调试策略却嫌调试器太绕?用可拖拽可折叠的面板把数据摆出来
◍ 表格行与单元格的插入防重逻辑
在自定义表格类里,往行对象塞单元格之前必须先查重。下面这段用 m_list_cell.Search(cell) 比对已存单元,若返回值不等于 WRONG_VALUE 就说明同列单元已在列表中,直接打印函数名和列号并 return false,避免重复插入导致后续绘图索引错乱。 插入动作本身走 InsertSort(cell) 做有序插入,失败同样回 false 并带出列号日志;只有两步都过才 return true。外汇与贵金属图表上跑这类自定义控件时,对象泄漏或重复创建可能拖慢 MT5 终端,建议直接抄这段防重结构。 取单元格的 GetCell 写法也值得注意:它用 new CTableCell(this.m_row, column) 造一个临时对象去 Search,拿到 index 后立刻 delete obj,再返回 m_list_cell.At(index)。这种「临时对象查索引 + 即删」的手法能把堆上短命对象控制在函数作用域内,回测挂多表时内存波动倾向更平稳。
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; } 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); }
「表格行与单元格的增查逻辑」
在自建表格类里,AddRow 负责把一行塞进内部有序链表,并顺手做去重和插入校验。先调用 Sort() 保证链表有序,再用 Search() 查重——若返回的索引不是 WRONG_VALUE(即 -1),说明这行已存在,往日志打一句带行号的提示后直接 return false。
插入阶段走 InsertSort(row),失败同样打印含行索引的报错并返回 false;两步都过才返回 true。实测中若重复添加同一 CTableRow 指针,日志会出现类似 AddRow: Table row with index 3 is already in the list 的输出,可作为调试表格渲染异常的锚点。
取数侧两个方法很轻量:GetRow(int index) 直接返回 m_list_rows.At(index) 的指针;GetCell(row,column) 先拿行对象,拿不到就返回 NULL,后续由调用方判空。写 EA 面板时,建议对 GetCell 返回值做 NULL 保护,否则贵金属跳空行情下界面刷新可能抛野指针。
class="type">bool AddRow(CTableRow *row) { class=class="str">"cmt">//--- Set the sorted list flag this.m_list_rows.Sort(); class=class="str">"cmt">//--- If such an object is already in the list(the search returns the object index, not -class="num">1), class=class="str">"cmt">//--- inform of that in the journal and class="kw">return &class="macro">#x27;false&class="macro">#x27; if(this.m_list_rows.Search(row)!=WRONG_VALUE) { ::PrintFormat("%s: Table row with index %lu is already in the list",__FUNCTION__,row.Row()); class="kw">return false; } class=class="str">"cmt">//--- If failed to add the pointer to the sorted list, inform of that and class="kw">return &class="macro">#x27;false&class="macro">#x27; if(!this.m_list_rows.InsertSort(row)) { ::PrintFormat("%s: Failed to add table cell with index %lu to list",__FUNCTION__,row.Row()); class="kw">return false; } class=class="str">"cmt">//--- Successful - class="kw">return &class="macro">#x27;true&class="macro">#x27; class="kw">return true; } class=class="str">"cmt">//--- Return the pointer to the(class="num">1) specified row and(class="num">2) specified cell in the specified table row CTableRow *GetRow(const class="type">int index) { class="kw">return this.m_list_rows.At(index); } CTableCell *GetCell(const class="type">int row,const class="type">int column) { class=class="str">"cmt">//--- Get a pointer to a class="type">class="kw">string object in a list of strings CTableRow *row_obj=this.GetRow(row); class=class="str">"cmt">//--- If failed to get the object, class="kw">return NULL if(row_obj==NULL)
表格单元格坐标与维度的取法
在自定义表格类里,取某个单元格对象不能直接下标访问,得先走 GetCell 拿到指针,空就返回 NULL,这是后续所有坐标读取的前置逻辑。 CellXY 方法把 x、y 先预置为 WRONG_VALUE(通常是 -1),再尝试取 cell;若指针为空直接 return,调用方读到 -1 就能判断该行列越界或尚未绘制。 CellX 与 CellY 是精简版:用三元运算在 cell 非空时返回对应坐标,否则回 WRONG_VALUE。MT5 里表格若未挂载到图表,坐标大概率为 -1,开终端拖一个 CTable 实例就能验证。 行数通过 m_list_rows.Total() 取得,列数原文未在此贴出但同思路走列容器。外汇与贵金属图表上叠加这类 GUI 存在重绘开销,高频 tick 下可能拖慢 EA,属高风险自定义控件用法。
.class="kw">return NULL; class=class="str">"cmt">//--- Get the pointer to the cell object in the row by a column number and CTableCell *cell=row_obj.GetCell(column); class=class="str">"cmt">//--- class="kw">return the result(object pointer or NULL) class="kw">return cell; } class=class="str">"cmt">//--- Write the X and Y coordinates of the specified table cell into the variables passed to the method class="type">void CellXY(const class="type">uint row,const class="type">uint column, class="type">int &x, class="type">int &y) { x=WRONG_VALUE; y=WRONG_VALUE; CTableCell *cell=this.GetCell(row,column); if(cell==NULL) class="kw">return; x=cell.X(); y=cell.Y(); } class=class="str">"cmt">//--- Return the X coordinate of the specified table cell class="type">int CellX(const class="type">uint row,const class="type">uint column) { CTableCell *cell=this.GetCell(row,column); class="kw">return(cell!=NULL ? cell.X() : WRONG_VALUE); } class=class="str">"cmt">//--- Return the Y coordinate of the specified table cell class="type">int CellY(const class="type">uint row,const class="type">uint column) { CTableCell *cell=this.GetCell(row,column); class="kw">return(cell!=NULL ? cell.Y() : WRONG_VALUE); } class=class="str">"cmt">//--- Return the number of table(class="num">1) rows and(class="num">2) columns class="type">int RowsTotal(class="type">void) { class="kw">return this.m_list_rows.Total(); }
◍ 表格行列与清空的底层实现
在 MT5 自定义表格类里,列数不是独立存储的,而是借第一行来推算。ColumnsTotal() 先判断 RowsTotal() 是否为 0,若没有行直接返回 0;否则取第 0 行指针,返回该行的 CellsTotal()。这意味着只要首行结构正常,整表列数就跟着首行走。 CellsTotal() 更粗暴,直接拿 RowsTotal() 乘 ColumnsTotal() 得出单元格总量。假设某表有 12 行、每行 8 列,该函数会返回 96——你可以用 Print() 打出来核对,避免界面渲染和实际存储对不上。 Clear() 的写法值得抄:它不靠析构偷懒,而是先遍历 m_list_rows,对每一行取 GetListCell() 拿单元格列表逐个 Clear(),最后才清 m_list_rows。这样能确定性释放嵌套对象,在 EA 反复重建报价面板时不容易留野指针。 别把首行当恒成立 若你动态删过首行却没重排,ColumnsTotal() 可能返回后续行的列数甚至 0,界面列宽计算会瞬间错乱。开 MT5 在 OnDeinit 前手动调一次 Clear() 验证释放是否干净。
class="type">int ColumnsTotal(class="type">void) { class=class="str">"cmt">//--- If there is no row in the list, class="kw">return class="num">0 if(this.RowsTotal()==class="num">0) class="kw">return class="num">0; class=class="str">"cmt">//--- Get a pointer to the first row and class="kw">return the number of cells in it CTableRow *row=this.GetRow(class="num">0); class="kw">return(row!=NULL ? row.CellsTotal() : class="num">0); } class=class="str">"cmt">//--- Return the total number of cells in the table class="type">int CellsTotal(class="type">void){ class="kw">return this.RowsTotal()*this.ColumnsTotal(); } class=class="str">"cmt">//--- Clear lists of rows and table cells class="type">void Clear(class="type">void) { class=class="str">"cmt">//--- In the loop by the number of rows in the list of table rows, for(class="type">int i=class="num">0;i<this.m_list_rows.Total();i++) { class=class="str">"cmt">//--- get the pointer to the next row CTableRow *row=this.m_list_rows.At(i); if(row==NULL) class="kw">continue; class=class="str">"cmt">//--- get cell list from the obtained row object, CArrayObj *list_cell=row.GetListCell(); class=class="str">"cmt">//--- clear cell list if(list_cell!=NULL) list_cell.Clear(); } class=class="str">"cmt">//--- Clear cell list this.m_list_rows.Clear();
「把表格数据打到日志里看结构」
在 MT5 里做自定义表格类时,最忌讳盲写——你以为行数列数对上了,实际某行漏了单元格。下面这段 Print 方法就是用来把 CTableData 的内部行列结构原样吐到专家日志里的,跑一遍就知道哪行哪列有数据、坐标 X/Y 是多少。
Print 先打表头,用 PrintFormat 输出 RowsTotal() 和 ColumnsTotal() 两个值,例如一张 3 行 5 列的表会显示 Table: Rows: 3, Columns: 5。随后双层 for 循环按 r、c 遍历,GetCell(r,c) 拿到单元格指针,非空才打印该行该列的 Row、Column 序号及 Cell X / Cell Y 坐标。
构造函数和析构函数都只做一件事:m_list_rows.Clear() 清空行链表,避免对象复用或销毁时残留上一轮的脏数据。RowsTotal() 直接返回 m_list_rows.Total(),ColumnsTotal() 则先判断 RowsTotal()==0 就直接返 0——空表去查列数会踩坑,这个守卫不能省。
开 MT5 新建 EA 把这段塞进你的表格类,调用 Print() 后切到「专家」标签页,对照日志里的坐标就能确认渲染层画的格子跟逻辑层是不是同一套。外汇与贵金属行情跳动快,这类结构校验最好在模拟盘跑,实盘误用可能放大下单逻辑偏差。
class=class="str">"cmt">//--- Print the table cell data in the journal class="type">void Print(const class="type">uint indent=class="num">0) { class=class="str">"cmt">//--- Print the header in the journal ::PrintFormat("Table: Rows: %lu, Columns: %lu",this.RowsTotal(),this.ColumnsTotal()); class=class="str">"cmt">//--- In the loop by table rows for(class="type">int r=class="num">0;r<this.RowsTotal();r++) class=class="str">"cmt">//--- in the loop by the next row cells, for(class="type">int c=class="num">0;c<this.ColumnsTotal();c++) { class=class="str">"cmt">//--- get the pointer to the next cell and display its data in the journal CTableCell *cell=this.GetCell(r,c); if(cell!=NULL) ::PrintFormat("%*s%-5s %-4lu %-8s %-6lu %-8s %-6lu %-8s %-4lu",indent,"","Row",r,"Column",c,"Cell X:",cell.X(),"Cell Y:",cell.Y()); } } class=class="str">"cmt">//--- Constructor/destructor CTableData(class="type">void) { this.m_list_rows.Clear(); } ~CTableData(class="type">void) { this.m_list_rows.Clear(); } }; class="type">int RowsTotal(class="type">void) { class="kw">return this.m_list_rows.Total(); } class="type">int ColumnsTotal(class="type">void) { class=class="str">"cmt">//--- If there is no row in the list, class="kw">return class="num">0 if(this.RowsTotal()==class="num">0)
表格单元格总数的两种取法
在自绘 CTable 控件里,想拿总行数下的单元格数量,有两种写法。一种是直接拿第一行指针,若行存在就返回该行的列数,否则返回 0;另一种更粗暴,用行数乘以列数一次性算总数。
后一种 CellsTotal 写法 return this.RowsTotal()*this.ColumnsTotal(); 假设行列规整,若某行缺列会高估。前一种只认第一行,若后续行不等宽会低估。
在 MT5 里接这段逻辑时,先确认你的表是定宽定高再选乘法,否则遍历每行累加更稳。外汇与贵金属图表上挂这类面板,注意高频刷新下的内存抖动风险。
class="kw">return class="num">0; class=class="str">"cmt">//--- Get a pointer to the first row and class="kw">return the number of cells in it CTableRow *row=this.GetRow(class="num">0); class="kw">return(row!=NULL ? row.CellsTotal() : class="num">0); } class="type">int CellsTotal(class="type">void){ class="kw">return this.RowsTotal()*this.ColumnsTotal(); }