使用 CCanvas 类绘制刻度表盘·进阶篇
(2/3)· 接上篇基础铺垫,这一篇把表盘定位、枚举宏与 CCanvas 源码级修改一次讲透
接上篇,我们继续深挖 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 看刻度排布,是最快的验证路径。
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 错误对象数增长,再挂真账户——外汇和贵金属杠杆高,图表对象堆积可能拖慢终端。
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 就能换盘底,不用动绘制逻辑。外汇与贵金属行情波动剧烈,这类视觉组件只辅助读图,不构成任何方向判断。
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 派生类就能编译。
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 改写;外汇和贵金属图表叠加这类自绘层时波动快,属于高风险操作,参数没调稳前别挂实盘窗口。
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);