利用 CCanvas 的自定义指标和信息图·进阶篇
(2/3)· 从圆角线到六边花瓣,进阶篇深挖 CustomGUI 里 5 类图元的分层绘制逻辑
「用三块基元拼出正六边形」
想在 MT5 里画一个干净的六边形指标,不必堆多边形再填充。最省事的做法是用 CCanvas 的三个基元:一个矩形加两个等腰三角形,刚好拼成规则六边形,视觉上像被方形画布框住。 几何约束先记牢:六边形边长等于外接圆半径,也就是画布边长的一半;六边形内角 120 度,所以那两个等腰三角形的底角是 30 度。用 CCanvas::FillTriangle() 时,这个 30 度直接决定三角形高度和底边坐标。 CHexagon 类继承自主画布基类,除了六边形本体,还挂两个文本对象——一个显示数值,一个显示描述。下面这段 Create() 就是核心可视化逻辑,注意 a 变量算的是三角形沿 Y 轴的顶点偏移。 代码里 r 取画布尺寸,a 用 sin(30°) 乘半高得到,约 0.5 倍半高。FillTriangle 两次调用分别画上下两个尖角,FillRectangle 填中间肚皮。跑完 Update() 就能在图表上看到六边形控件,外汇和贵金属图表加载此类自定义控件存在平台兼容性风险,建议先在模拟盘验证。
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 位置看到蓝底数据牌。外汇与贵金属波动剧烈,此类面板仅作视觉辅助,不构成方向判断。
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;卸载时反向清空。
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,能看到花瓣明显放大;外汇或贵金属图表叠加此类自绘指标仍属高风险辅助,信号仅供参考。
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,间隙放大后更容易观察命中区。
{
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 一一对应,缺一个就会漏释放。
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">//+------------------------------------------------------------------+
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">//+------------------------------------------------------------------+