MQL5 中的策略可视化:在标准图表中展示优化结果·进阶篇
◍ 表格单元类的取数与绘制方法
在 MT5 自定义表格控件里,先把单元格的坐标与尺寸暴露成只读接口,后续布局才不会乱。下面这组 getter 直接回传成员变量:X()/Y() 给左上角坐标,Width()/Height() 给宽高,Text() 回传单元格内字符串,全部带 const 修饰避免误写。 真正把字画到画布上的是 TextOut()。它接收 CCanvas 指针、偏移量 x_shift/y_shift,以及可选的背景色、字体标志与对齐方式;若 canvas 为空则直接 return,防止空指针崩 EA。 方法内部先记住原有 FontFlags,再把背景色转成 ARGB——clrNONE 时默认 0x00FFFFFF 全透,否则走 ColorToARGB()。随后用 FillRectangle 以 (m_x+1, m_y+1) 到 (m_x+m_w-1, m_y+m_h-1) 的四至擦掉旧标签,再设字体标志写新文本。 开 MT5 建个 CCanvas 派生面板,把这段贴进单元格结构体,就能验证:背景传 clrNONE 时单元透明,传 clrBlack 时整格被填黑再出字。外汇与贵金属图表叠加自绘控件波动剧烈,实盘前请在策略测试器跑够样本。
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="type">int Width(class="type">void) const { class="kw">return this.m_w; } class="type">int Height(class="type">void) const { class="kw">return this.m_h; } class="type">class="kw">string Text(class="type">void) const { class="kw">return this.m_text; } class=class="str">"cmt">//--- Prints the text written in cell properties to the canvas, the pointer to which is passed to the method class="type">void TextOut(CCanvas *canvas, const class="type">int x_shift, const class="type">int y_shift, const class="type">color bg_color=clrNONE, const class="type">uint flags=class="num">0, const class="type">uint alignment=class="num">0) { if(canvas==NULL) class="kw">return; class=class="str">"cmt">//--- Remember current font flags class="type">uint flags_prev=canvas.FontFlagsGet(); class=class="str">"cmt">//--- Set background class="type">color class="type">uint clr=(bg_color==clrNONE ? 0x00FFFFFF : ::ColorToARGB(bg_color)); class=class="str">"cmt">//--- Fill in the cell with the set background class="type">color (erase the previous label) canvas.FillRectangle(this.m_x+class="num">1, this.m_y+class="num">1, this.m_x+this.m_w-class="num">1, this.m_y+this.m_h-class="num">1, clr); class=class="str">"cmt">//--- Set font flags canvas.FontFlagsSet(flags); class=class="str">"cmt">//--- Print text in the cell
表格控件的颜色与透明度拆解
在 MT5 自定义表格控件里,颜色不是直接写死的对象,而是靠位运算从 color 类型里抠出 R/G/B 分量。GetR 用 clr&0xff 取最低字节,GetG 右移 8 位再 &0xff,GetB 右移 16 位,三者都返回 0~255 的 double,方便后续做渐变或高亮计算。
CTableDataControl 类把前景色 m_fore_color 和透明度 m_alpha 分开存,SetAlpha 只改 uchar 成员不改颜色本体,渲染时再由 Canvas 层合成 ARGB。这样你可以在不重画单元格的前提下,单独调透明度做 hover 或选中态。
TextOut 绘制前用 ColorToARGB 把 m_fore_color 转成带通道的整数,画完立刻 FontFlagsSet 还原之前记忆的字体标志并 Update(false) 避免画面闪烁。外汇与贵金属图表叠加这类自绘控件时,注意高 CPU 占用风险,建议先在 EURUSD 1 分钟周期挂测试实例验证帧率。
canvas.TextOut(this.m_x+x_shift, this.m_y+y_shift, this.m_text, ::ColorToARGB(this.m_fore_color), alignment); class=class="str">"cmt">//--- Return previously memorized font flags and update canvas canvas.FontFlagsSet(flags_prev); canvas.Update(false); } class=class="str">"cmt">//--- A class="kw">virtual method for comparing two objects class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Table control class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CTableDataControl : class="kw">public CTableData { class="kw">protected: class="type">uchar m_alpha; class="type">color m_fore_color; class=class="str">"cmt">//--- Converts RGB to class="type">color class="type">color RGBToColor(const class="type">class="kw">double r,const class="type">class="kw">double g,const class="type">class="kw">double b) const; class=class="str">"cmt">//--- Writes RGB component values to variables class="type">void ColorToRGB(const class="type">color clr,class="type">class="kw">double &r,class="type">class="kw">double &g,class="type">class="kw">double &b); class=class="str">"cmt">//--- Returns class="type">color component(class="num">1) Red, (class="num">2) Green, (class="num">3) Blue class="type">class="kw">double GetR(const class="type">color clr) { class="kw">return clr&0xff; } class="type">class="kw">double GetG(const class="type">color clr) { class="kw">return(clr>>class="num">8)&0xff; } class="type">class="kw">double GetB(const class="type">color clr) { class="kw">return(clr>>class="num">16)&0xff; } class=class="str">"cmt">//--- Returns a new class="type">color class="type">color NewColor(class="type">color base_color, class="type">int shift_red, class="type">int shift_green, class="type">int shift_blue); class="kw">public: class=class="str">"cmt">//--- Returns a pointer to itself CTableDataControl*Get(class="type">void) { class="kw">return &this; } class=class="str">"cmt">//--- (class="num">1) Sets, (class="num">2) returns transparency class="type">void SetAlpha(const class="type">uchar alpha){ this.m_alpha=alpha; }
「表格控件的网格与绘制接口」
在 MT5 自定义指标里做可视化表格,CTableDataControl 这个类把底层绘制逻辑封装得很干净。它先暴露了 Alpha() 这个只读方法,直接返回成员 m_alpha,默认构造时 m_alpha 被置为 255,也就是完全不透明。 网格部分给了两个入口:DrawGrid 让你手动指定起始坐标 x、y 以及 header_h、rows、columns、row_size、col_size,适合嵌在已有布局里;DrawGridAutoFill 只吃 border、header_h、rows、columns 四个参数,按画布边界自动算单元格尺寸,快速铺满更省事。两个方法都支持 line_color 默认 clrNONE 和 alternating_color 默认 true,也就是隔行换色。 文字与色块分别由 DrawText 和 DrawRectangleFill 负责。DrawText 的 align 默认 0、width/height 默认 WRONG_VALUE,意味着不强制限定文本框;DrawRectangleFill 则必须传 alpha 通道,和前面 m_alpha 形成呼应——你可以让背景块半透明而不影响文字清晰度。 构造函数有两种:带 id 的版本会顺带把 m_fore_color 初始化成 clrDimGray,无参版本只设 m_alpha=255。析构为空,说明资源回收交给 CCanvas 外层管理。开 MT5 把这段声明贴进 EA 头文件,改一行 alternating_color=false 就能立刻看出隔行着色的差异。
class="type">uchar Alpha(class="type">void) const { class="kw">return this.m_alpha; } class=class="str">"cmt">//--- Draws(class="num">1) a background grid, (class="num">2) with automatic cell size class="type">void DrawGrid(CCanvas *canvas,const class="type">int x,const class="type">int y,const class="type">uint header_h,const class="type">uint rows,const class="type">uint columns,const class="type">uint row_size,const class="type">uint col_size, const class="type">color line_color=clrNONE,class="type">bool alternating_color=true); class="type">void DrawGridAutoFill(CCanvas *canvas,const class="type">uint border,const class="type">uint header_h,const class="type">uint rows,const class="type">uint columns,const class="type">color line_color=clrNONE,class="type">bool alternating_color=true); class=class="str">"cmt">//--- Prints(class="num">1) a text message, (class="num">2) a filled rectangle at the specified coordinates class="type">void DrawText(CCanvas *canvas,const class="type">class="kw">string text,const class="type">int x,const class="type">int y,const class="type">color clr=clrNONE,const class="type">uint align=class="num">0,const class="type">int width=WRONG_VALUE,const class="type">int height=WRONG_VALUE); class="type">void DrawRectangleFill(CCanvas *canvas,const class="type">int x,const class="type">int y,const class="type">int width,const class="type">int height,const class="type">color clr,const class="type">uchar alpha); class=class="str">"cmt">//--- Constructors/Destructor CTableDataControl(const class="type">uint id) : CTableData(id), m_fore_color(clrDimGray), m_alpha(class="num">255) {} CTableDataControl(class="type">void) : m_alpha(class="num">255) {} ~CTableDataControl(class="type">void) {} };
◍ 表格边框与交替行底色的绘制逻辑
在 MT5 自定义面板里画一张表,第一步不是填数据,而是把坐标和网格线定下来。这段构造函数把行高和列宽做了硬性下限保护:小于 2 像素一律拉回 2,避免后续 FillRectangle 因区域过窄直接不渲染。 表体左上角 X1 直接取传入的 x,X2 则按「列数 × 列宽」向右推;Y1 从表头高度 header_h 下方起算,Y2 再按「行数 × 行高」向下推。注意 SetCoords 调用时统一减了 header_h,说明内部坐标系统把表头区域单独剥离。 网格线颜色默认是 C'200,200,200'(浅灰),也可由 line_color 参数覆盖。若传 clrNONE 就用这个灰。画线前用 ColorToARGB 把颜色和透明度 m_alpha 打包,这是 Canvas 类绘图的标准做法。 alternating_color 开启后,偶数行(i%2==0)会先调 NewColor(clr,45,45,45) 把底色提亮,再用 FillRectangle 填一个比单元格内缩 1 像素的矩形。实测这个 +45 的 RGB 偏移在深色背景上能拉开约 18% 的亮度差,肉眼可辨但不刺眼。 每行创建 CTableRow 对象并 AddRow 挂到链表;失败就 delete 防内存泄漏,同时 SetY(row_y-header_h) 让行对象记录自己在表体内的相对偏移,后续命中测试或重绘都靠它。
const class="type">color line_color=clrNONE,class="type">bool alternating_color=true) { class=class="str">"cmt">//--- Clear all lists of the tabular data object(class="kw">delete cells from rows and all rows) this.Clear(); class=class="str">"cmt">//--- Row height cannot be less than class="num">2 class="type">int row_h=class="type">int(row_size<class="num">2 ? class="num">2 : row_size); class=class="str">"cmt">//--- Row width cannot be less than class="num">2 class="type">int col_w=class="type">int(col_size<class="num">2 ? class="num">2 : col_size); class=class="str">"cmt">//--- Left coordinate(X1) of the table class="type">int x1=x; class=class="str">"cmt">//--- Calculate X2 coordinate(on the right) depending on the number of columns and their width class="type">int x2=x1+col_w*class="type">int(columns>class="num">0 ? columns : class="num">1); class=class="str">"cmt">//--- Y1 coordinate is located under the panel header area class="type">int y1=(class="type">int)header_h+y; class=class="str">"cmt">//--- Calculate Y2 coordinate(from below) depending on the number of rows and their height class="type">int y2=y1+row_h*class="type">int(rows>class="num">0 ? rows : class="num">1); class=class="str">"cmt">//--- Set coordinates of the table this.SetCoords(x1,y1-header_h,x2,y2-header_h); class=class="str">"cmt">//--- Get class="type">color of grid lines of table, either by class="kw">default or passed to method class="type">color clr=(line_color==clrNONE ? C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27; : line_color); class=class="str">"cmt">//--- Draw table border canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha)); class=class="str">"cmt">//--- In a loop by table rows for(class="type">int i=class="num">0;i<(class="type">int)rows;i++) { class=class="str">"cmt">//--- calculate Y coordinate of next horizontal grid line(Y coordinate of next row of table) class="type">int row_y=y1+row_h*i; class=class="str">"cmt">//--- if the flag of "alternating" row colors is passed and the row is even if(alternating_color && i%class="num">2==class="num">0) { class=class="str">"cmt">//--- lighten background class="type">color of table and draw a background rectangle class="type">color new_color=this.NewColor(clr,class="num">45,class="num">45,class="num">45); canvas.FillRectangle(x1+class="num">1,row_y+class="num">1,x2-class="num">1,row_y+row_h-class="num">1,::ColorToARGB(new_color,this.m_alpha)); } class=class="str">"cmt">//--- Draw horizontal grid line of table canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha)); class=class="str">"cmt">//--- Create new table row object CTableRow *row_obj=new CTableRow(i); if(row_obj==NULL) { ::PrintFormat("%s: Failed to create table row object at index %lu",(class="type">class="kw">string)__FUNCTION__,i); class="kw">continue; } class=class="str">"cmt">//--- Add it to list of rows of tabular data object class=class="str">"cmt">//--- (if failed to add object, class="kw">delete created object) if(!this.AddRow(row_obj)) class="kw">delete row_obj; class=class="str">"cmt">//--- Set Y coordinate in created row object, given offset from panel header row_obj.SetY(row_y-header_h); } class=class="str">"cmt">//--- In loop by table columns
用循环给表格铺网格并挂单元格
在 MT5 的 Canvas 面板里画表格,核心是用两层 for 循环:外层按列数推进 X 坐标,内层按行数把单元格对象挂到每一行。下面这段逻辑里,列宽 col_w 乘以列序号 i 得到 col_x,一旦 col_x 超过画布右边界减 2 像素就 break,避免越界绘制。
for(class="type">int i=class="num">0;i<(class="type">int)columns;i++) { class=class="str">"cmt">//--- calculate X coordinate of next vertical grid line(X coordinate of next table column) class="type">int col_x=x1+col_w*i; class=class="str">"cmt">//--- If grid line has gone beyond panel, break cycle if(x1==class="num">1 && col_x>=x1+canvas.Width()-class="num">2) break; class=class="str">"cmt">//--- Draw vertical grid line of table canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha)); class=class="str">"cmt">//--- Get the number of created rows from tabular data object class="type">int total=this.RowsTotal(); class=class="str">"cmt">//--- In loop through table rows for(class="type">int j=class="num">0;j<total;j++) { class=class="str">"cmt">//--- get next row CTableRow *row=this.GetRow(j); if(row==NULL) class="kw">continue; class=class="str">"cmt">//--- Create new table cell CTableCell *cell=new CTableCell(row.Row(),i); if(cell==NULL) { ::PrintFormat("%s: Failed to create table cell object at index %lu",(class="type">class="kw">string)__FUNCTION__,i); class="kw">continue; } class=class="str">"cmt">//--- Add created cell to row class=class="str">"cmt">//--- (if failed to add object, class="kw">delete created object) if(!row.AddCell(cell)) { class="kw">delete cell; class="kw">continue; } class=class="str">"cmt">//--- In created cell object, set its X coordinate and Y coordinate from row object. cell.SetXY(col_x,row.Y()); cell.SetSize(col_w, row_h); } } class=class="str">"cmt">//--- Update canvas without redrawing chart canvas.Update(false); }
for(class="type">int i=class="num">0;i<(class="type">int)columns;i++) { class=class="str">"cmt">//--- calculate X coordinate of next vertical grid line(X coordinate of next table column) class="type">int col_x=x1+col_w*i; class=class="str">"cmt">//--- If grid line has gone beyond panel, break cycle if(x1==class="num">1 && col_x>=x1+canvas.Width()-class="num">2) break; class=class="str">"cmt">//--- Draw vertical grid line of table canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha)); class=class="str">"cmt">//--- Get the number of created rows from tabular data object class="type">int total=this.RowsTotal(); class=class="str">"cmt">//--- In loop through table rows for(class="type">int j=class="num">0;j<total;j++) { class=class="str">"cmt">//--- get next row CTableRow *row=this.GetRow(j); if(row==NULL) class="kw">continue; class=class="str">"cmt">//--- Create new table cell CTableCell *cell=new CTableCell(row.Row(),i); if(cell==NULL) { ::PrintFormat("%s: Failed to create table cell object at index %lu",(class="type">class="kw">string)__FUNCTION__,i); class="kw">continue; } class=class="str">"cmt">//--- Add created cell to row class=class="str">"cmt">//--- (if failed to add object, class="kw">delete created object) if(!row.AddCell(cell)) { class="kw">delete cell; class="kw">continue; } class=class="str">"cmt">//--- In created cell object, set its X coordinate and Y coordinate from row object. cell.SetXY(col_x,row.Y()); cell.SetSize(col_w, row_h); } } class=class="str">"cmt">//--- Update canvas without redrawing chart canvas.Update(false);
「网格坐标与交替底色怎么落进画布」
这段逻辑干的事很直白:把表格的行列几何关系算清楚,再一笔笔画到 canvas 上。先定网格线颜色,没传 line_color 就用灰阶 C'200,200,200',传了就用传进来的;border 大于 0 才单独画表格外框,否则直接拿面板边框当表框用。 行高按总高 greed_h 除以 rows 四舍五入,列宽同理用 greed_w 除以 columns。假设面板内表格区高 240 像素、画 6 行,round(240/6)=40,每行占用 40 像素——调 rows 参数会直接改变这个整除结果,行密了字就容易挤。 交替底色只在 alternating_color 开且行号为偶数时触发:用 NewColor(clr,45,45,45) 把基线提亮,再 FillRectangle 填进单元格内部(四边各缩 1 像素避开网格线)。偶数行提亮、奇数行留底,肉眼扫数据行时不容易看错排。 每行 new 一个 CTableRow 挂进列表,失败就 delete 并 PrintFormat 报错继续循环;row_obj.SetY(row_y-header_h) 把行对象 Y 坐标扣掉表头高,保证后面填文本时不会和标题区重叠。列循环里 i>0 才画竖线,第一根竖线复用边框,省一次绘制调用。
class=class="str">"cmt">//--- Get class="type">color of grid lines of table, either by class="kw">default or passed to method class="type">color clr=(line_color==clrNONE ? C&class="macro">#x27;class="num">200,class="num">200,class="num">200&class="macro">#x27; : line_color); class=class="str">"cmt">//--- If indentation from edge of panel is greater than zero, draw border of table class=class="str">"cmt">//--- otherwise, panel border acts as table border if(border>class="num">0) canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha)); class=class="str">"cmt">//--- Height of entire table grid class="type">int greed_h=y2-y1; class=class="str">"cmt">//--- Calculate row height depending on table height and number of rows class="type">int row_h=(class="type">int)::round((class="type">class="kw">double)greed_h/(class="type">class="kw">double)rows); class=class="str">"cmt">//--- In loop through number of rows for(class="type">int i=class="num">0;i<(class="type">int)rows;i++) { class=class="str">"cmt">//--- calculate Y coordinate of next horizontal grid line(Y coordinate of next row of table) class="type">int row_y=y1+row_h*i; class=class="str">"cmt">//--- if the flag of "alternating" row colors is passed and the row is even if(alternating_color && i%class="num">2==class="num">0) { class=class="str">"cmt">//--- lighten background class="type">color of table and draw a background rectangle class="type">color new_color=this.NewColor(clr,class="num">45,class="num">45,class="num">45); canvas.FillRectangle(x1+class="num">1,row_y+class="num">1,x2-class="num">1,row_y+row_h-class="num">1,::ColorToARGB(new_color,this.m_alpha)); } class=class="str">"cmt">//--- Draw horizontal grid line of table canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha)); class=class="str">"cmt">//--- Create new table row object CTableRow *row_obj=new CTableRow(i); if(row_obj==NULL) { ::PrintFormat("%s: Failed to create table row object at index %lu",(class="type">class="kw">string)__FUNCTION__,i); class="kw">continue; } class=class="str">"cmt">//--- Add it to list of rows of tabular data object class=class="str">"cmt">//--- (if failed to add object, class="kw">delete created object) if(!this.AddRow(row_obj)) class="kw">delete row_obj; class=class="str">"cmt">//--- Set Y coordinate in created row object, given offset from panel header row_obj.SetY(row_y-header_h); } class=class="str">"cmt">//--- Width of table grid class="type">int greed_w=x2-x1; class=class="str">"cmt">//--- Calculate column width depending on table width and number of columns class="type">int col_w=(class="type">int)::round((class="type">class="kw">double)greed_w/(class="type">class="kw">double)columns); class=class="str">"cmt">//--- In loop by table columns for(class="type">int i=class="num">0;i<(class="type">int)columns;i++) { class=class="str">"cmt">//--- calculate X coordinate of next vertical grid line(X coordinate of next table column) class="type">int col_x=x1+col_w*i; class=class="str">"cmt">//--- If this is not the very first vertical line, draw it class=class="str">"cmt">//--- (the first vertical line is either table border or panel border) if(i>class="num">0) canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha));