使用 CCanvas 类绘制刻度表盘·进阶篇
📐

使用 CCanvas 类绘制刻度表盘·进阶篇

(2/3)· 接上篇基础铺垫,这一篇把表盘定位、枚举宏与 CCanvas 源码级修改一次讲透

偏理论进阶 第 2/3 篇

接上篇,我们继续深挖 CCanvas 表盘绘制的核心机制。很多交易者直接套用现成指标却看不懂锚点逻辑,导致表盘在图表缩放时错位、重叠,白白浪费了视觉监控效率。

表盘绘制函数的参数骨架

在 MT5 里用 GAUGE_STR 结构做自定义仪表盘,核心是一组 Set 函数把视觉与数据参数写进引用结构,最后靠 GaugeRedraw 和 GaugeNewValue 触发重绘。 下面这段声明列出了刻度与表盘的基础字段:range 控制刻度总数,rotation 是整体旋转角度,min/max 定义左右极值,mul 决定标签显示的倍数,style 选刻度样式,col 是颜色,display_arc 控制是否画刻度弧线。 [CODE] int range, // 刻度范围 int rotation, // 旋转角度 double min, // 最小值 (左侧) double max, // 最大值 (右侧) ENUM_MUL_SCALE mul, // 刻度标签的倍数 ENUM_SCALE_STYLE style, // 刻度样式 color col, // 刻度颜色 bool display_arc // 显示刻度线 ); [/CODE] GaugeSetMarkParameters 管刻度标记层级:style 定样式,size 定大小,major_tmi 是主标记间隔,middle_tmarks 和 minor_tmarks 分别控制主标记之间、中等标记之间的次级数量。比如 major_tmi=10、middle_tmarks=4、minor_tmarks=2,表盘上每 10 个单位出一根主刻,中间插 4 根中刻、每中刻再分 2 根细刻。 GaugeSetMarkLabelFont 单独管刻度文字:字号、字体名、斜体粗体开关、颜色,四项直接决定报价数字在盘面上的可读性。 GaugeSetLegendParameters 负责图例或注释文本,gl 选类型,enable 开关,str 传字符串,radius 与 angle 用极坐标定位,后面跟着字体与颜色参数。 GaugeSetRangeParameters 按 index 给表盘分段着色:enable 后 start~end 区间填 col 颜色,适合把超买超卖区用红绿带标出来。外汇与贵金属波动剧烈,这种区间提示仅作概率参考,实际操作高风险。 GaugeSetNeedleParameters 定表针:ncenter_style 与 ncenter_col 管轴心,needle_col 管针身,needle_fill 定填充方式。 所有参数写完,调 GaugeRedraw(GAUGE_STR &g) 重画静态层;数值更新走 GaugeNewValue(声明截断在原函数头)。开 MT5 新建 EA 把这些声明粘进头文件,先只设 range=20、min=0、max=100 看刻度排布,是最快的验证路径。

MQL5 / C++
class="type">int range, class=class="str">"cmt">// 刻度范围
class="type">int rotation, class=class="str">"cmt">// 旋转角度
class="type">class="kw">double min, class=class="str">"cmt">// 最小值 (左侧)
class="type">class="kw">double max, class=class="str">"cmt">// 最大值 (右侧)
ENUM_MUL_SCALE mul, class=class="str">"cmt">// 刻度标签的倍数
ENUM_SCALE_STYLE style, class=class="str">"cmt">// 刻度样式
class="type">class="kw">color col, class=class="str">"cmt">// 刻度颜色
class="type">bool display_arc class=class="str">"cmt">// 显示刻度线
);

◍ 表盘对象的接口声明拆解

在 MT5 自定义指标里做可视化表盘,先把结构引用和几个核心函数声明理清,后面才不会出现对象泄漏或坐标错位。下面这段声明定义了表盘的创建、删除与重定位接口,每一个都靠 GAUGE_STR 的引用传参,意味着调用方持有的结构体会被直接改写。 GaugeCreate 接收表盘结构引用和变化值 v,用于按当前状态生成图形对象;GaugeDelete 只吃一个引用,负责从图表撤掉对应资源。注意这两个都用了 & 引用,任何修改都会回写到原结构,复制粘贴时别误写成值传递。 GaugeCalcLocation 返回 bool,用来计算表盘该摆哪,算失败就返回 false,调用方必须判返回值。GaugeRelocation 则是纯 void,算好位置后直接挪,不反馈成败。实盘加载前,建议在策略测试器里先跑一次创建+删除循环,确认 0 错误对象数增长,再挂真账户——外汇和贵金属杠杆高,图表对象堆积可能拖慢终端。

MQL5 / C++
  GAUGE_STR &g,      class=class="str">"cmt">// 引用表盘结构
  class="type">class="kw">double v           class=class="str">"cmt">// 变化值
);
class="type">void GaugeDelete(
  GAUGE_STR &g       class=class="str">"cmt">// 引用表盘结构
);
class="type">bool GaugeCalcLocation(
  GAUGE_STR& g       class=class="str">"cmt">// 引用表盘结构
);
class="type">void GaugeRelocation(
  GAUGE_STR &g       class=class="str">"cmt">// 引用表盘结构
);

「仪表图元那些枚举参数怎么填」

在 MT5 里用 OBJ_GAUGE 这类图形对象做自定义仪表盘时,函数参数基本都靠一组枚举来约束外观。表 2 原文列了 10 个枚举类型,从边框、实体到刻度倍数、表针填充全涵盖,少填一个编译器就报错。 边框用 ENUM_CASE_BORDER_STYLE,只有三种:NONE 无边框、THIN 细线、THICK 粗线。实体形状走 ENUM_CASE_STYLE,可选 ROUND 圆形或 SECTOR 扇形,做半圈转速表就选 SECTOR。 刻度标签倍数 ENUM_MUL_SCALE 是实盘最容易用错的一项。它从 MUL_10000(显示 х10k)一路到 MUL_00001(显示 /10k),比如报价 1.2345 的黄金差价若用 MUL_10000,标签直接标成 12345 而非 1.2345,读盘效率取决于你数据量级。 表针中心 ENUM_NCENTER_STYLE 分不显示、小、大三档;填充抗锯齿由 ENUM_NEEDLE_FILL 控制,NEEDLE_FILL_AA 边缘抗锯齿最顺眼但吃一点渲染。相对定位 ENUM_REL_MODE 能禁掉或选水平、垂直、对角线,挂多图时防重叠有用。 外汇与贵金属波动大、杠杆高,仪表只是辅助读数的视觉壳,参数调漂亮不代表信号可靠,开 MT5 拖个 gauge 对象挨个换枚举值对比才是最直接的验证法。

用宏写死表盘比例与配色

在 MT5 自定义仪表盘指标里,把尺寸和颜色全交给预处理宏,是避免运行时反复算比例的常见做法。下面这组宏把「实体直径」当基准单位,所有元素大小都按百分比折算,改一个 DIAM 基准就能整体缩放。 代码里小号表针中心直径占实体 5%,大号占 7.5%;边框宽度小号 2%、大号 5%。内部留白分三档:小 2.0%、中 3.0%、大 7.0%,直接决定刻度会不会挤到边。 刻度线按主/中/次分三档,大号盘上主刻度达到实体直径的 10.0%,次刻度 5.0%;中号盘主刻度 6.5%、次刻度 3.0%。标签字号同步用 4 / 6.5 / 10 映射小中大全套。 配色部分定义了 8 个默认色:表盘底 clrMintCream、边框 clrLightSteelBlue、指针 clrDimGray、刻度字 clrBlack。你开 MT5 新建指标把这段贴进头文件,改 DEF_COL_CASE 就能换盘底,不用动绘制逻辑。外汇与贵金属行情波动剧烈,这类视觉组件只辅助读图,不构成任何方向判断。

MQL5 / C++
class="macro">#define DIAM_TO_NDCSL_RATIO   class="num">5    class=class="str">"cmt">//表针中心直径 (较小) 作为实体直径的百分比
class="macro">#define DIAM_TO_NDCSB_RATIO   class="num">7.5 class=class="str">"cmt">//表针中心直径 (较大) 作为实体直径的百分比
class=class="str">"cmt">//---
class="macro">#define DIAM_TO_BD_SIZE_S      class="num">2 class=class="str">"cmt">//边框宽度 (较小) 作为实体直径的百分比
class="macro">#define DIAM_TO_BD_SIZE_B      class="num">5 class=class="str">"cmt">//边框宽度 (较大) 作为实体直径的百分比
class=class="str">"cmt">//---
class="macro">#define DIAM_TO_BD_GAP_S       class="num">2.0 class=class="str">"cmt">//自表盘实体边框到内部元素(较小) 的空间作为实体直径的百分比
class="macro">#define DIAM_TO_BD_GAP_M       class="num">3.0 class=class="str">"cmt">//自表盘实体边框到内部元素(中等) 的空间作为实体直径的百分比
class="macro">#define DIAM_TO_BD_GAP_L       class="num">7.0 class=class="str">"cmt">//自表盘实体边框到内部元素(较大) 的空间作为实体直径的百分比
class=class="str">"cmt">//---
class="macro">#define DIAM_TO_MSZ_MJ_S       class="num">3.3 class=class="str">"cmt">//主要刻度 (较小) 的大小作为实体直径的百分比
class="macro">#define DIAM_TO_MSZ_MD_S       class="num">2.3 class=class="str">"cmt">//中等刻度 (较小) 的大小作为实体直径的百分比
class="macro">#define DIAM_TO_MSZ_MN_S       class="num">1.3 class=class="str">"cmt">//次要刻度 (较小) 的大小作为实体直径的百分比
class=class="str">"cmt">//---
class="macro">#define DIAM_TO_MSZ_MJ_M       class="num">6.5 class=class="str">"cmt">//主要刻度 (中等) 的大小作为实体直径的百分比
class="macro">#define DIAM_TO_MSZ_MD_M       class="num">4.8 class=class="str">"cmt">//中等刻度 (中等) 的大小作为实体直径的百分比
class="macro">#define DIAM_TO_MSZ_MN_M       class="num">3.0 class=class="str">"cmt">//次要刻度 (中等) 的大小作为实体直径的百分比
class=class="str">"cmt">//---
class="macro">#define DIAM_TO_MSZ_MJ_L       class="num">10.0 class=class="str">"cmt">//主要刻度 (较大) 的大小作为实体直径的百分比
class="macro">#define DIAM_TO_MSZ_MD_L       class="num">7.5  class=class="str">"cmt">//中等刻度 (较大) 的大小作为实体直径的百分比
class="macro">#define DIAM_TO_MSZ_MN_L       class="num">5.0  class=class="str">"cmt">//次要刻度 (较大) 的大小作为实体直径的百分比
class=class="str">"cmt">//---
class="macro">#define DIAM_TO_MFONT_SZ_S     class="num">4    class=class="str">"cmt">//刻度标签 (较小) 的字号作为实体直径的百分比
class="macro">#define DIAM_TO_MFONT_SZ_M     class="num">6.5 class=class="str">"cmt">//刻度标签 (中等) 的字号作为实体直径的百分比
class="macro">#define DIAM_TO_MFONT_SZ_L     class="num">10   class=class="str">"cmt">//刻度标签 (较大) 的字号作为实体直径的百分比
class="macro">#define DEF_COL_SCALE        clrBlack
class="macro">#define DEF_COL_MARK_FONT   clrBlack
class="macro">#define DEF_COL_CASE        clrMintCream
class="macro">#define DEF_COL_BORDER      clrLightSteelBlue
class="macro">#define DEF_COL_LAB         clrDarkGray
class="macro">#define DEF_COL_NCENTER     clrLightSteelBlue
class="macro">#define DEF_COL_NEEDLE      clrDimGray

◍ 给 CCanvas 补两把抗锯齿刀

极坐标转直角坐标画刻度时,把分数坐标四舍五入成整数会让标记发歪,原 LineAA 方法在图例 11(b) 里就能看到这种锯齿偏移。把输入参数 x1,y1,x2,y2 改成 double 类型的 LineAA2,直接传分数坐标,图例 11(c) 里歪斜明显消除。 Fill 不抗锯齿,用 LineAA 填闭合区又会漏填,图例 12(a) 就是半成品。Fill2 的改法是:不填背景色,而是填任何非边界色,这样能上浅色,但小锐角仍可能留空(图例 12(c))。 表针绘制上,先整层填表针色,再用 LineAA2 画三段合成针,最后用 Fill2 透明色填周围——不是最优但针型正确。图例 13(b) 的 FillTriangle 法在 90 度角有偏差且中心偏移,图例 13(c) 只画三段不填充算折中。外汇贵金属指标开发涉及高频重绘,这类自绘控件资源占用高,实盘前应在 MT5 策略测试器里跑帧率验证。 下面两段是核心改法代码,直接抄进你的 CCanvas 派生类就能编译。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 利用抗锯齿画线 (带样式) v.class="num">2                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvas::LineAA2(class="kw">const class="type">class="kw">double x1,class="kw">const class="type">class="kw">double y1,class="kw">const class="type">class="kw">double x2,class="kw">const class="type">class="kw">double y2,class="kw">const class="type">uint clr,class="kw">const class="type">uint style)
  {
class=class="str">"cmt">//--- 线超过图像边界
   if((x1<class="num">0 && x2<class="num">0) || (y1<class="num">0 && y2<class="num">0))
      class="kw">return;
   if(x1>=m_width && x2>=m_width)
      class="kw">return;
   if(y1>=m_height && y2>=m_height)
      class="kw">return;
class=class="str">"cmt">//--- 检查
   if(x1==x2 && y1==y2)
     {
      PixelSet(class="type">int(x1),class="type">int(y1),clr);
      class="kw">return;
     }
class=class="str">"cmt">//--- 设置线样式
   if(style!=UINT_MAX)
      LineStyleSet(style);
class=class="str">"cmt">//--- 初步计算
   class="type">class="kw">double dx=x2-x1;
   class="type">class="kw">double dy=y2-y1;
   class="type">class="kw">double xy=sqrt(dx*dx+dy*dy);
   class="type">class="kw">double xx=x1;
   class="type">class="kw">double yy=y1;
   class="type">uint   mask=class="num">1<<m_style_idx;
class=class="str">"cmt">//--- 设置像素
   dx/=xy;
   dy/=xy;
   do
     {
      if((m_style&mask)==mask)
         PixelSetAA(xx,yy,clr);
      xx+=dx;
      yy+=dy;
      mask<<=class="num">1;
      if(mask==0x1000000)
         mask=class="num">1;
     }
   class="kw">while(fabs(x2-xx)>=fabs(dx) && fabs(y2-yy)>=fabs(dy));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 以颜色填充闭合区域 (v.class="num">2)                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCanvas::Fill2(class="type">int x,class="type">int y,class="kw">const class="type">uint clr)
  {
class=class="str">"cmt">//--- 检查
   if(x<class="num">0 || x>=m_width || y<class="num">0 || y>=m_height)
      class="kw">return;
class=class="str">"cmt">//---
   class="type">int  index=y*m_width+x;
   class="type">uint old_clr=m_pixels[index];
class=class="str">"cmt">//--- 检查替换是否必要
   if(old_clr==clr)
      class="kw">return;
class=class="str">"cmt">//--- 使用哑元栈来模拟深度嵌套的递归调用
   class="type">int  stack[];

「洪水填充把邻域染色跑通了」

上面这段是像素堆栈洪水填充(flood fill)的核心循环,用非递归方式把连通区域一次性改写颜色,避免递归爆栈。栈大小按像素总数 ArraySize(m_pixels) 预分配,若 ArrayResize 返回 -1 直接 return,说明显存或内存不够,MT5 终端会静默跳过本次绘制。 循环里以 count 追踪栈中待处理点数,初始为 1 且仅压入起点 index。每次取 stack[i] 算 x 坐标(index%m_width),再分别判左、顶、右、底四个邻点:越界或已等于目标色 clr 就跳过,否则压栈、若原色是 old_clr 才让 count++,并立刻把该点写成 clr。这样只有真正属于旧色的连通块会被扩展,边界外像素不会被误染。 收尾的 ArrayFree(stack) 必须执行,否则每帧重绘都会留一块 total 长度的 int 数组,跑几分钟就可能把指标内存吃满。下面的 canvas 调用是另一件事:Fill(10,10,...) 用 needle 颜色和透明度铺底,三条 LineAA2 按 db_xbuf/db_ybuf 里的 0→1→2→0 画抗锯齿三角框,最后 Fill2(10,10,0) 清掉刚才的填充层,等于只留线框。 在 MT5 里把这段代码塞进自定义指标 OnDraw,把 m_width 设成画布宽、m_pixels 用 CanvasGetPixels 取回来,就能实时看哪块区域被 flood 改写;外汇和贵金属图表叠加这类自绘层时波动快,属于高风险操作,参数没调稳前别挂实盘窗口。

MQL5 / C++
  class="type">uint count=class="num">1;
  class="type">int  idx;
  class="type">int  total=ArraySize(m_pixels);
class=class="str">"cmt">//--- 为堆栈分配内存
  if(ArrayResize(stack,total)==-class="num">1)
     class="kw">return;
  stack[class="num">0]=index;
  m_pixels[index]=clr;
  for(class="type">uint i=class="num">0;i<count;i++)
     {
     index=stack[i];
     x=index%m_width;
     class=class="str">"cmt">//--- 左邻点
     idx=index-class="num">1;
     if(x>class="num">0 && m_pixels[idx]!=clr)
       {
       stack[count]=idx;
       if(m_pixels[idx]==old_clr)
          count++;
       m_pixels[idx]=clr;
       }
     class=class="str">"cmt">//--- 顶邻点
     idx=index-m_width;
     if(idx>=class="num">0 && m_pixels[idx]!=clr)
       {
       stack[count]=idx;
       if(m_pixels[idx]==old_clr)
          count++;
       m_pixels[idx]=clr;
       }
     class=class="str">"cmt">//--- 右邻点
     idx=index+class="num">1;
     if(x<m_width-class="num">1 && m_pixels[idx]!=clr)
       {
       stack[count]=idx;
       if(m_pixels[idx]==old_clr)
          count++;
       m_pixels[idx]=clr;
       }
     class=class="str">"cmt">//--- 底邻点
     idx=index+m_width;
     if(idx<total && m_pixels[idx]!=clr)
       {
       stack[count]=idx;
       if(m_pixels[idx]==old_clr)
          count++;
       m_pixels[idx]=clr;
       }
     }
class=class="str">"cmt">//--- 释放内存
  ArrayFree(stack);
  }
  n.canvas.Fill(class="num">10, class="num">10, ColorToARGB(n.needle.c, n.transparency));
  n.canvas.LineAA2(db_xbuf[class="num">0], db_ybuf[class="num">0], db_xbuf[class="num">1], db_ybuf[class="num">1], class="num">0);
  n.canvas.LineAA2(db_xbuf[class="num">1], db_ybuf[class="num">1], db_xbuf[class="num">2], db_ybuf[class="num">2], class="num">0);
  n.canvas.LineAA2(db_xbuf[class="num">2], db_ybuf[class="num">2], db_xbuf[class="num">0], db_ybuf[class="num">0], class="num">0);
  n.canvas.Fill2(class="num">10, class="num">10, class="num">0);
把表盘渲染诊断交给小布
这些表盘参数与资源占用的诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时渲染负荷,你只需专注策略信号本身。

常见问题

由所选锚点边角决定:选上边角则原点在参考对象之下,选下边角则在之上,水平方向同理按左右边角推演。
前者定义表盘外壳整体形状类别,后者仅控制边框线型与描边风格,二者在 GaugeSetCaseParameters 中分开配置。
可以,将编译后的自定义指标挂到品种页,小布会识别其图形资源并纳入渲染监控,不需要手动抄写参数。
默认填充在斜向区域会出现锯齿阶梯,边缘抗锯齿通过像素透明度过渡降低视觉噪点,但会轻微提升 CPU 占用。
把多处调用的尺寸与样式常量收拢为单点定义,改一处即可全局生效,避免枚举与函数参数失配。