利用 CCanvas 的自定义指标和信息图(基础篇)
用 CCanvas 在指标里画自定义图形
MT5 的自定义指标默认只能画线、画柱、画箭头,但借助标准库的 CCanvas 类,可以把指标窗口变成一块画布,直接绘制圆角线、六边形、花瓣、直方图、金字塔等任意矢量图形。2017 年 8 月发布的示例里,作者用 5 个封装类(CLineRounded、CHexagon、CPetal、CHistogram、CPyramid)在副图里拼出信息图,该帖当年浏览量 5774、评论 16 条,说明这类需求在量化社群里并不小众。 对交易者来说,价值不在「好看」,而在于把多维状态压进一个副图:比如用六边形表示波动率分位、用 Pyramid 表示多空持仓倾斜。你开 MT5 新建一个指标,include <Canvas/Canvas.h>,就能在 OnCalculate 里拿到 CCanvas 实例往图上写。 外汇与贵金属杠杆高、滑点随机,这类图形只是辅助观察,不能单独作为进出场依据,信号出现只代表概率倾斜。
◍ 从简单图元到伪3D图形库
前面几篇里我们用 CCanvas 类画过基础图元,原理是把指标逻辑直接映射到画布坐标。但原生图形库的能力远不止画线和画点,它支持带层级结构的复杂对象,也能在二维画布上做透视投影。 如果你只做过平面指标,建议去翻一下官方文档里带嵌套结构的例子,那里有更完整的对象生命周期管理写法。 本篇要落地的是两类东西:伪 3D 风格指标,以及随 tick 刷新的动态图。前者靠坐标变换模拟深度,后者靠每帧重绘保持实时性,两者都直接在 MT5 的自定义指标里可验证。
「从 CCanvasBase 派生圆形线性指标类」
做 MT5 自定义图形指标时,没必要重复造基类轮子。直接拿现成的 CustomGUI 函数库里的 CCanvasBase 当父类,在 CustomGUI/Indicators 目录下新建 LineRounded.mqh,写个 CLineRounded 类继承它,再把它 include 进 CustomGUI.mqh,后续所有库内类都能直接调。 这个线性指标不是单纯堆基本图元。Value 文字用 CCanvas::TextOut() 算简单元素;其余边框、底图、刻度实质都由两个实心圆(FillCircle)加一个矩形(FillRectangle)拼出来,属于复合元素。 绘制有严格层级:先画边框,再叠较小的底图,然后是指标刻度和数值。NewValue() 每次来新值,都先在方法开头重绘底图层,再按最大参数值定义的长度重画指标——因为元素是逐层覆盖的,不重绘底图旧长度会残留在画布上。 尺度差异很直观:两个指标当前值都取 50,但最大值一个设 100、另一个设 200,刻度长度会按比例自动适配,肉眼能直接比对出显示压缩感。外汇与贵金属图表加载此类自绘指标存在脚本报错、重绘闪烁等高风险,建议在模拟盘先验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| LineRounded.mqh | class=class="str">"cmt">//| 版权所有 class="num">2017, Alexander Fedosov | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "..\CanvasBase.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 圆形线性指标 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CLineRounded : class="kw">public CCanvasBase class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CustomGUI.mqh | class=class="str">"cmt">//| 版权所有 class="num">2017, Alexander Fedosov | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Indicators\CircleSimple.mqh" class="macro">#include "Indicators\CircleArc.mqh" class="macro">#include "Indicators\CircleSection.mqh" class="macro">#include "Indicators\LineGraph.mqh" class="macro">#include "Indicators\LineRounded.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建指标 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CLineRounded::Create(class="type">class="kw">string name,class="type">int x,class="type">int y) { class=class="str">"cmt">//--- 相对于坐标轴修改指标位置 x=(x<m_x_size/class="num">2)?m_x_size/class="num">2:x; y=(y<m_y_size/class="num">2)?m_y_size/class="num">2:y; Name(name); X(x); Y(y); XSize(m_x_size); YSize(m_y_size); if(!CreateCanvas()) Print("错误。不能创建画布。"); class=class="str">"cmt">//--- 边框
圆角进度条的绘制与数值刷新
在自定义指标面板里画一个两端半圆、中间矩形的进度条,核心就是先铺边框底图、再填内容层。下面这段把边框和背景分开画,左右两个 FillCircle 的半径都等于 YSize()/2,刚好拼出半圆形端帽,中间的 FillRectangle 负责直筒部分。 NewValue 函数每次来新数据就重画一遍:先用底图色覆盖旧刻度,再根据 value/maxvalue 的比例算出行走位置 v = int((XSize()-YSize()/2)/maxvalue*value)。v 落在区间内才画刻度矩形,超出右端就锁满格,小于左半圆半径则只亮左端圆——这决定了你的进度条在极端值下不会画出界。 文字用 TextOut 居中叠在 YSize()/2 处,DoubleToString 按 digits 控制小数位。外汇与贵金属行情跳动快,这种控件若挂在实时面板,建议 digits 跟品种报价精度一致,否则黄金(XAUUSD)显示 4 位小数会挤歪布局。 直接把下面代码丢进 MT5 的 CLineRounded 类就能跑,调 m_border 和 m_transparency 看描边和透明度变化最直观。
m_canvas.FillRectangle(YSize()/class="num">2,class="num">0,XSize()-YSize()/class="num">2,YSize(),ColorToARGB(m_border_color,m_transparency)); m_canvas.FillCircle(YSize()/class="num">2,YSize()/class="num">2-class="num">1,YSize()/class="num">2,ColorToARGB(m_border_color,m_transparency)); m_canvas.FillCircle(XSize()-YSize()/class="num">2-class="num">1,YSize()/class="num">2,YSize()/class="num">2,ColorToARGB(m_border_color,m_transparency)); class=class="str">"cmt">//--- 底图 m_canvas.FillRectangle(YSize()/class="num">2,m_border,XSize()-YSize()/class="num">2,YSize()-m_border,ColorToARGB(m_bg_color,m_transparency)); m_canvas.FillCircle(YSize()/class="num">2,YSize()/class="num">2,YSize()/class="num">2-m_border,ColorToARGB(m_bg_color,m_transparency)); m_canvas.FillCircle(XSize()-YSize()/class="num">2,YSize()/class="num">2,YSize()/class="num">2-m_border,ColorToARGB(m_bg_color,m_transparency)); m_canvas.Update(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置并更新指标数值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CLineRounded::NewValue(class="type">class="kw">double value,class="type">class="kw">double maxvalue,class="type">int digits=class="num">0) { class="type">int v; v=class="type">int((XSize()-YSize()/class="num">2)/maxvalue*value); class=class="str">"cmt">//--- 底图 m_canvas.FillRectangle(YSize()/class="num">2,m_border,XSize()-YSize()/class="num">2,YSize()-m_border,ColorToARGB(m_bg_color,m_transparency)); m_canvas.FillCircle(YSize()/class="num">2,YSize()/class="num">2,YSize()/class="num">2-m_border,ColorToARGB(m_bg_color,m_transparency)); m_canvas.FillCircle(XSize()-YSize()/class="num">2,YSize()/class="num">2,YSize()/class="num">2-m_border,ColorToARGB(m_bg_color,m_transparency)); class=class="str">"cmt">//--- 刻度 if(v>=YSize()/class="num">2 && v<=(XSize()-YSize()/class="num">2)) { m_canvas.FillRectangle(YSize()/class="num">2,m_border,v,YSize()-m_border,ColorToARGB(m_scale_color,m_transparency)); m_canvas.FillCircle(YSize()/class="num">2,YSize()/class="num">2,YSize()/class="num">2-m_border,ColorToARGB(m_scale_color,m_transparency)); m_canvas.FillCircle(v,YSize()/class="num">2,YSize()/class="num">2-m_border,ColorToARGB(m_scale_color,m_transparency)); } else if(v>(XSize()-YSize()/class="num">2)) { m_canvas.FillRectangle(YSize()/class="num">2,m_border,XSize()-YSize()/class="num">2,YSize()-m_border,ColorToARGB(m_scale_color,m_transparency)); m_canvas.FillCircle(YSize()/class="num">2,YSize()/class="num">2,YSize()/class="num">2-m_border,ColorToARGB(m_scale_color,m_transparency)); m_canvas.FillCircle(XSize()-YSize()/class="num">2,YSize()/class="num">2,YSize()/class="num">2-m_border,ColorToARGB(m_scale_color,m_transparency)); } else if(v>class="num">0 && v<YSize()/class="num">2) m_canvas.FillCircle(YSize()/class="num">2,YSize()/class="num">2,YSize()/class="num">2-m_border,ColorToARGB(m_scale_color,m_transparency)); class=class="str">"cmt">//--- 数值 m_canvas.FontSizeSet(m_font_size); m_canvas.TextOut(XSize()/class="num">2,YSize()/class="num">2,DoubleToString(value,digits),ColorToARGB(m_value_color,m_transparency),TA_CENTER|TA_VCENTER); m_canvas.Update(); } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 在图表上画两条可定位的参考线
MT5 自定义指标不一定非要在副图画曲线,借助 CustomGUI 库里的 CLineRounded 类,可以直接在主图叠加带坐标的视觉参考线,用来标记价格行为里的关键位。 下面这段代码在 OnInit 里建了两个线对象:ind1 尺寸 350×30,锚点放在主图坐标 (300,100);ind2 同样尺寸,但用 ScaleColor(clrFireBrick) 改成砖红色,锚点下移到 (300,150)。两个对象名分别为 line1、line2,互不冲突。 OnCalculate 里每次刷新都给两条线写入新值:ind1.NewValue(50,100,2) 与 ind2.NewValue(50,200,2),第三参数 2 是线宽。函数最后 return(rates_total) 而非 prev_calculated,属于该库示例的固定写法,实盘改逻辑时要注意重绘效率。 外汇与贵金属波动剧烈,这类画线仅作视觉辅助,不代表任何方向判定,实际信号需结合行情验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| class="num">001.mq5 | class=class="str">"cmt">//| 版权所有 class="num">2017, Alexander Fedosov | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property indicator_plots class="num">0 class="macro">#class="kw">property indicator_chart_window class="macro">#include <CustomGUI\CustomGUI.mqh> class=class="str">"cmt">//--- CLineRounded ind1,ind2; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- ind1.XSizeInd(class="num">350); class=class="str">"cmt">// 设置线对象1宽度350像素 ind1.YSizeInd(class="num">30); class=class="str">"cmt">// 设置线对象1高度30像素 ind1.Create("line1",class="num">300,class="num">100); class=class="str">"cmt">// 以line1为名,在主图(class="num">300,class="num">100)坐标创建 class=class="str">"cmt">//--- ind2.XSizeInd(class="num">350); class=class="str">"cmt">// 设置线对象2宽度350像素 ind2.YSizeInd(class="num">30); class=class="str">"cmt">// 设置线对象2高度30像素 ind2.ScaleColor(clrFireBrick); class=class="str">"cmt">// 线对象2染成砖红色 ind2.Create("line2",class="num">300,class="num">150); class=class="str">"cmt">// 以line2为名,在主图(class="num">300,class="num">150)坐标创建 class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); class=class="str">"cmt">// 初始化成功 } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标迭代函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">datetime &time[], const class="type">class="kw">double &open[], const class="type">class="kw">double &high[], const class="type">class="kw">double &low[], const class="type">class="kw">double &close[], const class="type">long &tick_volume[], const class="type">long &volume[], const class="type">int &spread[]) { class=class="str">"cmt">//--- ind1.NewValue(class="num">50,class="num">100,class="num">2); class=class="str">"cmt">// 线1写入值(class="num">50,class="num">100)线宽2 ind2.NewValue(class="num">50,class="num">200,class="num">2); class=class="str">"cmt">// 线2写入值(class="num">50,class="num">200)线宽2 class=class="str">"cmt">//--- 返回 prev_calculated 的数值以便下次调用 class="kw">return(rates_total); class=class="str">"cmt">// 返回总柱数 } class=class="str">"cmt">//+------------------------------------------------------------------+
「指标实例的清理收尾」
在 MT5 自定义指标里,OnDeinit 是终端卸载指标或切换周期时触发的逆初始化函数。若你在代码中用对象句柄创建了 ind1、ind2 这类指标实例,不在此时显式释放,MT5 会在日志里留下 orphaned indicator handle 警告,并占用部分内存直到图表重绘。 上面这段函数只做一件事:调用两个实例各自的 Delete 方法销毁资源。对外汇与贵金属交易而言,这类自动清理能降低多周期盯盘时的卡顿概率,但指标失效不代表行情风险消失,杠杆品种仍可能瞬间反向。 把这段直接贴进你的 .mq5 指标模板,下次编译后切周期观察一次,若日志不再报 handle 残留,说明回收路径通了。
class=class="str">"cmt">//| 自定义指标逆初函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { ind1.Delete(); ind2.Delete(); } class=class="str">"cmt">//+------------------------------------------------------------------+