利用 CCanvas 类开发自定义指标·进阶篇
🎨

利用 CCanvas 类开发自定义指标·进阶篇

(2/3)· 上篇铺垫了自定义绘图思路,本篇动手拆开 CCanvas 基类与圆形、弧线、线性四类图形的代码骨架

新手友好 第 2/3 篇
很多交易者以为 MQL5 自带的 18 种绘图样式够用了,遇到特殊视效就绕路走。其实 CCanvas 类能让你在指标里画任意自定义图形,只是缺一套可复用的类结构。本篇把基类到具体图形类的路径铺开,照着写就能省掉大量重复劳动。

「从零搭一个带数值的圆形指标类」

做自定义图形指标,先吃透一个最朴素的圆形结构:外圈框架、填充背景、中间数值文本、底部描述文字。这四层叠起来就是视觉骨架,后续再复杂的面板也是这个思路扩出来的。 在 CustomGUI 目录下新建 Indicator 文件夹,丢进 CircleSimple.mqh。文件头部只需要 #include "..\CanvasBase.mqh",让 CCircleSimple 以 CCanvasBase 为基类——这样画布基类的所有方法直接继承,不用重写。 顺手在 CustomGUI 里建一个 CustomGUI.mqh,把 Indicators 下所有类统一 include。以后 EA 或指标里只引这一个文件就能调全部图形类,少写一行手写引用就少一处出错可能。 画圆讲究层次顺序,和真实作画一样:先铺背景,再描外框,最后写数值和描述。框架用两个圆叠加实现——先画一个填充大圆,再画一个半径小一圈的圆抠出边宽,比直接描线更可控。 实时更新走 NewValue():必须按背景、数值、描述的原顺序重绘三个对象,否则图层会串。初始化时只改你关心的属性,其余走构造函数默认值,建实例仅用 CreateCanvas() 三个核心参数就够了,多余参数只会增加维护成本。 实测时在 MT5 建一个空指标,引 CustomGUI.mqh 并声明两个 CCircleSimple 实例,初始化里挂上标准指标作为数值源,计算函数里把当前值喂给 NewValue(),逆初始化里删图形资源防内存泄漏。跑出来两个圆指标参数差异明显,证明属性隔离是生效的。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 CircleSimple.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 CCircleSimple : 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=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 带有数值和描述的圆形指标                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CCircleSimple : class="kw">public CCanvasBase
  {
class="kw">private:
  class=class="str">"cmt">//--- 背景颜色
  class="type">color            m_bg_color;
  class=class="str">"cmt">//--- 框架颜色
  class="type">color            m_border_color;
  class=class="str">"cmt">//--- 文本颜色
  class="type">color            m_font_color;
  class=class="str">"cmt">//--- 标签颜色
  class="type">color            m_label_color;
  class=class="str">"cmt">//--- 透明度
  class="type">uchar            m_transparency;
  class=class="str">"cmt">//--- 框架宽度

圆形报价面板的成员变量与读写接口

在 MT5 自定义指标里画一个随价格浮动的圆形面板,先得把外观参数收进类私有成员。下面这段声明把边框宽度、半径、两套字号、小数位和标签文本都固化下来,后续绘制时直接读这些变量,避免每次重算。 私有段里 m_border 管边框像素,m_radius 是圆心到边缘的逻辑尺寸,m_font_size 与 m_label_font_size 分别控制数值和标签的字体大小,m_digits 决定报价保留几位小数,m_label 存面板旁的固定文字。 公开接口全用 C++ 风格 getter/setter 成对出现:Color() 无参返回背景色,带 color 参数则写入 m_bg_color;Radius() 读写 m_radius,默认不传参就能拿到当前半径。FontSize() 与 LabelSize() 同理,分别对接 m_font_size 和 m_label_font_size,你在 EA 里动态改 fontsize 就能实时缩放面板文字。 FontColor() 和 LabelColor() 管两路文字颜色,一个改数值色一个改标签色。实盘加载这类面板时,外汇与贵金属波动剧烈、滑点风险高,改完颜色或字号建议切到 MT5 可视化回放验证重叠是否遮挡 K 线。

MQL5 / C++
  class="type">int               m_border;
  class=class="str">"cmt">//--- 指标尺寸
  class="type">int               m_radius;
  class=class="str">"cmt">//--- 数值字号
  class="type">int               m_font_size;
  class=class="str">"cmt">//--- 标签字号
  class="type">int               m_label_font_size;
  class=class="str">"cmt">//---
  class="type">int               m_digits;
  class=class="str">"cmt">//--- 标签
  class="type">class="kw">string            m_label;
class="kw">public:
                    CCircleSimple(class="type">void);
                   ~CCircleSimple(class="type">void);
  class=class="str">"cmt">//--- 设置和获取背景颜色
  class="type">color             Color(class="type">void)                { class="kw">return(m_bg_color);                }
  class="type">void              Color(class="kw">const class="type">color clr)     { m_bg_color=clr;                     }
  class=class="str">"cmt">//--- 设置和获取尺寸
  class="type">int               Radius(class="type">void)               { class="kw">return(m_radius);                  }
  class="type">void              Radius(class="kw">const class="type">int r)        { m_radius=r;                         }
  class=class="str">"cmt">//--- 设置和获取数值字号
  class="type">int               FontSize(class="type">void)             { class="kw">return(m_font_size);               }
  class="type">void              FontSize(class="kw">const class="type">int fontsize){ m_font_size=fontsize;              }
  class=class="str">"cmt">//--- 设置和获取标签字号
  class="type">int               LabelSize(class="type">void)            { class="kw">return(m_label_font_size);         }
  class="type">void              LabelSize(class="kw">const class="type">int fontsize){ m_label_font_size=fontsize;       }
  class=class="str">"cmt">//--- 设置和获取数值字体颜色
  class="type">color             FontColor(class="type">void)            { class="kw">return(m_font_color);              }
  class="type">void              FontColor(class="kw">const class="type">color fontcolor) { m_font_color=fontcolor;         }
  class=class="str">"cmt">//--- 设置和获取标签字体颜色
  class="type">color             LabelColor(class="type">void)           { class="kw">return(m_label_color);            }

◍ 圆形指标面板的属性封装与构造默认值

这个类把圆形面板的视觉与数据属性全做成了 getter/setter 对内成员变量的读写,外部调用时不用碰底层变量,直接改颜色、边框、透明度即可。 LabelColor() 把文字色写进 m_label_color;BorderColor() 与 BorderSize() 分别接管框架颜色和线宽,线宽以像素为单位。Alpha() 有读写两个重载,返回或设定 m_transparency,取值范围 0~255,255 代表完全不透明。 Label() 同理存取 m_label 字符串,也就是圆心中央显示的文字内容。Create()、Delete()、NewValue() 这组接口负责在图表上建圆、删圆和刷新指标数值,NewValue 提供了 int 与 double 两种重载以适应不同品种报价精度。 构造器里给了一组可直接抄的默认值:背景 clrAliceBlue、边框 clrRoyalBlue、文字与标签色 clrBlack、透明度 255、边框宽 5、半径 40。外汇与贵金属图表上跑这类面板属高风险环境,参数仅作视觉参考,不代表任何方向判断。

MQL5 / C++
class="type">void                LabelColor(class="kw">const class="type">color fontcolor){ m_label_color=fontcolor;    }
class=class="str">"cmt">//--- 设置框架颜色和宽度
class="type">void                BorderColor(class="kw">const class="type">color clr)      { m_border_color=clr;          }
class="type">void                BorderSize(class="kw">const class="type">int border)      { m_border=border;             }
class=class="str">"cmt">//--- 设置和获取透明度
class="type">uchar               Alpha(class="type">void)                       { class="kw">return(m_transparency);     }
class="type">void                Alpha(class="kw">const class="type">uchar alpha)          { m_transparency=alpha;        }
class=class="str">"cmt">//--- 设置和获取标签值
class="type">class="kw">string              Label(class="type">void)                       { class="kw">return(m_label);             }
class="type">void                Label(class="kw">const class="type">class="kw">string label)         { m_label=label;               }
class=class="str">"cmt">//--- 创建指标
class="type">void                Create(class="type">class="kw">string name,class="type">int x,class="type">int y);
class=class="str">"cmt">//--- 删除指标
class="type">void                Delete(class="type">class="kw">string name);
class=class="str">"cmt">//--- 设置和获取指标值
class="type">void                NewValue(class="type">int value);
class="type">void                NewValue(class="type">class="kw">double value);
};
CCircleSimple::CCircleSimple(class="type">void) : m_bg_color(clrAliceBlue),
                     m_border_color(clrRoyalBlue),
                     m_font_color(clrBlack),
                     m_label_color(clrBlack),
                     m_transparency(class="num">255),
                     m_border(class="num">5),
                     m_radius(class="num">40),

「圆形指标控件的绘制与刷新逻辑」

在 MT5 自定义指标里封装一个圆形数值控件,核心是两个方法:Create 负责首次画布构建,NewValue 负责后续只重绘数值不重建结构。 Create 里先用半径 r 约束坐标,避免圆被切边;XSize 和 YSize 都设为 2*r+1 像素,保证画布正好包住整圆。若 m_border 大于 0,先 FillCircle 画边框色,再以内半径 r-m_border 填底色。 文字分两层:主数值用 m_font_size(示例 17)居中,标签用 m_label_font_size(示例 20)放在主字下方偏移处。两层都走 TA_CENTER|TA_VCENTER 对齐,最后 m_canvas.Update 推到图表。 NewValue 不重画背景边框,只以底色圆覆盖旧数字,再写新整数并 Update。这样在外汇或贵金属高频跳动行情下,控件刷新开销倾向更低,但 MT5 对象仍受限于主线程,极端 tick 密度可能掉帧。

MQL5 / C++
   m_font_size(class="num">17),
   m_label_font_size(class="num">20),
   m_digits(class="num">2),
   m_label("label")
   {
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建指标                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCircleSimple::Create(class="type">class="kw">string name,class="type">int x,class="type">int y)
  {
   class="type">int r=m_radius;
class=class="str">"cmt">//--- 相对于半径修改指标位置
   x=(x<r)?r:x;
   y=(y<r)?r:y;
   Name(name);
   X(x);
   Y(y);
   XSize(class="num">2*r+class="num">1);
   YSize(class="num">2*r+class="num">1);
   if(!CreateCanvas())
      Print("错误。未能创建指标。");
   if(m_border>class="num">0)
      m_canvas.FillCircle(r,r,r,ColorToARGB(m_border_color,m_transparency));
   m_canvas.FillCircle(r,r,r-m_border,ColorToARGB(m_bg_color,m_transparency));
class=class="str">"cmt">//---
   m_canvas.FontSizeSet(m_font_size);
   m_canvas.TextOut(r,r,"class="num">0",ColorToARGB(m_font_color,m_transparency),TA_CENTER|TA_VCENTER);
class=class="str">"cmt">//---
   m_canvas.FontSizeSet(m_label_font_size);
   m_canvas.TextOut(r,r+m_label_font_size,m_label,ColorToARGB(m_label_color,m_transparency),TA_CENTER|TA_VCENTER);
   m_canvas.Update();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置并更新指标数值                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCircleSimple::NewValue(class="type">int value)
  {
   class="type">int r=m_radius;
   m_canvas.FillCircle(r,r,r-m_border,ColorToARGB(m_bg_color,m_transparency));
class=class="str">"cmt">//---
   m_canvas.FontSizeSet(m_font_size);
   m_canvas.TextOut(r,r,IntegerToString(value),ColorToARGB(m_font_color,m_transparency),TA_CENTER|TA_VCENTER);
class=class="str">"cmt">//---
   m_canvas.FontSizeSet(m_label_font_size);
   m_canvas.TextOut(r,r+m_label_font_size,m_label,ColorToARGB(m_label_color,m_transparency),TA_CENTER|TA_VCENTER);
   m_canvas.Update();
  }

用圆形 GUI 把 ADX 与 RSI 摆上主图

想在 MT5 主图直接盯住 ADX 和 RSI,而不是来回切副图,可以用 CustomGUI 库画两个圆框。下面这段初始化代码把 ADX(10) 和 RSI(10, 收盘价) 的句柄先抓出来,失败就打印并返回 INIT_FAILED,避免空跑。 ind1 是半径 60 像素、标签写“ADX”的浅灰圆,放在坐标 (100,100);ind2 半径 55、边框 8 像素、字号 22、深红配色的“RSI”圆,放在 (250,100)。两个数组都设了 ArraySetAsSeries(true),保证 adx[0]、rsi[0] 是最新一根 K 线的值。 OnCalculate 里每次只 CopyBuffer 取 0~1 两根柱(长度 2),若任一指标拷贝失败直接 return(0)。这意味着你拖进 EURUSD 这类品种时,前两根 tick 若句柄未就绪,圆框不会刷新——属正常保护。外汇与贵金属波动剧烈,这类叠加显示只辅助读势,不预示方向。 别让圆框挡住蜡烛 把 Create 的 x 坐标从 100/250 调到 400 以后,或把 Radius 缩到 40,能在 15 分钟图上少遮实体。小布盯盘时习惯留左边 1/3 空白给 GUI。

MQL5 / C++
class="macro">#class="kw">property copyright "版权所有 class="num">2017, Alexander Fedosov"
class="macro">#class="kw">property link      "[MQL5官方文档]
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">//---
CCircleSimple ind1,ind2;
class=class="str">"cmt">//---
class="type">int InpInd_Handle,InpInd_Handle1;
class="type">class="kw">double adx[],rsi[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标初始化函数                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---- 获取指标句柄
   InpInd_Handle=iADX(Symbol(),PERIOD_CURRENT,class="num">10);
   InpInd_Handle1=iRSI(Symbol(),PERIOD_CURRENT,class="num">10,PRICE_CLOSE);
   if(InpInd_Handle==INVALID_HANDLE)
     {
       Print("获取指标句柄失败");
       class="kw">return(INIT_FAILED);
     }
   ArraySetAsSeries(adx,true);
   ArraySetAsSeries(rsi,true);
class=class="str">"cmt">//---
   ind1.Radius(class="num">60);
   ind1.Label("ADX");
   ind1.Color(clrWhiteSmoke);
   ind1.LabelColor(clrBlueViolet);
   ind1.Create("adx",class="num">100,class="num">100);
class=class="str">"cmt">//---
   ind2.Radius(class="num">55);
   ind2.BorderSize(class="num">8);
   ind2.FontSize(class="num">22);
   ind2.LabelColor(clrCrimson);
   ind2.BorderColor(clrFireBrick);
   ind2.Label("RSI");
   ind2.Create("rsi",class="num">250,class="num">100);
class="type">int OnCalculate(class="kw">const class="type">int rates_total,
                class="kw">const class="type">int prev_calculated,
                class="kw">const class="type">class="kw">datetime &time[],
                class="kw">const class="type">class="kw">double &open[],
                class="kw">const class="type">class="kw">double &high[],
                class="kw">const class="type">class="kw">double &low[],
                class="kw">const class="type">class="kw">double &close[],
                class="kw">const class="type">long &tick_volume[],
                class="kw">const class="type">long &volume[],
                class="kw">const class="type">int &spread[])
  {
class=class="str">"cmt">//---
   if(CopyBuffer(InpInd_Handle,class="num">0,class="num">0,class="num">2,adx)<=class="num">0 ||
       CopyBuffer(InpInd_Handle1,class="num">0,class="num">0,class="num">2,rsi)<=class="num">0
      )
      class="kw">return(class="num">0);

◍ 收尾时把指标实例清干净

在自定义指标退出时,必须显式释放前面用 new 创建的指标实例,否则 MT5 会在图表重载或周期切换后留下悬空句柄。 上面这段收尾代码里,ind1.Delete() 与 ind2.Delete() 分别对应 ADX 与 RSI 两个平滑实例的销毁,紧接着调用 ChartRedraw() 强制重绘,避免残留图形。 实际在 MT5 里验证很简单:挂上这个指标,切一次周期,若没写 OnDeinit 清理,数据窗口里会看到实例数只增不减;补上这两行 Delete 后,重复切换十次周期,句柄计数应回到初始值。外汇与贵金属品种波动剧烈,这类资源泄漏在长周期盯盘时可能拖慢终端,属高风险环境下的基础 hygiene。

MQL5 / C++
  ind1.NewValue(adx[class="num">0]);
  ind2.NewValue(rsi[class="num">0]);
class=class="str">"cmt">//--- 返回 prev_calculated 的数值以便下次调用
  class="kw">return(rates_total);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标的逆初始化函数                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
  ind1.Delete();
  ind2.Delete();
  ChartRedraw();
  }
把图形调试交给小布
这些自定义指标类的编译与图形渲染诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到异常提示,你只管调参数和逻辑。

常见问题

这两个方法负责图形资源(画布)的创建与释放,任何自定义图形对象在构造和销毁时都会调用,遗漏会导致资源泄漏或绘制失败。
它集中包含了 Indicators 文件夹下所有图形类,EA 或指标只需引入这一个文件就能访问整套函数库,避免逐个手工 include。
小布盯盘支持 MT5 自定义指标挂载,你编译好放入对应目录后,在品种页添加即可,AIGC 会辅助识别图形渲染状态。
框架是边界轮廓,背景承载文本空间,数值显示实时读数,描述放名称周期等信息,四者构成最基础的可读圆形指标结构。