DoEasy 函数库中的图形(第七十七部分):阴影对象类·进阶篇
(2/3)· 直接画阴影重绘太烧算力?拆出独立对象让终端自己叠透明度
面板配色与画布位移的底层定义
在 MT5 自定义控件库里,配色方案通常用二维数组固化。下面这段把两套主题(Blue steel、Light cyan gray)各占一行,每行 4 个 color 值分别对应表单背景、边框、外描边矩形、阴影。 TOTAL_COLOR_THEMES 与 TOTAL_COLOR_THEME_COLORS 是两个编译期常量,数组维度必须与之严格匹配,否则 OnInit 阶段就会报数组越界。从代码看,Blue steel 背景用 C'134,160,181'(RGB 134/160/181 的冷钢蓝),Light cyan gray 用 C'181,196,196'(偏青灰),两者阴影都取 clrGray,说明阴影不参与主题切换。 画布交互靠 Move(x,y,redraw) 完成坐标偏移,redraw 默认 false,意味着你手动算好位置后若不传 true,控件不会立即重绘。 亮度与饱和度调整给了多组重载:ChangeColorLightness 既能吃 uint 型 ARGB 也能吃 color 型,后者还可附带 uchar opacity 控制透明度;ChangeColorSaturation 只接受 uint。实盘外接显示器校色时,用这些函数动态压暗面板,可能在强光环境下减少视觉干扰。外汇与贵金属波动剧烈,这类 UI 微调不改变信号逻辑,但高频盯盘者值得在策略测试仪之外单独验证。
class="type">color array_color_themes[TOTAL_COLOR_THEMES][TOTAL_COLOR_THEME_COLORS]= { class=class="str">"cmt">//--- Parameters of the "Blue steel" class="type">color scheme { C&class="macro">#x27;class="num">134,class="num">160,class="num">181&class="macro">#x27;, class=class="str">"cmt">// Form background class="type">color C&class="macro">#x27;class="num">134,class="num">160,class="num">181&class="macro">#x27;, class=class="str">"cmt">// Form frame class="type">color clrDimGray, class=class="str">"cmt">// Form outline rectangle class="type">color clrGray, class=class="str">"cmt">// Form shadow class="type">color }, class=class="str">"cmt">//--- Parameters of the "Light cyan gray" class="type">color scheme { C&class="macro">#x27;class="num">181,class="num">196,class="num">196&class="macro">#x27;, class=class="str">"cmt">// Form background class="type">color C&class="macro">#x27;class="num">181,class="num">196,class="num">196&class="macro">#x27;, class=class="str">"cmt">// Form frame class="type">color clrGray, class=class="str">"cmt">// Form outline rectangle class="type">color clrGray, class=class="str">"cmt">// Form shadow class="type">color }, }; class=class="str">"cmt">//--- Update the coordinates(shift the canvas) class="type">bool Move(const class="type">int x,const class="type">int y,const class="type">bool redraw=false); class=class="str">"cmt">//--- Change the brightness of(class="num">1) ARGB and(class="num">2) COLOR by a specified amount class="type">uint ChangeColorLightness(const class="type">uint clr,const class="type">class="kw">double change_value); class="type">color ChangeColorLightness(const class="type">color colour,const class="type">uchar opacity,const class="type">class="kw">double change_value); class=class="str">"cmt">//--- Change the brightness of(class="num">1) ARGB and(class="num">2) COLOR by a specified amount class="type">uint ChangeColorLightness(const class="type">uint clr,const class="type">class="kw">double change_value); class="type">color ChangeColorLightness(const class="type">color colour,const class="type">class="kw">double change_value); class=class="str">"cmt">//--- Change the saturation of(class="num">1) ARGB and(class="num">2) COLOR by a specified amount class="type">uint ChangeColorSaturation(const class="type">uint clr,const class="type">class="kw">double change_value);
◍ 图形元素里的色彩饱和度与亮度微调
在 MT5 自定义指标或面板开发中,直接改颜色常量很难做出层次感。CGCnvElement 类提供了两组重载方法,分别针对 ARGB(uint)与 COLOR(color)类型做饱和度偏移,逻辑完全一致:先拆通道、转 HSL、改 S 分量、再转回。 饱和度调整函数里,change_value 以百分比思路传入,内部乘 0.01 后叠加到原饱和度 s 上。比如传 30 即尝试 +0.30 饱和度,上限锁 1.0、下限锁 0.0,避免溢出导致颜色异常。 color 版本先用 ColorToARGB(colour,0) 把 COLOR 转成带透明度的 uint,再走和 uint 版本相同的 RGB→HSL→HSL→RGB 流程,最后用 CColors::RGBToColor 合回 color 类型返回。 亮度函数 ChangeColorLightness 前半段和饱和度函数同构,只是后续改的是 l 分量而非 s,开发者可以复用同一套拆色模板快速扩展其他 HSL 操作。 开 MT5 新建一个面板类继承 CGCnvElement,传 change_value=50 对比按钮悬停前后色块,能直观验证饱和度提升约 0.5 的可见差异。外汇与贵金属图表美化属低风险辅助,但自动化面板若绑定下单逻辑仍需注意执行风险。
class="type">uint CGCnvElement::ChangeColorSaturation(const class="type">uint clr,const class="type">class="kw">double change_value) { if(change_value==class="num">0.0) class="kw">return clr; class="type">class="kw">double a=GETRGBA(clr); class="type">class="kw">double r=GETRGBR(clr); class="type">class="kw">double g=GETRGBG(clr); class="type">class="kw">double b=GETRGBB(clr); class="type">class="kw">double h=class="num">0,s=class="num">0,l=class="num">0; CColors::RGBtoHSL(r,g,b,h,s,l); class="type">class="kw">double ns=s+change_value*class="num">0.01; if(ns>class="num">1.0) ns=class="num">1.0; if(ns<class="num">0.0) ns=class="num">0.0; CColors::HSLtoRGB(h,ns,l,r,g,b); class="kw">return ARGB(a,r,g,b); } class="type">color CGCnvElement::ChangeColorSaturation(const class="type">color colour,const class="type">class="kw">double change_value) { if(change_value==class="num">0.0) class="kw">return colour; class="type">uint clr=::ColorToARGB(colour,class="num">0); class="type">class="kw">double r=GETRGBR(clr); class="type">class="kw">double g=GETRGBG(clr); class="type">class="kw">double b=GETRGBB(clr); class="type">class="kw">double h=class="num">0,s=class="num">0,l=class="num">0; CColors::RGBtoHSL(r,g,b,h,s,l); class="type">class="kw">double ns=s+change_value*class="num">0.01; if(ns>class="num">1.0) ns=class="num">1.0; if(ns<class="num">0.0) ns=class="num">0.0; CColors::HSLtoRGB(h,ns,l,r,g,b); class="kw">return CColors::RGBToColor(r,g,b); } class="type">color CGCnvElement::ChangeColorLightness(const class="type">color colour,const class="type">class="kw">double change_value) { if(change_value==class="num">0.0) class="kw">return colour; class="type">uint clr=::ColorToARGB(colour,class="num">0); class="type">class="kw">double r=GETRGBR(clr); class="type">class="kw">double g=GETRGBG(clr); class="type">class="kw">double b=GETRGBB(clr); class="type">class="kw">double h=class="num">0,s=class="num">0,l=class="num">0;
「调亮度的两套写法与元素属性钩子」
在 MT5 的画布元素类里,亮度调整分 RGB 和 ARGB 两种入口。前者直接吃 CColors::RGBToColor 的返回值,后者把 alpha 通道单独拆出来用 ARGB(a,r,g,b) 拼回,做半透明控件时别调错函数。 change_value 以百分比思路工作:代码里乘了 0.01,意味着传 10 代表把亮度加减 0.1(HSL 的 L 通道量程是 0.0~1.0)。越界会被硬截断,nl>1.0 归 1.0、nl<0.0 归 0.0,所以传 200 也不会溢出成异常色。 顺带把元素的可交互标志也暴露了:SetMovable / SetActive / SetID / SetNumber 全走 SetProperty 统一入口,唯独 SetShadow 直接写 m_shadow 成员,说明阴影是脱离通用属性表的特殊字段。 开 MT5 自建个 CGCnvElement 派生类,把 ChangeColorLightness 抄进去,给按钮背景传 change_value=15,肉眼能看到 L 通道大约抬了 0.15,外汇面板的高亮态可以这么搓。
CColors::RGBtoHSL(r,g,b,h,s,l); class="type">class="kw">double nl=l+change_value*class="num">0.01; if(nl>class="num">1.0) nl=class="num">1.0; if(nl<class="num">0.0) nl=class="num">0.0; CColors::HSLtoRGB(h,s,nl,r,g,b); class="kw">return CColors::RGBToColor(r,g,b); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Change the ARGB class="type">color brightness by a specified value | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">uint CGCnvElement::ChangeColorLightness(const class="type">uint clr,const class="type">class="kw">double change_value) { if(change_value==class="num">0.0) class="kw">return clr; class="type">class="kw">double a=GETRGBA(clr); class="type">class="kw">double r=GETRGBR(clr); class="type">class="kw">double g=GETRGBG(clr); class="type">class="kw">double b=GETRGBB(clr); class="type">class="kw">double h=class="num">0,s=class="num">0,l=class="num">0; CColors::RGBtoHSL(r,g,b,h,s,l); class="type">class="kw">double nl=l+change_value*class="num">0.01; if(nl>class="num">1.0) nl=class="num">1.0; if(nl<class="num">0.0) nl=class="num">0.0; CColors::HSLtoRGB(h,s,nl,r,g,b); class="kw">return ARGB(a,r,g,b); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- Set the flag of(class="num">1) object moveability, (class="num">2) activity, (class="num">3) element ID, (class="num">4) element index in the list and(class="num">5) shadow presence class="type">void SetMovable(const class="type">bool flag) { this.SetProperty(CANV_ELEMENT_PROP_MOVABLE,flag); } class="type">void SetActive(const class="type">bool flag) { this.SetProperty(CANV_ELEMENT_PROP_ACTIVE,flag); } class="type">void SetID(const class="type">int id) { this.SetProperty(CANV_ELEMENT_PROP_ID,id); } class="type">void SetNumber(const class="type">int number) { this.SetProperty(CANV_ELEMENT_PROP_NUM,number); } class="type">void SetShadow(const class="type">bool flag) { this.m_shadow=flag; } class=class="str">"cmt">//--- Return the shift(class="num">1) of the left, (class="num">2) right, (class="num">3) top and(class="num">4) bottom edge of the element active area class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| The methods of filling, clearing and updating raster data | class=class="str">"cmt">//+------------------------------------------------------------------+
画布元素的擦除与渐变填充实现
在 MT5 的自定义图形库里,CGCnvElement 类提供了三套 Erase 重载:纯色单参数擦除、多色渐变擦除、以及无参完全清空。渐变版本接受 color 数组、opacity(0~255)、vgradient 方向标记、cycle 循环标记和 redraw 重绘标记,实际开发中靠它做背景过渡比手绘矩形省事得多。 核心逻辑在 Erase(color &colors[],...) 里:先取数组长度,若小于 2 则退化处理——空数组直接调无参 Erase 清空,单色则调单色重载。数组达标后,按 vgradient 决定以 Height() 还是 Width() 作为渐变总段数,再交给 CColors::Gradient 展开成逐像素颜色序列。 循环里用 switch 切方向:横向渐变(vgradient=false)对每个 i 调 DrawLineVertical 画一条从顶到底的竖线;纵向则反之。opacity 直接透传到底层绘制,不另做归一化,所以传 255 才是完全不透明。 验证方法很简单:在 EA 的 OnChartEvent 里构造一个 2 色数组塞给 Erase,把 cycle 设为 true,看元素背景是否无缝循环渐变;外汇与贵金属图表叠加此类自绘层时需注意,过度重绘会拖慢 tick 响应,属高风险界面的性能隐患。
class="type">void CGCnvElement::Erase(class="type">color &colors[],const class="type">uchar opacity,const class="type">bool vgradient=true,const class="type">bool cycle=false,const class="type">bool redraw=false) { class=class="str">"cmt">//--- Check the size of the class="type">color array class="type">int size=::ArraySize(colors); class=class="str">"cmt">//--- If there are less than two colors in the array if(size<class="num">2) { class=class="str">"cmt">//--- if the array is empty, erase the background completely and leave if(size==class="num">0) { this.Erase(redraw); class="kw">return; } class=class="str">"cmt">//--- in case of one class="type">color, fill the background with this class="type">color and opacity, and leave this.Erase(colors[class="num">0],opacity,redraw); class="kw">return; } class=class="str">"cmt">//--- Declare the receiver array class="type">color out[]; class=class="str">"cmt">//--- Set the gradient size depending on the filling direction(vertical/horizontal) class="type">int total=(vgradient ? this.Height() : this.Width()); class=class="str">"cmt">//--- and get the set of colors in the receive array CColors::Gradient(colors,out,total,cycle); total=::ArraySize(out); class=class="str">"cmt">//--- In the loop by the number of colors in the array for(class="type">int i=class="num">0;i<total;i++) { class=class="str">"cmt">//--- depending on the filling direction class="kw">switch(vgradient) { class=class="str">"cmt">//--- Horizontal gradient - draw vertical segments from left to right with the class="type">color from the array case false : DrawLineVertical(i,class="num">0,this.Height()-class="num">1,out[i],opacity); break;
◍ 纵向渐变里的逐行水平绘制
在自定义图形对象的绘制逻辑里,default 分支专门处理纵向渐变:从顶部往下按行扫描,每一行用数组里对应的颜色画一条贯穿宽度的水平线段。 具体看这段实现,DrawLineHorizontal(0, this.Width()-1, i, out[i], opacity) 中,起止横坐标固定为 0 到 Width()-1,i 是当前行号,out[i] 是该行颜色,opacity 控制整体不透明度。 循环结束后若 redraw 为真,this.Update(redraw) 会触发画布重绘;外汇与贵金属图表叠加此类渐变层时波动剧烈,视觉信号仅作辅助,实盘须自行评估高风险。
class=class="str">"cmt">//--- Vertical gradient - draw horizontal segments downwards with the class="type">color from the array class="kw">default: DrawLineHorizontal(class="num">0,this.Width()-class="num">1,i,out[i],opacity); break; } } class=class="str">"cmt">//--- If specified, update the canvas this.Update(redraw); }
「阴影对象类」
在函数库图形对象的 \MQL5\Include\DoEasy\Objects\ Graph\ 目录中,创建含 CShadowObj 类的新文件 ShadowObj.mqh 。 图形元素的文件 和 ALGLIB 数值分析库文件 均应包含在文件之中。 该类应继承自图形元素对象类 : 在类的私密部分, 声明存储颜色和阴影不透明度的变量 ,以及 类的操作方法 : 此处的 DrawShadowFigureRect() 方法根据会话窗对象的维度绘制一个非模糊形状,并为该对象绘制所需投射的阴影。 GetQuadratureWeights() 方法应用 ALGLIB 库 来计算和返回 DrawShadowFigureRect() 方法绘制模糊造型所需的权重比数组。 造型的模糊由 GaussianBlur() 方法执行。 所有方法均在下面研讨。 在类的公开部分,声明 参数型构造函数 、 返回支持对象属性标志的方法 (直到两个方法都返回 true )、和 绘制阴影的方法 ,并 编写简化访问阴影对象属性的方法 : 我们来更详细地研讨类方法的结构。 参数型构造函数: 构造函数接收图表的 ID、创建阴影对象的子窗口索引、其名称、左上角的坐标及其尺寸。 在初始化清单中,调用图形元素类的受保护构造函数,在方法参数里 传递元素类型 — shadow 对象 ,和其他参数。 在构造函数主体中,设置 省缺的对象背景 、 完全透明 、和 对象非活跃标志 (阴影对象不应以任何方式对外部影响做出反应)。 在画布上绘制的阴影默认不透明度设置为 127 。 这是一个半透明的阴影。 接下来, 计算默认阴影颜色 。 这个图表背景颜色表示 100 个单位中的 50 个单位变暗了。 在此,我们首先将图表背景颜色转换为灰色阴影,然后再把生成的颜色变暗。 绘制阴影的对象不应有投射,故此 将其阴影标志设置为 false , 将对象可见性标志设置为 true ,并清除画布。 该方法绘制阴影对象: 整个方法逻辑在代码的注释中均有讲述。 该方法首先绘制一个普通矩形并填充阴影颜色。 计算矩形的宽度和高度,从而其尺寸可为会话窗对象投射阴影。 接下来,运用高斯方法绘制模糊矩形,将阴影对象相对于需投射阴影的造型对象按照指定的偏移量平移,并刷新阴影对象所在的画布。 该方法绘制物体阴影会话窗: 在此,我们在 X/Y 坐标中绘制的矩形等于 OUTER_AREA_SIZE 常量值。 第二个 X/Y 坐标的计算,依据第一个坐标的偏移量 + 宽度(高度)减 1。 造型绘制完毕后,画布会更新。 该方法运用高斯方法绘制模糊造型: 方法逻辑已在代码注释中描述。 您可从借取该方法的 文章 中找到更多详细信息。 该方法返回权重比数组: 该方法调用 ALGLIB 数值分析库 计算模糊率,并将它们设置在通过引用传递的 权重 数组当中。 在以下文章章节中 可找到详细信息。 至此,第一个版本的阴影对象类开发完毕。 现在我们需要实现直接从会话窗对象快速创建和绘制阴影的能力。 打开会话窗对象类的 \MQL5\Include\DoEasy\Objects\Graph\ Form.mqh ,并进行必要的改进。 为了让会话窗对象类能看到阴影对象类,请将最近创建的阴影类的文件包含到其中: 从类的私密部分, 删除存储会话窗阴影颜色的变量 : 现在,阴影颜色会被存储在阴影对象类之中。 结果就是,我们的会话窗对象能够在其中创建新的图形元素对象,并将它们附加到其所依赖的对象列表当中。 换句话说,这些新创建的对象完全依赖,并从属于造型对象。 会话窗对象将能够管理它们。 为