利用 CCanvas 的自定义指标和信息图(基础篇)
📘

利用 CCanvas 的自定义指标和信息图(基础篇)

第 1/3 篇

用 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,刻度长度会按比例自动适配,肉眼能直接比对出显示压缩感。外汇与贵金属图表加载此类自绘指标存在脚本报错、重绘闪烁等高风险,建议在模拟盘先验证。

MQL5 / C++
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 看描边和透明度变化最直观。

MQL5 / C++
  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,属于该库示例的固定写法,实盘改逻辑时要注意重绘效率。 外汇与贵金属波动剧烈,这类画线仅作视觉辅助,不代表任何方向判定,实际信号需结合行情验证。

MQL5 / C++
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 残留,说明回收路径通了。

MQL5 / C++
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">//+------------------------------------------------------------------+

常见问题

用 CCanvas 类在指标里开一块画布,先画矩形、线、圆这些基础图元,再逐步叠成复杂图形,代码里直接调绘制函数即可。
重点重写绘制与刷新接口,构造时绑定好画布坐标,销毁前释放资源,避免指标重载时残留图形。
小布可以解析指标代码结构与图形刷新机制,用白话指出哪段在画什么、参数怎么调,省去自己硬读源码的时间。
多半是没在 OnCalculate 里调用重绘并传入新数值,确认每次 tick 都清画布再画进度条即可解决。
用坐标转换把价格和时间映射成画布像素,在品种或周期变更时重算位置并重绘,线就不会跑偏。