DoEasy 函数库中的图形(第七十七部分):阴影对象类·进阶篇
🌑

DoEasy 函数库中的图形(第七十七部分):阴影对象类·进阶篇

(2/3)· 直接画阴影重绘太烧算力?拆出独立对象让终端自己叠透明度

含代码示例 第 2/3 篇
很多人把阴影直接画在会话窗画布上,结果一改坐标就整块重绘、CPU 占用直接飙起来。阴影和主体重叠还要逐像素算透明度合并,纯属给自己加无谓负担。用单独对象承载阴影,终端自动处理层叠,省下的算力留给策略更划算。

面板配色与画布位移的底层定义

在 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 微调不改变信号逻辑,但高频盯盘者值得在策略测试仪之外单独验证。

MQL5 / C++
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 的可见差异。外汇与贵金属图表美化属低风险辅助,但自动化面板若绑定下单逻辑仍需注意执行风险。

MQL5 / C++
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,外汇面板的高亮态可以这么搓。

MQL5 / C++
  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 响应,属高风险界面的性能隐患。

MQL5 / C++
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) 会触发画布重绘;外汇与贵金属图表叠加此类渐变层时波动剧烈,视觉信号仅作辅助,实盘须自行评估高风险。

MQL5 / C++
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 ,并进行必要的改进。 为了让会话窗对象类能看到阴影对象类,请将最近创建的阴影类的文件包含到其中: 从类的私密部分, 删除存储会话窗阴影颜色的变量 : 现在,阴影颜色会被存储在阴影对象类之中。 结果就是,我们的会话窗对象能够在其中创建新的图形元素对象,并将它们附加到其所依赖的对象列表当中。 换句话说,这些新创建的对象完全依赖,并从属于造型对象。 会话窗对象将能够管理它们。 为

把阴影层管理交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到对象层级与重绘开销提示,你只管调参数。

常见问题

模糊半径 4 像素时,5 像素余量会在画布边缘留下残影污迹;经验值 16 像素可让阴影完全透明过渡,避免背景脏斑。
阴影对象类从图形元素对象类继承,与会话窗对象同属基准图形元素衍生后代,会话窗内提供快速创建和改属性方法。
它提供数值分析库的高斯模糊方法,用来对独立阴影对象做柔化投影,具体调用方式见文内 Data.mqh 改动与测试节。
目前小布内置的是品种页对象层级与重绘开销提示,不直接编译库类;你可以把本文示例跑通后对照小布的开销读数验证效果。
需额外管控一个底层对象的坐标与生命周期,但换来的是改坐标零重绘和终端自动透明度合并。