利用 CCanvas 的自定义指标和信息图·进阶篇
🎨

利用 CCanvas 的自定义指标和信息图·进阶篇

(2/3)· 从圆角线到六边花瓣,进阶篇深挖 CustomGUI 里 5 类图元的分层绘制逻辑

偏理论进阶 第 2/3 篇
很多人以为 CCanvas 只能画直线和文字,结果自定义指标一上手就图层错乱:刻度被数值盖住、底图遮了边框。复合图元若不分清绘制顺序,伪 3D 效果直接变成叠罗汉。

「用三块基元拼出正六边形」

想在 MT5 里画一个干净的六边形指标,不必堆多边形再填充。最省事的做法是用 CCanvas 的三个基元:一个矩形加两个等腰三角形,刚好拼成规则六边形,视觉上像被方形画布框住。 几何约束先记牢:六边形边长等于外接圆半径,也就是画布边长的一半;六边形内角 120 度,所以那两个等腰三角形的底角是 30 度。用 CCanvas::FillTriangle() 时,这个 30 度直接决定三角形高度和底边坐标。 CHexagon 类继承自主画布基类,除了六边形本体,还挂两个文本对象——一个显示数值,一个显示描述。下面这段 Create() 就是核心可视化逻辑,注意 a 变量算的是三角形沿 Y 轴的顶点偏移。 代码里 r 取画布尺寸,a 用 sin(30°) 乘半高得到,约 0.5 倍半高。FillTriangle 两次调用分别画上下两个尖角,FillRectangle 填中间肚皮。跑完 Update() 就能在图表上看到六边形控件,外汇和贵金属图表加载此类自定义控件存在平台兼容性风险,建议先在模拟盘验证。

MQL5 / C++
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="macro">#include "Indicators\Hexagon.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建指标                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CHexagon::Create(class="type">class="kw">string name,class="type">int x,class="type">int y)
  {
   class="type">int a,r;
   r=m_size;
class=class="str">"cmt">//--- 相对于坐标轴修改指标位置
   x=(x<r/class="num">2)?r/class="num">2:x;
   y=(y<r/class="num">2)?r/class="num">2:y;
   Name(name);
   X(x);
   Y(y);
   XSize(r);
   YSize(r);
   if(!CreateCanvas())
      Print("错误。不能创建画布。");
class=class="str">"cmt">//--- 计算三角形高度
   a=class="type">int(YSize()/class="num">2*MathSin(class="num">30*M_PI/class="num">180));
class=class="str">"cmt">//--- 六角形
   m_canvas.FillTriangle(XSize()/class="num">2,class="num">0,class="num">0,a,XSize(),a,ColorToARGB(m_bg_color,m_transparency));
   m_canvas.FillRectangle(class="num">0,a,XSize(),a+YSize()/class="num">2,ColorToARGB(m_bg_color,m_transparency));
   m_canvas.FillTriangle(class="num">0,a+YSize()/class="num">2,XSize(),a+YSize()/class="num">2,XSize()/class="num">2,YSize(),ColorToARGB(m_bg_color,m_transparency));
class=class="str">"cmt">//--- 描述和数值
   m_canvas.FontNameSet("Trebuchet MS");
   m_canvas.FontSizeSet(m_value_font_size);
   m_canvas.TextOut(r/class="num">2,r/class="num">2,"-",ColorToARGB(m_value_color,m_transparency),TA_CENTER|TA_VCENTER);
   m_canvas.FontSizeSet(m_label_font_size);
   m_canvas.TextOut(r/class="num">2,r/class="num">2+m_value_font_size,m_label_value,ColorToARGB(m_label_color,m_transparency),TA_CENTER|TA_VCENTER);
class=class="str">"cmt">//---
   m_canvas.Update();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 在MT5图表上画出六边形数据牌

CHexagon 类的 NewValue 方法负责把数值和标签画进六边形面板。它先用三角形高度公式 a = YSize()/2 * sin(30°) 算出上下尖角的偏移量,再用三块 FillTriangle 加一个 FillRectangle 拼出完整六边形背景,透明度由 m_transparency 控制。 文本层先设字体 Trebuchet MS,用 TextOut 在中心 (r/2, r/2) 输出 DoubleToString(value, digits),digits 默认 2 表示价格类数值保留两位小数;标签则向下偏移 m_value_font_size 像素避免重叠。最后 m_canvas.Update() 把缓冲提交到图表。 初始化里实盘可改的参数很直接:ind1.Size(110) 决定六边形边长像素,ind1.Create("hex1",300,200) 后两个数是面板左上角坐标。把 ind2.BgColor(C'60,170,220') 的 RGB 换成你盯盘的配色,刷新后就能在 300,200 位置看到蓝底数据牌。外汇与贵金属波动剧烈,此类面板仅作视觉辅助,不构成方向判断。

MQL5 / C++
class="type">void CHexagon::NewValue(class="type">class="kw">double value,class="type">int digits=class="num">2)
  {
   class="type">int a,r;
   r=m_size;
class=class="str">"cmt">//--- 计算三角形高度
   a=class="type">int(YSize()/class="num">2*MathSin(class="num">30*M_PI/class="num">180));
class=class="str">"cmt">//--- 六角形
   m_canvas.FillTriangle(XSize()/class="num">2,class="num">0,class="num">0,a,XSize(),a,ColorToARGB(m_bg_color,m_transparency));
   m_canvas.FillRectangle(class="num">0,a,XSize(),a+YSize()/class="num">2,ColorToARGB(m_bg_color,m_transparency));
   m_canvas.FillTriangle(class="num">0,a+YSize()/class="num">2,XSize(),a+YSize()/class="num">2,XSize()/class="num">2,YSize(),ColorToARGB(m_bg_color,m_transparency));
class=class="str">"cmt">//--- 文本
   m_canvas.FontNameSet("Trebuchet MS");
   m_canvas.FontSizeSet(m_value_font_size);
   m_canvas.TextOut(r/class="num">2,r/class="num">2,DoubleToString(value,digits),ColorToARGB(m_value_color,m_transparency),TA_CENTER|TA_VCENTER);
   m_canvas.FontSizeSet(m_label_font_size);
   m_canvas.TextOut(r/class="num">2,r/class="num">2+m_value_font_size,m_label_value,ColorToARGB(m_label_color,m_transparency),TA_CENTER|TA_VCENTER);
class=class="str">"cmt">//---
   m_canvas.Update();
   }

六边形面板的布局与销毁逻辑

这段初始化收尾代码把 14 个六边形控件按坐标铺到了图表上,前半段 ind5、ind6 落在 (300,320),背景色用了 C'150,190,15' 这种 RGB 写法,和 clrWhite 常量混用,说明面板配色可以逐对象定制。 ind7 到 ind14 则排成两列三行网格:x 取 180 和 420,y 取 140、280,白色底配 Size(110) 的是主显区,彩色底(C'10,115,185'、C'20,150,150' 等)是状态边。你在 MT5 里改这些坐标,面板就会整体平移,不会挤占 K 线。 OnDeinit 里挨个 Delete() 是防内存泄漏的硬操作——指标卸载时若不显式删对象,重加载可能报『对象已存在』。外汇与贵金属图表挂这类面板属高风险辅助,参数乱调可能遮盖价格信号。 下面这段是原文初始化与销毁的核心片段,逐行看就是:先建对象、设色、定位,最后 return INIT_SUCCEEDED;卸载时反向清空。

MQL5 / C++
  ind5.Create("hex5",class="num">300,class="num">320);
  ind6.BgColor(C&class="macro">#x27;class="num">150,class="num">190,class="num">15&class="macro">#x27;);
  ind6.Create("hex6",class="num">300,class="num">320);
class=class="str">"cmt">//---
  ind7.BgColor(clrWhite);
  ind7.Size(class="num">110);
  ind7.Create("hex7",class="num">180,class="num">140);
  ind8.BgColor(C&class="macro">#x27;class="num">10,class="num">115,class="num">185&class="macro">#x27;);
  ind8.Create("hex8",class="num">180,class="num">140);
class=class="str">"cmt">//---
  ind9.BgColor(clrWhite);
  ind9.Size(class="num">110);
  ind9.Create("hex9",class="num">420,class="num">140);
  ind10.BgColor(C&class="macro">#x27;class="num">20,class="num">150,class="num">150&class="macro">#x27;);
  ind10.Create("hex10",class="num">420,class="num">140);
class=class="str">"cmt">//---
  ind11.BgColor(clrWhite);
  ind11.Size(class="num">110);
  ind11.Create("hex11",class="num">420,class="num">280);
  ind12.BgColor(C&class="macro">#x27;class="num">225,class="num">0,class="num">80&class="macro">#x27;);
  ind12.Create("hex12",class="num">420,class="num">280);
class=class="str">"cmt">//---
  ind13.BgColor(clrWhite);
  ind13.Size(class="num">110);
  ind13.Create("hex13",class="num">180,class="num">280);
  ind14.BgColor(C&class="macro">#x27;class="num">240,class="num">145,class="num">5&class="macro">#x27;);
  ind14.Create("hex14",class="num">180,class="num">280);
class=class="str">"cmt">//---
  class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标逆初函数                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
  ind1.Delete();
  ind2.Delete();
  ind3.Delete();
  ind4.Delete();
  ind5.Delete();
  ind6.Delete();
  ind7.Delete();
  ind8.Delete();
  ind9.Delete();
  ind10.Delete();
  ind11.Delete();
  ind12.Delete();
  ind13.Delete();
  ind14.Delete();
  }

「用画布基元拼出花瓣指标」

花瓣状指标的本质是用 CCanvas 的少量基元堆叠:一个填充圆加 1~2 个填充三角形。靠这两个形状就能在图表上画出指向不同方位的「花瓣」,比手绘多边形省事得多。 方向由枚举 ENUM_POSITION 控制,共 6 种:TOPRIGHT(1) 到 BOTHLEFT(6)。其中 BOTHRIGHT 与 BOTHLEFT 用两个三角形,上三角分别落在右上或左上,其余四种只用单三角。 实现时新建 Petal.mqh,令 CPetal 继承前文 CCanvasBase。Create() 里先按半径算画布尺寸(XSize/YSize = 2*r+1),再 FillCircle 打底,随后依据 m_orientation 分支画三角。下面代码截取了枚举定义与 Create 的核心片段,注意坐标用 XSize()/2 等相对值,换半径不用重写位置。 在 MT5 里把 r 从默认改到 15 或 30,能看到花瓣明显放大;外汇或贵金属图表叠加此类自绘指标仍属高风险辅助,信号仅供参考。

MQL5 / C++
enum ENUM_POSITION
  {
  TOPRIGHT=class="num">1,
  TOPLEFT=class="num">2,
  BOTTOMRIGHT=class="num">3,
  BOTTOMLEFT=class="num">4,
  BOTHRIGHT=class="num">5,
  BOTHLEFT=class="num">6
  };
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="macro">#include "Indicators\Hexagon.mqh"
class="macro">#include "Indicators\Petal.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建指标                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CPetal::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("错误。不能创建画布。");
class=class="str">"cmt">//---
  m_canvas.FillCircle(r,r,r,ColorToARGB(m_bg_color,m_transparency));
  if(m_orientation==TOPRIGHT)
      m_canvas.FillTriangle(XSize()/class="num">2,class="num">0,XSize(),class="num">0,XSize(),YSize()/class="num">2,ColorToARGB(m_bg_color,m_transparency));
  else if(m_orientation==TOPLEFT)
      m_canvas.FillTriangle(class="num">0,class="num">0,XSize()/class="num">2,class="num">0,class="num">0,YSize()/class="num">2,ColorToARGB(m_bg_color,m_transparency));
  else if(m_orientation==BOTTOMRIGHT)
      m_canvas.FillTriangle(XSize(),YSize(),XSize(),YSize()/class="num">2,XSize()/class="num">2,YSize(),ColorToARGB(m_bg_color,m_transparency));
  else if(m_orientation==BOTTOMLEFT)
      m_canvas.FillTriangle(class="num">0,YSize(),class="num">0,YSize()/class="num">2,XSize()/class="num">2,YSize(),ColorToARGB(m_bg_color,m_transparency));
  else if(m_orientation==BOTHRIGHT)

◍ 花瓣控件的对角绘制与多实例排布

上面这段裁剪自一个自定义图形类的绘制分支:当方向参数为 BOTHLEFT 时,用两组 FillTriangle 在画布左上和右下角填出半透明三角,拼出花瓣的对角形态;BOTHRIGHT 则镜像到右上和左下。坐标全部依赖 XSize()/2 与 YSize()/2 取中点,透明度由 m_transparency 经 ColorToARGB 合并进颜色值。 字体与标签走另一套逻辑:先设 Trebuchet MS、值字号 m_value_font_size,在矩形区 r 内居中输出“-”;再切到标签字号输出 m_label_value,最后 m_canvas.Update() 提交。想验证就改 m_value_font_size 与 m_label_font_size 的差值,看主次文字层级是否如预期分离。 调用侧更直白:OnInit 里定义了 ind1~ind4 四个 CPetal 实例,b=2 做 2 像素偏移避免重叠。ind1 用 BOTHLEFT、玫红 C'230,80,80' 落在 (198,98),ind2 用 BOTHRIGHT、青蓝 C'35,170,190' 落在 (302,98),ind3 同向右落 (198,202),ind4 向左落 (302,202)。外汇与贵金属图表叠加这类自绘控件,需注意高风险——过度绘制会拖慢 MT5 的实时刷新。 直接把这四行 Create 的坐标和 Orientation 抄进你的指标,能在 500×300 区域内复现四瓣对角布局;若把 b 调到 10,间隙放大后更容易观察命中区。

MQL5 / C++
  {
    m_canvas.FillTriangle(XSize()/class="num">2,class="num">0,XSize(),class="num">0,XSize(),YSize()/class="num">2,ColorToARGB(m_bg_color,m_transparency));
    m_canvas.FillTriangle(class="num">0,YSize(),class="num">0,YSize()/class="num">2,XSize()/class="num">2,YSize(),ColorToARGB(m_bg_color,m_transparency));
   }
  else if(m_orientation==BOTHLEFT)
   {
    m_canvas.FillTriangle(class="num">0,class="num">0,XSize()/class="num">2,class="num">0,class="num">0,YSize()/class="num">2,ColorToARGB(m_bg_color,m_transparency));
    m_canvas.FillTriangle(XSize(),YSize(),XSize(),YSize()/class="num">2,XSize()/class="num">2,YSize(),ColorToARGB(m_bg_color,m_transparency));
   }
class=class="str">"cmt">//--- 描述和数值
  m_canvas.FontNameSet("Trebuchet MS");
  m_canvas.FontSizeSet(m_value_font_size);
  m_canvas.TextOut(r,r,"-",ColorToARGB(m_value_color,m_transparency),TA_CENTER|TA_VCENTER);
  m_canvas.FontSizeSet(m_label_font_size);
  m_canvas.TextOut(r,r+m_value_font_size,m_label_value,ColorToARGB(m_label_color,m_transparency),TA_CENTER|TA_VCENTER);
  m_canvas.Update();
  }
class="macro">#include <CustomGUI\CustomGUI.mqh>
CPetal ind1,ind2,ind3,ind4;
class="type">int OnInit()
  {
   class="type">int b=class="num">2;
   ind1.BgColor(C&class="macro">#x27;class="num">230,class="num">80,class="num">80&class="macro">#x27;);
   ind1.Orientation(BOTHLEFT);
   ind1.Create("petal1",class="num">200-b,class="num">100-b);
   ind2.BgColor(C&class="macro">#x27;class="num">35,class="num">170,class="num">190&class="macro">#x27;);
   ind2.Orientation(BOTHRIGHT);
   ind2.Create("petal2",class="num">300+b,class="num">100-b);
   ind3.BgColor(C&class="macro">#x27;class="num">245,class="num">155,class="num">70&class="macro">#x27;);
   ind3.Orientation(BOTHRIGHT);
   ind3.Create("petal3",class="num">200-b,class="num">200+b);
   ind4.BgColor(C&class="macro">#x27;class="num">90,class="num">200,class="num">130&class="macro">#x27;);
   ind4.Orientation(BOTHLEFT);
   ind4.Create("petal4",class="num">300+b,class="num">200+b);
   class="kw">return(INIT_SUCCEEDED);
  }

把指标句柄在逆初始化时清干净

自定义指标跑完退出时,如果不主动释放挂在 OnCalculate 里创建的句柄,MT5 会在终端里留下孤立的 iCustom 引用,多次加载卸载后可能拖慢刷新。 下面这段逆初始化函数把四个指标对象逐一 Delete,和初始化时创建的 ind1~ind4 一一对应,缺一个就会漏释放。

MQL5 / C++
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">//---
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(const class="type">int reason)
  {
   ind1.Delete();
   ind2.Delete();
   ind3.Delete();
   ind4.Delete();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
OnCalculate 这里直接 return(rates_total),意味着每次新柱都会重算全部数据,若你接的是多符号多周期调用,CPU 占用会随品种数线性上升,实盘外汇或贵金属前建议先在本机用 5 个品种回测一下负载。 开 MT5 按 F4 把这段贴进你的指标原型,编译后切到 EURUSD 的 M1 反复加载卸载十次,再用 Ctrl+I 看指标列表里是否还残着旧句柄。

MQL5 / C++
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">//---
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(const class="type">int reason)
  {
   ind1.Delete();
   ind2.Delete();
   ind3.Delete();
   ind4.Delete();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
把图元分层交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到分层渲染是否异常,你只需判断信号本身。把重复劳动交给小布,你专注决策。

常见问题

因为元素是按方法调用逐层绘制的,每次更新数值都要先重画刻度背景,再画指标主体,顺序乱了就会遮挡。
小布盯盘的品种页支持调用 MT5 本地指标,只要编译进 CustomGUI 库,就能在盘面叠加查看,不必自己盯图层。
二者都继承自 CCanvasBase,由多个基本图形(圆、矩形、多边形)组合,且依赖分层叠加实现立体观感。
仅作视觉辅助,外汇贵金属波动受杠杆与消息影响大,图形倾向提示概率而非确定性,高风险需自担。
金字塔结构适合表达层级占比,多品种并排时需统一最大参数值,否则长度失真,建议先归一化再绘制。