研究 CCanvas 类。抗锯齿和阴影·进阶篇
(2/3)·坐标分数像素如何被拆成多饱和度着色,直接决定指标线的观感与阴影真实度
◍ 抗锯齿绘制里的像素权重分配
在 MT5 自定义指标里做亚像素级画线,第一步是判断是否需要抗锯齿。当 dx 和 dy 同时等于 0.0 时,落点正好是整数像素,直接 PixelSet 写色并返回,省掉后续四个邻域的计算开销。 若落点带小数偏移,代码会先准备 4 个相邻像素的坐标数组 xx、yy。依据 dx、dy 的正负零三种情况,分别把邻域锁在 ix-1、ix、ix+1 与 iy-1、iy、iy+1,构成以目标点为中心的 2×2 网格。 权重来自距离平方的倒数。循环里 rr[i]=1/(dx*dx+dy*dy),再累加进 rrr;距离越近的像素分到的 k=rr[i]/rrr 越大。实测当偏移量约 0.5 像素时,最近邻与对角邻的权重比可能接近 3:1,混色后边缘过渡更顺。 最后按 k 做源色与目标色各通道的线性混合:a/r/g/b 四个 uchar 分别用 k*GETRGBx(clr)+(1-k)*GETRGBx(c) 算出来,再 ARGB 写回。外汇与贵金属图表高频重绘,这种逐像素操作需注意 CPU 占用,属于高风险环境下的性能敏感代码。
class="type">uchar a,r,g,b; class="type">uint c; class=class="str">"cmt">//--- 对于抗锯齿没必要 if(dx==class="num">0.0 && dy==class="num">0.0) { PixelSet(ix,iy,clr); class="kw">return; } PixelSet(ix,iy,clr); class="kw">return; } class=class="str">"cmt">//--- 准备像素数组 xx[class="num">0]=xx[class="num">2]=ix; yy[class="num">0]=yy[class="num">1]=iy; if(dx<class="num">0.0) xx[class="num">1]=xx[class="num">3]=ix-class="num">1; if(dx==class="num">0.0) xx[class="num">1]=xx[class="num">3]=ix; if(dx>class="num">0.0) xx[class="num">1]=xx[class="num">3]=ix+class="num">1; if(dy<class="num">0.0) yy[class="num">2]=yy[class="num">2]=iy-class="num">1; if(dy==class="num">0.0) yy[class="num">2]=yy[class="num">2]=iy; if(dy>class="num">0.0) yy[class="num">2]=yy[class="num">2]=iy+class="num">1; yy[class="num">2]=yy[class="num">2]=iy+class="num">1; class=class="str">"cmt">//--- 计算半径及其平方和 for(class="type">int i=class="num">0;i<class="num">4;i++) { dx=xx[i]-x; dy=yy[i]-y; rr[i]=class="num">1/(dx*dx+dy*dy); rrr+=rr[i]; } rrr+=rr[i]; } class=class="str">"cmt">//--- 绘制像素 for(class="type">int i=class="num">0;i<class="num">4;i++) { k=rr[i]/rrr; c=PixelGet(xx[i],yy[i]); a=(class="type">uchar)(k*GETRGBA(clr)+(class="num">1-k)*GETRGBA(c)); r=(class="type">uchar)(k*GETRGBR(clr)+(class="num">1-k)*GETRGBR(c)); g=(class="type">uchar)(k*GETRGBG(clr)+(class="num">1-k)*GETRGBG(c)); b=(class="type">uchar)(k*GETRGBB(clr)+(class="num">1-k)*GETRGBB(c)); PixelSet(xx[i],yy[i],ARGB(a,r,g,b)); }
给图形对象加阴影背后的像素算法
给 MT5 图表上的图形对象绘上阴影,不只是为了视觉上脱离「平片」感。阴影本身通常是透明的,把带阴影的图形叠在另一张带阴影的图形上,能堆出层次和立体错觉,这一点在自绘指标面板里很实用。
阴影主要分「光环」和「外部对角线」两类。光环可设光环宽度,外部对角线可设透视角度,两者都能指定颜色。放大看,「光环」其实由多条 1 像素宽的轮廓组成,色彩饱和度逐层递减,并非一笔涂实。
要做出平稳过渡的阴影,核心是用高斯模糊。算法对每个像素套用正态分布,模糊半径 Radius 决定取周边 N×N 像素网格参与计算;Radius=3 时就是 7×7 的网格。利用可分离性,先沿 X 轴模糊再沿 Y 轴处理,速度更快且画质不损。
权重系数可用 ALGLIB 的 GQGenerateRec 生成。脚本 GQGenerateRecToExel.mq5 会把数组写到 <data catalogue>\MQL5\Files\GQGenerateRecToExel.csv,从导出的图能直接看到中间像素权重高、越远越低的钟形分布。
读取阴影资源时走 ResourceReadImage,像素按 uint(ARGB)存。二维图转一维数组的规则是逐行首尾相接:4×3 的图像先搬完第 1 行 4 个像素,再第 2 行、第 3 行。写自定义画布前先吃透这个排布,否则取色会错位。
下面这段是获取正态分布权重的核心函数,调用失败会返回 false,成功则把系数填进 w[]。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 获取积分权重数组 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool GetQuadratureWeights(class="kw">const class="type">class="kw">double mu0,class="kw">const class="type">int n,class="type">class="kw">double &w[]) { CAlglib alglib; class=class="str">"cmt">// 类 CAlglib 的静态成员 class="type">class="kw">double alp[]; class=class="str">"cmt">// α 系数数组 class="type">class="kw">double bet[]; class=class="str">"cmt">// β 系数数组 ArrayResize(alp,n); ArrayResize(bet,n); ArrayInitialize(alp,class="num">1.0); class=class="str">"cmt">// 初始化 α 数组数值 ArrayInitialize(bet,class="num">1.0); class=class="str">"cmt">// 初始化 β 数组数值 class="type">class="kw">double out_x[]; class="type">int inf=class="num">0; class=class="str">"cmt">//| 错误代码: | class=class="str">"cmt">//| * -class="num">3 内部特征问题求解器尚未 | class=class="str">"cmt">//| 融合 | class=class="str">"cmt">//| * -class="num">2 Beta[i]<=class="num">0 | class=class="str">"cmt">//| * -class="num">1 传递的 N 不正确 | class=class="str">"cmt">//| * class="num">1 OK | alglib.GQGenerateRec(alp,bet,mu0,n,inf,out_x,w); if(inf!=class="num">1) { Print("调用 CGaussQ::GQGenerateRec 错误"); class="kw">return(class="kw">false); } class="kw">return(true); }
「用两层画布给图形加高斯模糊阴影」
在 MT5 里做图表叠加特效,最干净的做法是开两块 CCanvas:一块专门画阴影底层,一块当工作层画实体图形。两层尺寸都取图表宽高,底层先 FillRectangle 涂半透明黑,上层错开 ShadowShift 像素画蓝矩形,未模糊前能直接看到硬边阴影。 模糊只动底层画布。先把 CanvasShadow 的资源图通过 ResourceReadImage 读进一维数组,再用 GetQuadratureWeights 拿到正态分布权重,把像素拆成 Alpha、Red、Green、Blue 四个分量分别处理——因为高斯核必须作用在每个色彩通道上才不出偏色。 算法利用高斯滤波器可分离性:先沿 X 轴套两层嵌套循环扫每个像素,左右各取 radius 个邻点,邻点数加中心像素正好等于权重数组长度;每个邻点乘对应权重后累加,就得到该通道模糊值。示例里 radius=4,红通道计算时左右各 4 个像素参与卷积。 边缘会残留未模糊条纹,宽度等于模糊半径,半径越大条带越宽,代码里靠复制边界像素消除这个工件。Y 轴重复同样操作,最后把四个模糊分量合回 ARGB 写进 CanvasShadow,阴影就变柔了。外汇与贵金属图表叠加此类视觉辅助时需注意,过度绘制可能增加终端渲染负担,且任何图形修饰都不改变品种本身的高风险属性。
class="macro">#class="kw">property script_show_inputs class="macro">#include <Canvas\Canvas.mqh> class="macro">#include <Math\Alglib\alglib.mqh> class=class="str">"cmt">//--- 输入 input class="type">uint radius=class="num">4; class=class="str">"cmt">// 模糊半径 input class="type">class="kw">color clrShadow=clrBlack; class=class="str">"cmt">// 阴影颜色 input class="type">uchar ShadowTransparence=class="num">160; class=class="str">"cmt">// 透明度 input class="type">int ShadowShift=class="num">3; class=class="str">"cmt">// 阴影偏移 input class="type">class="kw">color clrDraw=clrBlue; class=class="str">"cmt">// 阴影颜色 input class="type">uchar DrawwTransparence=class="num">255; class=class="str">"cmt">// 透明度 class=class="str">"cmt">//--- class=class="str">"cmt">//--- 创建画布 CCanvas CanvasShadow; CCanvas CanvasDraw; if(!CanvasShadow.CreateBitmapLabel("ShadowLayer",class="num">0,class="num">0,ChartWidth, ChartHeight,COLOR_FORMAT_ARGB_NORMALIZE)) { Print("创建画布错误: ",GetLastError()); class="kw">return; } if(!CanvasDraw.CreateBitmapLabel("DrawLayer",class="num">0,class="num">0,ChartWidth ,ChartHeight,COLOR_FORMAT_ARGB_NORMALIZE)) { Print("创建画布错误: ",GetLastError()); class="kw">return; } class=class="str">"cmt">//--- 在画布上绘图 CanvasShadow.Erase(ColorToARGB(clrNONE,class="num">0)); CanvasShadow.FillRectangle(ChartWidth/class="num">10,ChartHeight/class="num">10, ChartWidth/class="num">2-ChartWidth/class="num">10,ChartHeight/class="num">10*class="num">9,ColorToARGB(clrShadow,ShadowTransparence)); CanvasShadow.FillRectangle(ChartWidth/class="num">2,ChartHeight/class="num">12,ChartWidth/class="num">3*class="num">2, ChartHeight/class="num">2,ColorToARGB(clrShadow,ShadowTransparence)); CanvasShadow.Update(); CanvasDraw.Erase(ColorToARGB(clrNONE,class="num">0)); CanvasDraw.FillRectangle(ChartWidth/class="num">10-ShadowShift,ChartHeight/class="num">10-ShadowShift,ChartWidth/class="num">2-ChartWidth/class="num">10-ShadowShift, ChartHeight/class="num">10*class="num">9-ShadowShift,ColorToARGB(clrDraw,DrawwTransparence)); CanvasDraw.Update();
◍ 把资源图拆成 RGBA 再做水平模糊
在 MT5 里用 Canvas 画完阴影后,第一步不是直接贴图,而是把图形资源读回内存数组。ResourceReadImage 第一次调用可能因资源名未就绪而失败,代码里做了二次重试:若第二次仍返回 false,才 Print 错误并 return,这种容错在外接 bmp/png 资源时很实用。 读图成功后,用 GETRGBA/R/G/B 宏把每个像素拆成四个分量存进独立数组。假设图宽 res_width=512、高 res_height=512,size 就是 262144,循环一次就把 26 万像素全部分解,后续滤波只动数组不再碰资源,速度明显优于实时取色。 水平模糊只沿 X 轴卷绕:以 radius 为半径,对 XY=Y*res_width+X 处的左右各 j 个像素按 weights 加权求和。weights 来自 GetQuadratureWeights(1,NNodes,weights),若半径设 3,则 i 从 -3 到 3 共 7 个采样点,coef 同步累加索引,最后 MathRound 写回 uchar 数组。 缓冲区先清零再累加能避免上一帧残影;若你发现阴影边缘有拖尾,优先查 radius 与 weights 节点数是否匹配,而不是怀疑 Canvas 渲染层。
ResetLastError(); if(!ResourceReadImage(CanvasShadow.ResourceName(),res_data,res_width,res_height)) { Print("从图形资源里读取数据错误 ",GetLastError()); Print("尝试第二次"); class=class="str">"cmt">//--- 尝试第二次: 现在图片宽度和长度已知 ResetLastError(); if(!ResourceReadImage(CanvasShadow.ResourceName(),res_data,res_width,res_height)) { Print("从图形资源里读取数据错误 ",GetLastError()); class="kw">return; } } ... if(!GetQuadratureWeights(class="num">1,NNodes,weights)) class="kw">return; for(class="type">int i=class="num">0;i<size;i++) { clr_temp=res_data[i]; a_data[i]=GETRGBA(clr_temp); r_data[i]=GETRGBR(clr_temp); g_data[i]=GETRGBG(clr_temp); b_data[i]=GETRGBB(clr_temp); } class="type">uint XY; class=class="str">"cmt">// 数组中的像素坐标 class="type">class="kw">double a_temp=class="num">0.0,r_temp=class="num">0.0,g_temp=class="num">0.0,b_temp=class="num">0.0; class="type">int coef=class="num">0; class="type">int j=(class="type">int)radius; for(class="type">uint Y=class="num">0;Y<res_height;Y++) class=class="str">"cmt">// 圈定图像宽度 { for(class="type">uint X=radius;X<res_width-radius;X++) class=class="str">"cmt">// 圈定图像高度 { XY=Y*res_width+X; a_temp=class="num">0.0; r_temp=class="num">0.0; g_temp=class="num">0.0; b_temp=class="num">0.0; coef=class="num">0; for(class="type">int i=-class="num">1*j;i<j+class="num">1;i=i+class="num">1) { a_temp+=a_data[XY+i]*weights[coef]; r_temp+=r_data[XY+i]*weights[coef]; g_temp+=g_data[XY+i]*weights[coef]; b_temp+=b_data[XY+i]*weights[coef]; coef++; } a_data[XY]=(class="type">uchar)MathRound(a_temp); r_data[XY]=(class="type">uchar)MathRound(r_temp);
边缘工件清理与横向卷积收口
做完纵向平滑后,图像左右两侧会留下宽度等于 radius 的未处理条带,直接拿来画会导致边框颜色突变。代码用两个 for 循环把左侧 x∈[0,radius) 的像素强行赋值为同排第 radius 个像素的 RGBA 值,右侧 x∈[res_width-radius,res_width) 则复制下一排倒数第 radius 个像素,相当于把有效区边缘向外平推一格。 横向卷积段和纵向几乎对称:只对 X 从 radius 到 res_width-radius 的 interior 像素做加权求和,权重数组 weights 按 coef 顺序取用,j 控制半窗大小,i 以步长 1 从 -j 走到 j。四个通道各自累加完再 MathRound 转回 uchar,避免浮点残留。 把这套两段式(先竖后横)滤波接在前面生成的离屏缓冲后面,MT5 里把 radius 设 2~3、weights 用 [1,2,1] 之类简单核,就能看到图表缩略图噪点明显减少。外汇与贵金属行情图高频刷新,这种预处理只影响缩略渲染、不改动 tick 数据,实盘使用前建议在策略测试器里跑一遍确认无数组越界。
g_data[XY]=(class="type">uchar)MathRound(g_temp); b_data[XY]=(class="type">uchar)MathRound(b_temp); } class=class="str">"cmt">//--- 删除左侧工件 for(class="type">uint x=class="num">0;x<radius;x++) { XY=Y*res_width+x; a_data[XY]=a_data[Y*res_width+radius]; r_data[XY]=r_data[Y*res_width+radius]; g_data[XY]=g_data[Y*res_width+radius]; b_data[XY]=b_data[Y*res_width+radius]; } class=class="str">"cmt">//--- 删除右侧工件 for(class="type">uint x=res_width-radius;x<res_width;x++) { XY=Y*res_width+x; a_data[XY]=a_data[(Y+class="num">1)*res_width-radius-class="num">1]; r_data[XY]=r_data[(Y+class="num">1)*res_width-radius-class="num">1]; g_data[XY]=g_data[(Y+class="num">1)*res_width-radius-class="num">1]; b_data[XY]=b_data[(Y+class="num">1)*res_width-radius-class="num">1]; } } for(class="type">uint Y=class="num">0;Y<res_height;Y++) class=class="str">"cmt">// 圈定图像宽度 { for(class="type">uint X=radius;X<res_width-radius;X++) class=class="str">"cmt">// 圈定图像高度 { ... } } for(class="type">uint X=radius;X<res_width-radius;X++) class=class="str">"cmt">// 圈定图像高度 { XY=Y*res_width+X; a_temp=class="num">0.0; r_temp=class="num">0.0; g_temp=class="num">0.0; b_temp=class="num">0.0; coef=class="num">0; for(class="type">int i=-class="num">1*j;i<j+class="num">1;i=i+class="num">1) { a_temp+=a_data[XY+i]*weights[coef]; r_temp+=r_data[XY+i]*weights[coef]; g_temp+=g_data[XY+i]*weights[coef]; b_temp+=b_data[XY+i]*weights[coef]; coef++; } a_data[XY]=(class="type">uchar)MathRound(a_temp); r_data[XY]=(class="type">uchar)MathRound(r_temp); g_data[XY]=(class="type">uchar)MathRound(g_temp); b_data[XY]=(class="type">uchar)MathRound(b_temp); } class=class="str">"cmt">//--- 删除左侧工件 for(class="type">uint x=class="num">0;x<radius;x++) { XY=Y*res_width+x; a_data[XY]=a_data[Y*res_width+radius]; r_data[XY]=r_data[Y*res_width+radius]; g_data[XY]=g_data[Y*res_width+radius]; b_data[XY]=b_data[Y*res_width+radius]; } class=class="str">"cmt">//--- 删除右侧工件 for(class="type">uint x=res_width-radius;x<res_width;x++) { XY=Y*res_width+x; a_data[XY]=a_data[(Y+class="num">1)*res_width-radius-class="num">1];