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