![DoEasy 函数库中的图形(第七十七部分):阴影对象类](https://c.mql5.com/i/articles/overlay.png)
DoEasy 函数库中的图形(第七十七部分):阴影对象类
内容
概述
在上一篇文章中开发会话窗对象时,我稍微触及了一点为测试对象工件构建阴影。 今天,我将扩展这个概念,并加以修订,从而可与会话窗对象结合使用作为其稳固组件。 会话窗对象在需要时拥有为在其上创建的对象绘制阴影的能力,并其显示在屏幕上。
我曾研究过两种绘制对象阴影的方法:
- 直接在会话窗对象本身的画布上,
- “躺在”会话窗对象之下的一个单独的对象上。
由于实现更简单,故我选择了第二个选项。 该方法的缺点是我们必须管控一个额外的对象。 该方法的优点在于我们可以通过简单地重新定位绘制阴影的图形元素来快速实现任何变更(例如,更改阴影位置坐标)。
若我们已在会话窗对象上绘制了阴影,我们不得不彻底重绘整个会话窗阴影(或擦除阴影,重新计算其新坐标,并重绘),这都需要耗费更多算力。 此外,阴影应该应用于位于其下方的对象,这意味着在阴影与对象重叠的地方需要重新计算颜色合并和透明度,以及逐像素重绘对象阴影的投射背景。 而当用单独对象绘制阴影时,我们不再需要这样麻烦。 在单独的对象上按照自己的颜色和透明度绘制阴影,它们无需我们的参与即可叠加在对象下层。 终端会为我们计算一切。
当然,直接在会话窗画布上绘制阴影的方法也有其优点,但我还是采用第二种方式,因其实现和控制更简单。 在阴影对象的首次实现中,我将采用 ALGLIB 数值分析库的高斯模糊方法。 构建阴影时的一些细微差别已在文章 “Studying the CCanvas Class Anti-aliasing and Shadows" 里有所阐述,该书作者是 Vladimir Karputov。 我们应用他在文章中论述的高斯模糊方法。
阴影对象是从图形元素对象类继承的一个新类,与创建会话窗对象的方式相同。 所有这些对象都是基准图形元素的衍生后代,就像许多其它对象一样。 在会话窗对象中,我将创建快速创建阴影对象和更改其属性的方法。 与往常一样,我们修改已经编写好的库类。
改进库类
在 \MQL5\Include\DoEasy\Data.mqh 里,添加新的函数消息索引:
MSG_LIB_SYS_FAILED_DRAWING_ARRAY_RESIZE, // Failed to change the array size of drawn buffers MSG_LIB_SYS_FAILED_COLORS_ARRAY_RESIZE, // Failed to change the color array size MSG_LIB_SYS_FAILED_ARRAY_RESIZE, // Failed to change the array size MSG_LIB_SYS_FAILED_ADD_BUFFER, // Failed to add buffer object to the list MSG_LIB_SYS_FAILED_CREATE_BUFFER_OBJ, // Failed to create \"Indicator buffer\" object
...
//--- CChartObjCollection MSG_CHART_COLLECTION_TEXT_CHART_COLLECTION, // Chart collection MSG_CHART_COLLECTION_ERR_FAILED_CREATE_CHART_OBJ, // Failed to create a new chart object MSG_CHART_COLLECTION_ERR_FAILED_ADD_CHART, // Failed to add a chart object to the collection MSG_CHART_COLLECTION_ERR_CHARTS_MAX, // Cannot open new chart. Number of open charts at maximum MSG_CHART_COLLECTION_CHART_OPENED, // Chart opened MSG_CHART_COLLECTION_CHART_CLOSED, // Chart closed MSG_CHART_COLLECTION_CHART_SYMB_CHANGED, // Chart symbol changed MSG_CHART_COLLECTION_CHART_TF_CHANGED, // Chart timeframe changed MSG_CHART_COLLECTION_CHART_SYMB_TF_CHANGED, // Chart symbol and timeframe changed //--- CForm MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT,// No shadow object. Create it using the CreateShadowObj() method MSG_FORM_OBJECT_ERR_FAILED_CREATE_SHADOW_OBJ, // Failed to create a new shadow object //--- CShadowObj MSG_SHADOW_OBJ_IMG_SMALL_BLUR_LARGE, // Error! Image size too small or blur too extensive }; //+------------------------------------------------------------------+
并编写与新添加的索引相对应的消息文本:
{"Не удалось изменить размер массива рисуемых буферов","Failed to resize drawing buffers array"}, {"Не удалось изменить размер массива цветов","Failed to resize color array"}, {"Не удалось изменить размер массива ","Failed to resize array "}, {"Не удалось добавить объект-буфер в список","Failed to add buffer object to list"}, {"Не удалось создать объект \"Индикаторный буфер\"","Failed to create object \"Indicator buffer\""},
...
//--- CChartObjCollection {"Коллекция чартов","Chart collection"}, {"Не удалось создать новый объект-чарт","Failed to create new chart object"}, {"Не удалось добавить объект-чарт в коллекцию","Failed to add chart object to collection"}, {"Нельзя открыть новый график, так как количество открытых графиков уже максимальное","You cannot open a new chart, since the number of open charts is already maximum"}, {"Открыт график","Open chart"}, {"Закрыт график","Closed chart"}, {"Изменён символ графика","Changed chart symbol"}, {"Изменён таймфрейм графика","Changed chart timeframe"}, {"Изменён символ и таймфрейм графика","Changed the symbol and timeframe of the chart"}, //--- CForm {"Отсутствует объект тени. Необходимо сначала его создать при помощи метода CreateShadowObj()","There is no shadow object. You must first create it using the CreateShadowObj () method"}, {"Не удалось создать новый объект для тени","Failed to create new object for shadow"}, //--- CShadowObj {"Ошибка! Размер изображения очень маленький или очень большое размытие","Error! Image size is very small or very large blur"}, }; //+---------------------------------------------------------------------+
在上一篇文章中,我在会话窗对象周围留下了每边五个像素大小的余量空间来绘制阴影。 事实证明,我们需要更多空间来执行正常的高斯模糊。 根据经验,我发现在模糊半径为 4 像素的情况下,我们需要在每侧留出 16 像素的余量空间。 像素太少在绘制阴影的画布边缘会产生残影(阴影已完全透明且实际上不存在,但背景依然留有污迹)。
在 \MQL5\Include\DoEasy\Defines.mqh 里,将阴影的默认余量空间大小设置为 16(替换掉之前设置的数值 5):
//--- Canvas parameters #define PAUSE_FOR_CANV_UPDATE (16) // Canvas update frequency #define NULL_COLOR (0x00FFFFFF) // Zero for the canvas with the alpha channel #define OUTER_AREA_SIZE (16) // Size of one side of the outer area around the workspace //+------------------------------------------------------------------+
在图形元素类型的枚举当中添加新类型(Shadow 对象):
//+------------------------------------------------------------------+ //| The list of graphical element types | //+------------------------------------------------------------------+ enum ENUM_GRAPH_ELEMENT_TYPE { GRAPH_ELEMENT_TYPE_ELEMENT, // Element GRAPH_ELEMENT_TYPE_SHADOW_OBJ, // Shadow object GRAPH_ELEMENT_TYPE_FORM, // Form GRAPH_ELEMENT_TYPE_WINDOW, // Window }; //+------------------------------------------------------------------+
在创建新的阴影对象时,我们会指定这种类型。 这可令我们稍后选择所有阴影对象,并同时处理它们。
我即将在这里创建的阴影对象,拥有自己的属性,这些属性会影响会话窗对象的投射阴影的外观。
在 \MQL5\Include\DoEasy\GraphINI.mqh 里,我们为会话窗样式设置添加这些参数:
//+------------------------------------------------------------------+ //| List of form style parameter indices | //+------------------------------------------------------------------+ enum ENUM_FORM_STYLE_PARAMS { FORM_STYLE_FRAME_WIDTH_LEFT, // Form frame width to the left FORM_STYLE_FRAME_WIDTH_RIGHT, // Form frame width to the right FORM_STYLE_FRAME_WIDTH_TOP, // Form frame width on top FORM_STYLE_FRAME_WIDTH_BOTTOM, // Form frame width below FORM_STYLE_FRAME_SHADOW_OPACITY, // Shadow opacity FORM_STYLE_FRAME_SHADOW_BLUR, // Shadow blur FORM_STYLE_DARKENING_COLOR_FOR_SHADOW, // Form shadow color darkening FORM_STYLE_FRAME_SHADOW_X_SHIFT, // Shadow X axis shift FORM_STYLE_FRAME_SHADOW_Y_SHIFT, // Shadow Y axis shift }; #define TOTAL_FORM_STYLE_PARAMS (9) // Number of form style parameters //+------------------------------------------------------------------+ //| Array containing form style parameters | //+------------------------------------------------------------------+ int array_form_style[TOTAL_FORM_STYLES][TOTAL_FORM_STYLE_PARAMS]= { //--- "Flat form" style parameters { 3, // Form frame width to the left 3, // Form frame width to the right 3, // Form frame width on top 3, // Form frame width below 80, // Shadow opacity 4, // Shadow blur 80, // Form shadow color darkening 2, // Shadow X axis shift 2, // Shadow Y axis shift }, //--- "Embossed form" style parameters { 4, // Form frame width to the left 4, // Form frame width to the right 4, // Form frame width on top 4, // Form frame width below 80, // Shadow opacity 4, // Shadow blur 80, // Form shadow color darkening 2, // Shadow X axis shift 2, // Shadow Y axis shift }, }; //+------------------------------------------------------------------+
Shadow blur 设置图像模糊半径。
Form shadow color darkening 设置点数,即阴影颜色变暗的宽度。 如果阴影颜色依赖于图表背景颜色,则这是必要的。 在这种情况下,图表背景颜色将转换为灰色,然后按此处指定的量值变暗。
Shadow X/Y axis shifts 指示阴影从投射它的对象中心的偏移度。 零表示阴影位于对象周围。 正值表示阴影相对于对象向右下方偏移,而负值表示阴影向左上方偏移。
由于我修改了参数数量,因此需要明确指出这一点。 设置新的数字 9,替代之前用的 5。
此外,往配色方案设置里添加另一个参数 — "Form outline rectangle color"。
为了更清晰地显示会话窗,我将在会话窗周围加上框架(不要与会话窗框架混淆)— 这是一个简单的矩形,其颜色与外部背景高亮对比。 在此设置中指定矩形颜色。
//+------------------------------------------------------------------+ //| List of indices of color scheme parameters | //+------------------------------------------------------------------+ enum ENUM_COLOR_THEME_COLORS { COLOR_THEME_COLOR_FORM_BG, // Form background color COLOR_THEME_COLOR_FORM_FRAME, // Form frame color COLOR_THEME_COLOR_FORM_RECT_OUTER, // Form outline rectangle color COLOR_THEME_COLOR_FORM_SHADOW, // Form shadow color }; #define TOTAL_COLOR_THEME_COLORS (4) // Number of parameters in the color theme //+------------------------------------------------------------------+ //| The array containing color schemes | //+------------------------------------------------------------------+ color array_color_themes[TOTAL_COLOR_THEMES][TOTAL_COLOR_THEME_COLORS]= { //--- Parameters of the "Blue steel" color scheme { C'134,160,181', // Form background color C'134,160,181', // Form frame color clrDimGray, // Form outline rectangle color clrGray, // Form shadow color }, //--- Parameters of the "Light cyan gray" color scheme { C'181,196,196', // Form background color C'181,196,196', // Form frame color clrGray, // Form outline rectangle color clrGray, // Form shadow color }, }; //+------------------------------------------------------------------+
我们来改进 \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh 中的图形元素类。
该类的公开部分含有 ChangeColorLightness() 方法,可按指定的量值更改颜色光强。
该方法是按 ARGB 格式 接收需要变更的颜色。 有时这可能不方便,所以我们声明一个重载方法,以 “color” 格式 和不透明度接收颜色:
//--- Update the coordinates (shift the canvas) bool Move(const int x,const int y,const bool redraw=false); //--- Change the brightness of (1) ARGB and (2) COLOR by a specified amount uint ChangeColorLightness(const uint clr,const double change_value); color ChangeColorLightness(const color colour,const uchar opacity,const double change_value);
此外,我还需要更改颜色饱和度的方法。 例如,若要从任何颜色中生成灰色,我们需要将其饱和度分量(HSL、HSI、HSV 和 HSB 格式中的 S)左移 — 至零。 因此,颜色彻底变为不饱和 — 它变成了灰色阴影,而这恰是我们绘制阴影所需的。
声明两个改变颜色饱和度的重载方法:
//--- Change the brightness of (1) ARGB and (2) COLOR by a specified amount uint ChangeColorLightness(const uint clr,const double change_value); color ChangeColorLightness(const color colour,const double change_value); //--- Change the saturation of (1) ARGB and (2) COLOR by a specified amount uint ChangeColorSaturation(const uint clr,const double change_value); color ChangeColorSaturation(const color colour,const double change_value); protected:
在类主体之外实现所声明的方法。
该方法按指定量值更改 ARGB 颜色饱和度:
//+------------------------------------------------------------------+ //| Change the ARGB color saturation by a specified amount | //+------------------------------------------------------------------+ uint CGCnvElement::ChangeColorSaturation(const uint clr,const double change_value) { if(change_value==0.0) return clr; double a=GETRGBA(clr); double r=GETRGBR(clr); double g=GETRGBG(clr); double b=GETRGBB(clr); double h=0,s=0,l=0; CColors::RGBtoHSL(r,g,b,h,s,l); double ns=s+change_value*0.01; if(ns>1.0) ns=1.0; if(ns<0.0) ns=0.0; CColors::HSLtoRGB(h,ns,l,r,g,b); return ARGB(a,r,g,b); } //+------------------------------------------------------------------+
此处,将获得的 uint 值表示的颜色分解为分量 — alpha 通道、红、绿和蓝。
利用 第七十五篇文章里介绍的 CColors 类的 RGBtoHSL() 方法将 RGB 颜色转换为 HSL 颜色模型,我们需要其中的 S 分量 — 颜色饱和度。 接下来,简单地在方法中添加数值,并取饱和度值乘以 0.01 来计算新的饱和度。 检查所得结果是否超出了可接受数值范围(0-1)。 接下来,调用 CColors 类及其 HSLtoRGB 方法把 H 颜色分量、新的 S 和 L 转换为 RGB 格式。
获取的 RGB 颜色随同原始颜色的 alpha 通道值返回。.
为什么我要把传递给方法的数值乘以 0.01 来修改饱和度? 我这样做只是为了方便。 在 HSL 颜色模型中,分量值从 0 变为 1。 故此,以 100 的倍数(1 代替 0.01、10 代替 0.1、100 代替 1)传递这些数值会更方便。 更重要的是,会话窗样式中的所有值都设置为整数,其中各种会话窗或文本的颜色饱和度可能会发生变化。
该方法按指定量值修改颜色饱和度:
//+------------------------------------------------------------------+ //| Change the COLOR saturation by a specified amount | //+------------------------------------------------------------------+ color CGCnvElement::ChangeColorSaturation(const color colour,const double change_value) { if(change_value==0.0) return colour; uint clr=::ColorToARGB(colour,0); double r=GETRGBR(clr); double g=GETRGBG(clr); double b=GETRGBB(clr); double h=0,s=0,l=0; CColors::RGBtoHSL(r,g,b,h,s,l); double ns=s+change_value*0.01; if(ns>1.0) ns=1.0; if(ns<0.0) ns=0.0; CColors::HSLtoRGB(h,ns,l,r,g,b); return CColors::RGBToColor(r,g,b); } //+------------------------------------------------------------------+
方法逻辑与上面讨论的类似。 唯一的区别是此处的 opacity 参数仅用于将颜色及其不透明度转换为 ARGB 颜色。 Alpha 通道则未在其他任何地方用到。 因此,我们可以在转换过程中忽略它,并传递零值。 接下来,从 ARGB 颜色中提取 R、G 和 B 分量,将它们转换成 HSL 颜色模型,按照传递给方法的值修改 S 分量,将 HSL 模型转换回 RGB,并返回 RGB 颜色模型转换成 “color” 格式后的颜色。
该方法按指定量值修改 COLOR 亮度:
//+------------------------------------------------------------------+ //| Change the COLOR brightness by a specified amount | //+------------------------------------------------------------------+ color CGCnvElement::ChangeColorLightness(const color colour,const double change_value) { if(change_value==0.0) return colour; uint clr=::ColorToARGB(colour,0); double r=GETRGBR(clr); double g=GETRGBG(clr); double b=GETRGBB(clr); double h=0,s=0,l=0; CColors::RGBtoHSL(r,g,b,h,s,l); double nl=l+change_value*0.01; if(nl>1.0) nl=1.0; if(nl<0.0) nl=0.0; CColors::HSLtoRGB(h,s,nl,r,g,b); return CColors::RGBToColor(r,g,b); } //+------------------------------------------------------------------+
该方法与上面研讨过的方法雷同,只是在此我们修改了 HSL 颜色模型的 L 分量。
由于在研讨过的所有方法中,我们把需被修改的颜色分量值乘以 0.01,因此我们还需要更改先前开发的方法,将 ARGB 颜色亮度更改为指定值:
//+------------------------------------------------------------------+ //| Change the ARGB color brightness by a specified value | //+------------------------------------------------------------------+ uint CGCnvElement::ChangeColorLightness(const uint clr,const double change_value) { if(change_value==0.0) return clr; double a=GETRGBA(clr); double r=GETRGBR(clr); double g=GETRGBG(clr); double b=GETRGBB(clr); double h=0,s=0,l=0; CColors::RGBtoHSL(r,g,b,h,s,l); double nl=l+change_value*0.01; if(nl>1.0) nl=1.0; if(nl<0.0) nl=0.0; CColors::HSLtoRGB(h,s,nl,r,g,b); return ARGB(a,r,g,b); } //+------------------------------------------------------------------+
类的公开部分,简化访问对象属性的方法模块,含有设置会话窗阴影标志的方法声明。 然而,由于某种原因,该方法尚未实现。 我们来修复这个问题:
//--- Set the flag of (1) object moveability, (2) activity, (3) element ID, (4) element index in the list and (5) shadow presence void SetMovable(const bool flag) { this.SetProperty(CANV_ELEMENT_PROP_MOVABLE,flag); } void SetActive(const bool flag) { this.SetProperty(CANV_ELEMENT_PROP_ACTIVE,flag); } void SetID(const int id) { this.SetProperty(CANV_ELEMENT_PROP_ID,id); } void SetNumber(const int number) { this.SetProperty(CANV_ELEMENT_PROP_NUM,number); } void SetShadow(const bool flag) { this.m_shadow=flag; } //--- Return the shift (1) of the left, (2) right, (3) top and (4) bottom edge of the element active area
由于添加了框架,我当前创建的所有会话窗对象外表都拥有一些三维特征。 框架的特点是发光的边缘闪烁,不发光的边缘变暗,这会产生三维的错觉,但这还不够。 我们继续添加制作拥有三维幻觉背景的能力。 为达此目的,我们需要用至少两种颜色对背景进行渐变填充 — 从深到浅。 原始颜色亮度的细微变化,原始颜色配合光强的平滑混合及阴影,足以令会话窗呈现出艳丽的颜色:
我已实现了两种清除会话窗,和填充颜色的方法。 若要用渐变色填充背景,需声明另一个 Erase() 方法:
//+------------------------------------------------------------------+ //| The methods of filling, clearing and updating raster data | //+------------------------------------------------------------------+ //--- Clear the element filling it with color and opacity void Erase(const color colour,const uchar opacity,const bool redraw=false); //--- Clear the element with a gradient fill void Erase(color &colors[],const uchar opacity,const bool vgradient=true,const bool cycle=false,const bool redraw=false); //--- Clear the element completely void Erase(const bool redraw=false); //--- Update the element void Update(const bool redraw=false) { this.m_canvas.Update(redraw); }
我们在类主体之外编写其实现:
//+------------------------------------------------------------------+ //| Clear the element with a gradient fill | //+------------------------------------------------------------------+ void CGCnvElement::Erase(color &colors[],const uchar opacity,const bool vgradient=true,const bool cycle=false,const bool redraw=false) { //--- Check the size of the color array int size=::ArraySize(colors); //--- If there are less than two colors in the array if(size<2) { //--- if the array is empty, erase the background completely and leave if(size==0) { this.Erase(redraw); return; } //--- in case of one color, fill the background with this color and opacity, and leave this.Erase(colors[0],opacity,redraw); return; } //--- Declare the receiver array color out[]; //--- Set the gradient size depending on the filling direction (vertical/horizontal) int total=(vgradient ? this.Height() : this.Width()); //--- and get the set of colors in the receive array CColors::Gradient(colors,out,total,cycle); total=::ArraySize(out); //--- In the loop by the number of colors in the array for(int i=0;i<total;i++) { //--- depending on the filling direction switch(vgradient) { //--- Horizontal gradient - draw vertical segments from left to right with the color from the array case false : DrawLineVertical(i,0,this.Height()-1,out[i],opacity); break; //--- Vertical gradient - draw horizontal segments downwards with the color from the array default: DrawLineHorizontal(0,this.Width()-1,i,out[i],opacity); break; } } //--- If specified, update the canvas this.Update(redraw); } //+------------------------------------------------------------------+
整个方法逻辑在清单注释中均有所讲述。 该方法接收欲填充的颜色数组、不透明度值、垂直渐变标志(如果为 true,向下填充,如果为 false —从左到右)、循环标志(如果设置,以开始时的相同颜色填充)、以及指示在填充后重绘画布的必要标志。 为了获取颜色数组,调用 CColors 类的 Gradient() 方法。
库类中的修改和添加至此完毕。 现在我们为阴影对象编写新类,它是图形元素对象类的衍生后代。
阴影对象类
在函数库图形对象的 \MQL5\Include\DoEasy\Objects\Graph\ 目录中,创建含 CShadowObj 类的新文件 ShadowObj.mqh。
图形元素的文件和 ALGLIB 数值分析库文件均应包含在文件之中。 该类应继承自图形元素对象类:
//+------------------------------------------------------------------+ //| ShadowObj.mqh | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" #property strict // Necessary for mql4 //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "GCnvElement.mqh" #include <Math\Alglib\alglib.mqh> //+------------------------------------------------------------------+ //| Shadow object class | //+------------------------------------------------------------------+ class CShadowObj : public CGCnvElement { }
在类的私密部分,声明存储颜色和阴影不透明度的变量,以及类的操作方法:
//+------------------------------------------------------------------+ //| Shadow object class | //+------------------------------------------------------------------+ class CShadowObj : public CGCnvElement { private: color m_color_shadow; // Shadow color uchar m_opacity_shadow; // Shadow opacity //--- Gaussian blur bool GaussianBlur(const uint radius); //--- Return the array of weight ratios bool GetQuadratureWeights(const double mu0,const int n,double &weights[]); //--- Draw the object shadow form void DrawShadowFigureRect(const int w,const int h); public:
此处的 DrawShadowFigureRect() 方法根据会话窗对象的维度绘制一个非模糊形状,并为该对象绘制所需投射的阴影。
GetQuadratureWeights() 方法应用 ALGLIB 库 来计算和返回 DrawShadowFigureRect() 方法绘制模糊造型所需的权重比数组。
造型的模糊由 GaussianBlur() 方法执行。 所有方法均在下面研讨。
在类的公开部分,声明参数型构造函数、返回支持对象属性标志的方法(直到两个方法都返回 true)、和绘制阴影的方法,并编写简化访问阴影对象属性的方法:
public: CShadowObj(const long chart_id, const int subwindow, const string name, const int x, const int y, const int w, const int h); //--- Supported object properties (1) integer and (2) string ones virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER property) { return true; } virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING property) { return true; } //--- Draw an object shadow void DrawShadow(const int shift_x,const int shift_y,const uchar blur_value); //+------------------------------------------------------------------+ //| Methods of simplified access to object properties | //+------------------------------------------------------------------+ //--- (1) Set and (2) return the shadow color void SetColorShadow(const color colour) { this.m_color_shadow=colour; } color ColorShadow(void) const { return this.m_color_shadow; } //--- (1) Set and (2) return the shadow opacity void SetOpacityShadow(const uchar opacity) { this.m_opacity_shadow=opacity; } uchar OpacityShadow(void) const { return this.m_opacity_shadow; } }; //+------------------------------------------------------------------+
我们来更详细地研讨类方法的结构。
参数型构造函数:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CShadowObj::CShadowObj(const long chart_id, const int subwindow, const string name, const int x, const int y, const int w, const int h) : CGCnvElement(GRAPH_ELEMENT_TYPE_SHADOW_OBJ,chart_id,subwindow,name,x,y,w,h) { CGCnvElement::SetColorBackground(clrNONE); CGCnvElement::SetOpacity(0); CGCnvElement::SetActive(false); this.m_opacity_shadow=127; color gray=CGCnvElement::ChangeColorSaturation(ChartColorBackground(),-100); this.m_color_shadow=CGCnvElement::ChangeColorLightness(gray,255,-50); this.m_shadow=false; this.m_visible=true; CGCnvElement::Erase(); } //+------------------------------------------------------------------+
构造函数接收图表的 ID、创建阴影对象的子窗口索引、其名称、左上角的坐标及其尺寸。 在初始化清单中,调用图形元素类的受保护构造函数,在方法参数里传递元素类型 — shadow 对象,和其他参数。
在构造函数主体中,设置省缺的对象背景、完全透明、和对象非活跃标志(阴影对象不应以任何方式对外部影响做出反应)。 在画布上绘制的阴影默认不透明度设置为 127。 这是一个半透明的阴影。 接下来,计算默认阴影颜色。 这个图表背景颜色表示 100 个单位中的 50 个单位变暗了。 在此,我们首先将图表背景颜色转换为灰色阴影,然后再把生成的颜色变暗。 绘制阴影的对象不应有投射,故此将其阴影标志设置为 false,将对象可见性标志设置为 true ,并清除画布。
该方法绘制阴影对象:
//+------------------------------------------------------------------+ //| Draw the object shadow | //+------------------------------------------------------------------+ void CShadowObj::DrawShadow(const int shift_x,const int shift_y,const uchar blur_value) { //--- Calculate the height and width of the drawn rectangle int w=this.Width()-OUTER_AREA_SIZE*2; int h=this.Height()-OUTER_AREA_SIZE*2; //--- Draw a filled rectangle with calculated dimensions this.DrawShadowFigureRect(w,h); //--- Calculate the blur radius, which cannot exceed a quarter of the OUTER_AREA_SIZE constant int radius=(blur_value>OUTER_AREA_SIZE/4 ? OUTER_AREA_SIZE/4 : blur_value); //--- If failed to blur the shape, exit the method (GaussianBlur() displays the error on the journal) if(!this.GaussianBlur(radius)) return; //--- Shift the shadow object by X/Y offsets specified in the method arguments and update the canvas CGCnvElement::Move(this.CoordX()+shift_x,this.CoordY()+shift_y); CGCnvElement::Update(); } //+------------------------------------------------------------------+
整个方法逻辑在代码的注释中均有讲述。 该方法首先绘制一个普通矩形并填充阴影颜色。 计算矩形的宽度和高度,从而其尺寸可为会话窗对象投射阴影。 接下来,运用高斯方法绘制模糊矩形,将阴影对象相对于需投射阴影的造型对象按照指定的偏移量平移,并刷新阴影对象所在的画布。
该方法绘制物体阴影会话窗:
//+------------------------------------------------------------------+ //| Draw the object shadow form | //+------------------------------------------------------------------+ void CShadowObj::DrawShadowFigureRect(const int w,const int h) { CGCnvElement::DrawRectangleFill(OUTER_AREA_SIZE,OUTER_AREA_SIZE,OUTER_AREA_SIZE+w-1,OUTER_AREA_SIZE+h-1,this.m_color_shadow,this.m_opacity_shadow); CGCnvElement::Update(); } //+------------------------------------------------------------------+
在此,我们在 X/Y 坐标中绘制的矩形等于 OUTER_AREA_SIZE 常量值。 第二个 X/Y 坐标的计算,依据第一个坐标的偏移量 + 宽度(高度)减 1。 造型绘制完毕后,画布会更新。
该方法运用高斯方法绘制模糊造型:
//+------------------------------------------------------------------+ //| Gaussian blur | //| https://www.mql5.com/en/articles/1612#chapter4 | //+------------------------------------------------------------------+ bool CShadowObj::GaussianBlur(const uint radius) { //--- int n_nodes=(int)radius*2+1; uint res_data[]; // Array for storing graphical resource data uint res_w=this.Width(); // Graphical resource width uint res_h=this.Height(); // Graphical resource height //--- Read graphical resource data. If failed, return false ::ResetLastError(); if(!::ResourceReadImage(this.NameRes(),res_data,res_w,res_h)) { CMessage::OutByID(MSG_LIB_SYS_FAILED_GET_DATA_GRAPH_RES); return false; } //--- Check the blur amount. If the blur radius exceeds half of the width or height, return 'false' if(radius>=res_w/2 || radius>=res_h/2) { ::Print(DFUN,CMessage::Text(MSG_SHADOW_OBJ_IMG_SMALL_BLUR_LARGE)); return false; } //--- Decompose image data from the resource into a, r, g, b color components int size=::ArraySize(res_data); //--- arrays for storing A, R, G and B color components //--- for horizontal and vertical blur uchar a_h_data[],r_h_data[],g_h_data[],b_h_data[]; uchar a_v_data[],r_v_data[],g_v_data[],b_v_data[]; //--- Change the size of component arrays according to the array size of the graphical resource data if(::ArrayResize(a_h_data,size)==-1) { CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"a_h_data\""); return false; } if(::ArrayResize(r_h_data,size)==-1) { CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"r_h_data\""); return false; } if(::ArrayResize(g_h_data,size)==-1) { CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"g_h_data\""); return false; } if(ArrayResize(b_h_data,size)==-1) { CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"b_h_data\""); return false; } if(::ArrayResize(a_v_data,size)==-1) { CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"a_v_data\""); return false; } if(::ArrayResize(r_v_data,size)==-1) { CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"r_v_data\""); return false; } if(::ArrayResize(g_v_data,size)==-1) { CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"g_v_data\""); return false; } if(::ArrayResize(b_v_data,size)==-1) { CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"b_v_data\""); return false; } //--- Declare the array for storing blur weight ratios and, //--- if failed to get the array of weight ratios, return 'false' double weights[]; if(!this.GetQuadratureWeights(1,n_nodes,weights)) return false; //--- Set components of each image pixel to the color component arrays for(int i=0;i<size;i++) { a_h_data[i]=GETRGBA(res_data[i]); r_h_data[i]=GETRGBR(res_data[i]); g_h_data[i]=GETRGBG(res_data[i]); b_h_data[i]=GETRGBB(res_data[i]); } //--- Blur the image horizontally (along the X axis) uint XY; // Pixel coordinate in the array double a_temp=0.0,r_temp=0.0,g_temp=0.0,b_temp=0.0; int coef=0; int j=(int)radius; //--- Loop by the image width for(uint Y=0;Y<res_h;Y++) { //--- Loop by the image height for(uint X=radius;X<res_w-radius;X++) { XY=Y*res_w+X; a_temp=0.0; r_temp=0.0; g_temp=0.0; b_temp=0.0; coef=0; //--- Multiply each color component by the weight ratio corresponding to the current image pixel for(int i=-1*j;i<j+1;i=i+1) { a_temp+=a_h_data[XY+i]*weights[coef]; r_temp+=r_h_data[XY+i]*weights[coef]; g_temp+=g_h_data[XY+i]*weights[coef]; b_temp+=b_h_data[XY+i]*weights[coef]; coef++; } //--- Save each rounded color component calculated according to the ratios to the component arrays a_h_data[XY]=(uchar)::round(a_temp); r_h_data[XY]=(uchar)::round(r_temp); g_h_data[XY]=(uchar)::round(g_temp); b_h_data[XY]=(uchar)::round(b_temp); } //--- Remove blur artifacts to the left by copying adjacent pixels for(uint x=0;x<radius;x++) { XY=Y*res_w+x; a_h_data[XY]=a_h_data[Y*res_w+radius]; r_h_data[XY]=r_h_data[Y*res_w+radius]; g_h_data[XY]=g_h_data[Y*res_w+radius]; b_h_data[XY]=b_h_data[Y*res_w+radius]; } //--- Remove blur artifacts to the right by copying adjacent pixels for(uint x=res_w-radius;x<res_w;x++) { XY=Y*res_w+x; a_h_data[XY]=a_h_data[(Y+1)*res_w-radius-1]; r_h_data[XY]=r_h_data[(Y+1)*res_w-radius-1]; g_h_data[XY]=g_h_data[(Y+1)*res_w-radius-1]; b_h_data[XY]=b_h_data[(Y+1)*res_w-radius-1]; } } //--- Blur vertically (along the Y axis) the image already blurred horizontally int dxdy=0; //--- Loop by the image height for(uint X=0;X<res_w;X++) { //--- Loop by the image width for(uint Y=radius;Y<res_h-radius;Y++) { XY=Y*res_w+X; a_temp=0.0; r_temp=0.0; g_temp=0.0; b_temp=0.0; coef=0; //--- Multiply each color component by the weight ratio corresponding to the current image pixel for(int i=-1*j;i<j+1;i=i+1) { dxdy=i*(int)res_w; a_temp+=a_h_data[XY+dxdy]*weights[coef]; r_temp+=r_h_data[XY+dxdy]*weights[coef]; g_temp+=g_h_data[XY+dxdy]*weights[coef]; b_temp+=b_h_data[XY+dxdy]*weights[coef]; coef++; } //--- Save each rounded color component calculated according to the ratios to the component arrays a_v_data[XY]=(uchar)::round(a_temp); r_v_data[XY]=(uchar)::round(r_temp); g_v_data[XY]=(uchar)::round(g_temp); b_v_data[XY]=(uchar)::round(b_temp); } //--- Remove blur artifacts at the top by copying adjacent pixels for(uint y=0;y<radius;y++) { XY=y*res_w+X; a_v_data[XY]=a_v_data[X+radius*res_w]; r_v_data[XY]=r_v_data[X+radius*res_w]; g_v_data[XY]=g_v_data[X+radius*res_w]; b_v_data[XY]=b_v_data[X+radius*res_w]; } //--- Remove blur artifacts at the bottom by copying adjacent pixels for(uint y=res_h-radius;y<res_h;y++) { XY=y*res_w+X; a_v_data[XY]=a_v_data[X+(res_h-1-radius)*res_w]; r_v_data[XY]=r_v_data[X+(res_h-1-radius)*res_w]; g_v_data[XY]=g_v_data[X+(res_h-1-radius)*res_w]; b_v_data[XY]=b_v_data[X+(res_h-1-radius)*res_w]; } } //--- Set the twice blurred (horizontally and vertically) image pixels to the graphical resource data array for(int i=0;i<size;i++) res_data[i]=ARGB(a_v_data[i],r_v_data[i],g_v_data[i],b_v_data[i]); //--- Display the image pixels on the canvas in a loop by the image height and width from the graphical resource data array for(uint X=0;X<res_w;X++) { for(uint Y=radius;Y<res_h-radius;Y++) { XY=Y*res_w+X; CGCnvElement::GetCanvasObj().PixelSet(X,Y,res_data[XY]); } } //--- Done return true; } //+------------------------------------------------------------------+
方法逻辑已在代码注释中描述。 您可从借取该方法的 文章中找到更多详细信息。
该方法返回权重比数组:
//+------------------------------------------------------------------+ //| Return the array of weight ratios | //| https://www.mql5.com/en/articles/1612#chapter3_2 | //+------------------------------------------------------------------+ bool CShadowObj::GetQuadratureWeights(const double mu0,const int n,double &weights[]) { CAlglib alglib; double alp[]; double bet[]; ::ArrayResize(alp,n); ::ArrayResize(bet,n); ::ArrayInitialize(alp,1.0); ::ArrayInitialize(bet,1.0); //--- double out_x[]; int info=0; alglib.GQGenerateRec(alp,bet,mu0,n,info,out_x,weights); if(info!=1) { string txt=(info==-3 ? "internal eigenproblem solver hasn't converged" : info==-2 ? "Beta[i]<=0" : "incorrect N was passed"); ::Print("Call error in CGaussQ::GQGenerateRec: ",txt); return false; } return true; } //+------------------------------------------------------------------+
该方法调用 ALGLIB 数值分析库计算模糊率,并将它们设置在通过引用传递的权重数组当中。 在以下文章章节中可找到详细信息。
至此,第一个版本的阴影对象类开发完毕。
现在我们需要实现直接从会话窗对象快速创建和绘制阴影的能力。
打开会话窗对象类的 \MQL5\Include\DoEasy\Objects\Graph\Form.mqh,并进行必要的改进。
为了让会话窗对象类能看到阴影对象类,请将最近创建的阴影类的文件包含到其中:
//+------------------------------------------------------------------+ //| Form.mqh | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" #property strict // Necessary for mql4 //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "GCnvElement.mqh" #include "ShadowObj.mqh" //+------------------------------------------------------------------+ //| Form object class | //+------------------------------------------------------------------+
从类的私密部分,删除存储会话窗阴影颜色的变量:
color m_color_shadow; // Form shadow color
现在,阴影颜色会被存储在阴影对象类之中。
结果就是,我们的会话窗对象能够在其中创建新的图形元素对象,并将它们附加到其所依赖的对象列表当中。 换句话说,这些新创建的对象完全依赖,并从属于造型对象。 会话窗对象将能够管理它们。 为了创建这种对象,我们需要创建它们的名称,其中应包含会话窗对象名称,并在其末尾添加自己的名称。 为达此目的,在类的私密部分添加方法来创建依赖对象的名称:
//--- Initialize the variables void Initialize(void); //--- Return the name of the dependent object string CreateNameDependentObject(const string base_name) const { return ::StringSubstr(this.NameObj(),::StringLen(::MQLInfoString(MQL_PROGRAM_NAME))+1)+"_"+base_name; } //--- Create a new graphical object
在上一篇讲述绘画窗对象的时候,我已为对象实现了创建这样的名称:
... 从名称对象中提取尾端(名称由程序名称和在其创建期间分配的对象名称组成)。 我们需要在创建过程中提取对象名称,再加上传递给方法的名称。
例如,在名称为 "Program_name_Form01" 的情况下,我们提取 "Form01" 子字符串,再加上传递给方法的名称。 如果我们创建一个阴影对象,其传递名称为 "Shadow",则对象名称为 "Form01_ Shadow ",而最终创建的对象名称如下:...Program_name_Form01_Shadow"...
现在,这将在一个单独的方法中完成,因为不止一次需要它。
此外,在私密部分,声明创建阴影对象的方法:
//--- Create a new graphical object CGCnvElement *CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type, const int element_num, const string name, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool movable, const bool activity); //--- Create a shadow object void CreateShadowObj(const color colour,const uchar opacity); public:
从类的公开部分,删除方法声明:
//--- Create a new attached element bool CreateNewElement(const int element_num, const string name, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool movable, const bool activity); //--- Create a shadow object void CreateShadow(const uchar opacity); //--- Draw an object shadow
现在该方法将不会被公开调用,且阴影的颜色及其不透明度会额外传递给它。
绘制对象阴影的公开方法现在也会有更多参数:
//--- Create a new attached element bool CreateNewElement(const int element_num, const string name, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool movable, const bool activity); //--- Draw an object shadow void DrawShadow(const int shift_x,const int shift_y,const color colour,const uchar opacity=127,const uchar blur=4); //--- Draw the form frame
这样做是为了替代初创阴影对象,且在渲染之后,我们可以立即调用阴影绘制方法。 这里的逻辑很简单。 如果我们调用阴影绘制方法,则我们不需要它。 如果我们还没有创建阴影对象,则新方法首先创建对象,在其上绘制阴影,并将其显示在屏幕上。
为了简化访问对象属性,从方法模块中删除设置和返回阴影颜色的方法的实现:
//+------------------------------------------------------------------+ //| Methods of simplified access to object properties | //+------------------------------------------------------------------+ //--- (1) Set and (2) get the form frame color void SetColorFrame(const color colour) { this.m_color_frame=colour; } color ColorFrame(void) const { return this.m_color_frame; } //--- (1) Set and (2) return the form shadow color void SetColorShadow(const color colour) { this.m_color_shadow=colour; } color ColorShadow(void) const { return this.m_color_shadow; }
现在这些方法将被移到类的主体之外(那里需要检查阴影对象的存在),而在此只保留它们的声明。 另外,添加声明设置和返回阴影不透明度的方法:
//+------------------------------------------------------------------+ //| Methods of simplified access to object properties | //+------------------------------------------------------------------+ //--- (1) Set and (2) get the form frame color void SetColorFrame(const color colour) { this.m_color_frame=colour; } color ColorFrame(void) const { return this.m_color_frame; } //--- (1) Set and (2) return the form shadow color void SetColorShadow(const color colour); color ColorShadow(void) const; //--- (1) Set and (2) return the form shadow opacity void SetOpacityShadow(const uchar opacity); uchar OpacityShadow(void) const; }; //+------------------------------------------------------------------+
在创建新图形元素的方法中替换以下字符串
int pos=::StringLen(::MQLInfoString(MQL_PROGRAM_NAME));
string pref=::StringSubstr(NameObj(),pos+1);
string name=pref+"_"+obj_name;
调用创建依赖对象名称的方法:
//+------------------------------------------------------------------+ //| Create a new graphical object | //+------------------------------------------------------------------+ CGCnvElement *CForm::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type, const int obj_num, const string obj_name, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool movable, const bool activity) { string name=this.CreateNameDependentObject(obj_name); CGCnvElement *element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity); if(element==NULL) ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name); return element; } //+------------------------------------------------------------------+
创建阴影对象后,应立即为其设置默认参数。
为了实现这一点,我们来略微改进创建阴影对象的方法:
//+------------------------------------------------------------------+ //| Create the shadow object | //+------------------------------------------------------------------+ void CForm::CreateShadowObj(const color colour,const uchar opacity) { //--- If the shadow flag is disabled or the shadow object already exists, exit if(!this.m_shadow || this.m_shadow_obj!=NULL) return; //--- Calculate the shadow object coordinates according to the offset from the top and left int x=this.CoordX()-OUTER_AREA_SIZE; int y=this.CoordY()-OUTER_AREA_SIZE; //--- Calculate the width and height in accordance with the top, bottom, left and right offsets int w=this.Width()+OUTER_AREA_SIZE*2; int h=this.Height()+OUTER_AREA_SIZE*2; //--- Create a new shadow object and set the pointer to it in the variable this.m_shadow_obj=new CShadowObj(this.ChartID(),this.SubWindow(),this.CreateNameDependentObject("Shadow"),x,y,w,h); if(this.m_shadow_obj==NULL) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_FAILED_CREATE_SHADOW_OBJ)); return; } //--- Set the properties for the created shadow object this.m_shadow_obj.SetID(this.ID()); this.m_shadow_obj.SetNumber(-1); this.m_shadow_obj.SetOpacityShadow(opacity); this.m_shadow_obj.SetColorShadow(colour); this.m_shadow_obj.SetMovable(true); this.m_shadow_obj.SetActive(false); this.m_shadow_obj.SetVisible(false); //--- Move the form object to the foreground this.BringToTop(); } //+------------------------------------------------------------------+
改进绘制阴影的方法,如此即可在没有阴影对象的情况下首先创建,然后在其上绘制阴影:
//+------------------------------------------------------------------+ //| Draw the shadow | //+------------------------------------------------------------------+ void CForm::DrawShadow(const int shift_x,const int shift_y,const color colour,const uchar opacity=127,const uchar blur=4) { //--- If the shadow flag is disabled, exit if(!this.m_shadow) return; //--- If there is no shadow object, create it if(this.m_shadow_obj==NULL) this.CreateShadowObj(colour,opacity); //--- If the shadow object exists, draw the shadow on it, //--- set the shadow object visibility flag and //--- move the form object to the foreground if(this.m_shadow_obj!=NULL) { this.m_shadow_obj.DrawShadow(shift_x,shift_y,blur); this.m_shadow_obj.SetVisible(true); this.BringToTop(); } } //+------------------------------------------------------------------+
方法逻辑已在代码注释中进行了阐述,应该不会造成任何误解。
设置配色方案的方法中,在设置阴影对象的绘制颜色之前,检查使用阴影的标志,和所创建阴影对象的存在:
//+------------------------------------------------------------------+ //| Set a color scheme | //+------------------------------------------------------------------+ void CForm::SetColorTheme(const ENUM_COLOR_THEMES theme,const uchar opacity) { this.SetOpacity(opacity); this.SetColorBackground(array_color_themes[theme][COLOR_THEME_COLOR_FORM_BG]); this.SetColorFrame(array_color_themes[theme][COLOR_THEME_COLOR_FORM_FRAME]); if(this.m_shadow && this.m_shadow_obj!=NULL) this.SetColorShadow(array_color_themes[theme][COLOR_THEME_COLOR_FORM_SHADOW]); } //+------------------------------------------------------------------+
设置会话窗样式的方法 接收新的输入,指示需要用图表背景颜色来创建阴影颜色,以及添加绘制阴影:
//+------------------------------------------------------------------+ //| Set the form style | //+------------------------------------------------------------------+ void CForm::SetFormStyle(const ENUM_FORM_STYLE style, const ENUM_COLOR_THEMES theme, const uchar opacity, const bool shadow=false, const bool use_bg_color=true, const bool redraw=false) { //--- Set opacity parameters and the size of the form frame side this.m_shadow=shadow; this.m_frame_width_top=array_form_style[style][FORM_STYLE_FRAME_WIDTH_TOP]; this.m_frame_width_bottom=array_form_style[style][FORM_STYLE_FRAME_WIDTH_BOTTOM]; this.m_frame_width_left=array_form_style[style][FORM_STYLE_FRAME_WIDTH_LEFT]; this.m_frame_width_right=array_form_style[style][FORM_STYLE_FRAME_WIDTH_RIGHT]; //--- Create the shadow object this.CreateShadowObj(clrNONE,(uchar)array_form_style[style][FORM_STYLE_FRAME_SHADOW_OPACITY]); //--- Set a color scheme this.SetColorTheme(theme,opacity); //--- Calculate a shadow color with color darkening color clr=array_color_themes[theme][COLOR_THEME_COLOR_FORM_SHADOW]; color gray=CGCnvElement::ChangeColorSaturation(ChartColorBackground(),-100); color color_shadow=CGCnvElement::ChangeColorLightness((use_bg_color ? gray : clr),-fabs(array_form_style[style][FORM_STYLE_DARKENING_COLOR_FOR_SHADOW])); this.SetColorShadow(color_shadow); //--- Draw a rectangular shadow int shift_x=array_form_style[style][FORM_STYLE_FRAME_SHADOW_X_SHIFT]; int shift_y=array_form_style[style][FORM_STYLE_FRAME_SHADOW_Y_SHIFT]; this.DrawShadow(shift_x,shift_y,color_shadow,this.OpacityShadow(),(uchar)array_form_style[style][FORM_STYLE_FRAME_SHADOW_BLUR]); //--- Fill in the form background with color and opacity this.Erase(this.ColorBackground(),this.Opacity()); //--- Depending on the selected form style, draw the corresponding form frame and the outer bounding frame switch(style) { case FORM_STYLE_BEVEL : this.DrawFormFrame(this.m_frame_width_top,this.m_frame_width_bottom,this.m_frame_width_left,this.m_frame_width_right,this.ColorFrame(),this.Opacity(),FRAME_STYLE_BEVEL); break; //---FORM_STYLE_FLAT default: this.DrawFormFrame(this.m_frame_width_top,this.m_frame_width_bottom,this.m_frame_width_left,this.m_frame_width_right,this.ColorFrame(),this.Opacity(),FRAME_STYLE_FLAT); break; } this.DrawRectangle(0,0,Width()-1,Height()-1,array_color_themes[theme][COLOR_THEME_COLOR_FORM_RECT_OUTER],this.Opacity()); } //+------------------------------------------------------------------+
方法逻辑已在注释中讲述。 简而言之,首先创建阴影对象。 设置调色板后,计算绘制阴影所需的颜色。 如果设置了使用背景颜色标志,我将把图表背景颜色转换为单色,并依据 GraphINI.mqh 文件里会话窗样式设置中的明暗参数值将其变暗。 如果未设置标志,则采用相同方式变暗颜色。 颜色在 GraphINI.mqh 文件中的会话窗配色方案中设置。 接下来,调用绘制阴影的方法,仅在设置了会话窗对象的阴影使用标志时才绘制阴影。
会话窗框架变亮/变暗的所有方法中,替换以实数型指定的数值
//--- Darken the horizontal sides of the frame for(int i=0;i<width;i++) { this.m_canvas.PixelSet(x+i,y,CGCnvElement::ChangeColorLightness(this.GetPixel(x+i,y),-0.05)); this.m_canvas.PixelSet(x+i,y+height-1,CGCnvElement::ChangeColorLightness(this.GetPixel(x+i,y+height-1),-0.07)); }
为相应的整数型,数值会放大一百倍(在这些代码调用的方法中,我已添加代码将传递给它们的数值除以 100):
//--- Darken the horizontal sides of the frame for(int i=0;i<width;i++) { this.m_canvas.PixelSet(x+i,y,CGCnvElement::ChangeColorLightness(this.GetPixel(x+i,y),-5)); this.m_canvas.PixelSet(x+i,y+height-1,CGCnvElement::ChangeColorLightness(this.GetPixel(x+i,y+height-1),-7)); }
这已在所有需要替换数值的方法中完成了。 我们不会在此重述相同类型的修改 — 文后附带的文件里可找到代码。
该方法设置窗体阴影颜色:
//+------------------------------------------------------------------+ //| Set the form shadow color | //+------------------------------------------------------------------+ void CForm::SetColorShadow(const color colour) { if(this.m_shadow_obj==NULL) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT)); return; } this.m_shadow_obj.SetColorShadow(colour); } //+------------------------------------------------------------------+
在此我们检查阴影对象是否存在,并仅在对象存在时为其设置阴影颜色。 否则,显示一条日志消息,通知阴影对象不存在,并提示要首先创建它。
该方法返回会话窗阴影颜色:
//+------------------------------------------------------------------+ //| Return the form shadow color | //+------------------------------------------------------------------+ color CForm::ColorShadow(void) const { if(this.m_shadow_obj==NULL) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT)); return clrNONE; } return this.m_shadow_obj.ColorShadow(); } //+------------------------------------------------------------------+
在此,我们首先检查对象是否存在,并从其返回阴影颜色。
该方法设置和返回阴影不透明度:
//+------------------------------------------------------------------+ //| Set the form shadow opacity | //+------------------------------------------------------------------+ void CForm::SetOpacityShadow(const uchar opacity) { if(this.m_shadow_obj==NULL) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT)); return; } this.m_shadow_obj.SetOpacityShadow(opacity); } //+------------------------------------------------------------------+ //| Return the form shadow opacity | //+------------------------------------------------------------------+ uchar CForm::OpacityShadow(void) const { if(this.m_shadow_obj==NULL) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT)); return 0; } return this.m_shadow_obj.OpacityShadow(); } //+------------------------------------------------------------------+
这些方法的逻辑与上面研讨的两种方法的逻辑雷同。
我们现在准备测试为会话窗创建阴影对象。
测试
我们检查一下为会话窗对象创建阴影。 依据会话窗样式和配色方案编写的参数创建了两个会话窗(类似于我在上一篇文章中所做的)。 第三个会话窗是“手动”创建的,它也是如何绘制自定义会话窗的另一个示例。 鉴于会话窗的阴影对象是在创建会话窗本身之后才绘制的,找出哪些对象要对单击其上做出反应:如果会话窗对象高于其所绘制的阴影,则单击后在日志里显示会话窗名称。 如果阴影对象高于会话窗,则日志显示会话窗阴影对象的名称。
为了执行测试,我们借用来自上一篇文章中的 EA,并将其保存到 \MQL5\Experts\TestDoEasy\Part77\ 之下,命名为 TestDoEasyPart77.mq5。
在 EA 输入清单中,添加设置允许我们选择阴影颜色 — 图表背景颜色或指定颜色,颜色值可在下一个输入中指定。 在全局变量清单中,添加存储填充会话窗所用的渐变颜色的数组:
//+------------------------------------------------------------------+ //| TestDoEasyPart77.mq5 | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" //--- includes #include <Arrays\ArrayObj.mqh> #include <DoEasy\Services\Select.mqh> #include <DoEasy\Objects\Graph\Form.mqh> //--- defines #define FORMS_TOTAL (3) // Number of created forms //--- input parameters sinput bool InpMovable = true; // Movable forms flag sinput ENUM_INPUT_YES_NO InpUseColorBG = INPUT_YES; // Use chart background color to calculate shadow color sinput color InpColorForm3 = clrCadetBlue; // Third form shadow color (if not background color) //--- global variables CArrayObj list_forms; color array_clr[]; //+------------------------------------------------------------------+
在 OnInit() 处理程序中,添加创建第三个会话窗对象:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Set the permissions to send cursor movement and mouse scroll events ChartSetInteger(ChartID(),CHART_EVENT_MOUSE_MOVE,true); ChartSetInteger(ChartID(),CHART_EVENT_MOUSE_WHEEL,true); //--- Set EA global variables ArrayResize(array_clr,2); array_clr[0]=C'26,100,128'; // Original ≈Dark-azure color array_clr[1]=C'35,133,169'; // Lightened original color //--- Create the specified number of form objects list_forms.Clear(); int total=FORMS_TOTAL; for(int i=0;i<total;i++) { //--- When creating an object, pass all the required parameters to it CForm *form=new CForm("Form_0"+(string)(i+1),300,40+(i*80),100,(i<2 ? 70 : 30)); if(form==NULL) continue; //--- Set activity and moveability flags for the form form.SetActive(true); form.SetMovable(false); //--- Set the form ID equal to the loop index and the index in the list of objects form.SetID(i); form.SetNumber(0); // (0 - main form object) Auxiliary objects may be attached to the main one. The main object is able to manage them //--- Set the partial opacity for the middle form and the full one for the rest uchar opacity=(i==1 ? 250 : 255); //--- Set the form style and its color theme depending on the loop index if(i<2) { ENUM_FORM_STYLE style=(ENUM_FORM_STYLE)i; ENUM_COLOR_THEMES theme=(ENUM_COLOR_THEMES)i; //--- Set the form style and theme form.SetFormStyle(style,theme,opacity,true,false); } //--- If this is the first (top) form if(i==0) { //--- Draw a concave field slightly shifted from the center of the form downwards form.DrawFieldStamp(3,10,form.Width()-6,form.Height()-13,form.ColorBackground(),form.Opacity()); form.Update(true); } //--- If this is the second (middle) form if(i==1) { //--- Draw a concave semi-transparent "tainted glass" field in the center form.DrawFieldStamp(10,10,form.Width()-20,form.Height()-20,clrWheat,200); form.Update(true); } //--- If this is the third (bottom) form if(i==2) { //--- Set the opacity of 200 form.SetOpacity(200); //--- The form background color is set as the first color from the color array form.SetColorBackground(array_clr[0]); //--- Form outlining frame color form.SetColorFrame(clrDarkBlue); //--- Draw the shadow drawing flag form.SetShadow(true); //--- Calculate the shadow color as the chart background color converted to the monochrome one color clrS=form.ChangeColorSaturation(form.ColorBackground(),-100); //--- If the settings specify the usage of the chart background color, replace the monochrome color with 20 units //--- Otherwise, use the color specified in the settings for drawing the shadow color clr=(InpUseColorBG ? form.ChangeColorLightness(clrS,255,-20) : InpColorForm3); //--- Draw the form shadow with the right-downwards offset from the form by three pixels along all axes //--- Set the shadow opacity to 200, while the blur radius is equal to 4 form.DrawShadow(3,3,clr,200,4); //--- Fill the form background with a vertical gradient form.Erase(array_clr,form.Opacity()); //--- Draw an outlining rectangle at the edges of the form form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,form.ColorFrame(),form.Opacity()); //--- Display the text describing the gradient type and update the form form.Text(form.Width()/2,form.Height()/2,TextByLanguage("V-Градиент","V-Gradient"),C'211,233,149',255,TEXT_ANCHOR_CENTER); form.Update(true); } //--- Add objects to the list if(!list_forms.Add(form)) { delete form; continue; } } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
整个会话窗创建逻辑在代码注释中均有论述。 这是创建自定义会话窗对象的另一个选项。
在 OnChartEvent() 处理程序中,添加点击会话窗时,在日志中显示图形对象名称:
//+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- If clicking on an object if(id==CHARTEVENT_OBJECT_CLICK) { Print(sparam); } } //+------------------------------------------------------------------+
编译 EA,在图表上启动它,并更改默认阴影设置:
不幸的是,GIF 图像不能让我们观察整个调色板。
下面我们可以在 PNG 图像格式中看到带有渐变背景的会话窗外观:
当点击每个会话窗时,日志中会显示会话窗名称(而非其阴影对象的名称):
TestDoEasyPart77_Form_01 TestDoEasyPart77_Form_02 TestDoEasyPart77_Form_03
这意味着阴影对象自会话窗对象创建后,仍然可以移至背景,从而避免干扰创建它的会话窗。
下一步是什么?
在下一篇文章中,我将继续开发会话窗对象类,并开始逐步令静态图像“动画化”。
以下是该函数库当前版本的所有文件,以及 MQL5 的测试 EA 文件,供您测试和下载。
请您在评论中留下问题和建议。
*该系列的前几篇文章:
DoEasy 函数库中的图形(第七十三部分):图形元素的交互窗对象
DoEasy 函数库中的图形(第七十四部分):由 CCanvas 类提供强力支持的基本图形元素
DoEasy 函数库中的图形(第七十五部分):处理基本图形元素图元和文本的方法
DoEasy 函数库中的图形(第七十六部分):会话窗对象和预定义的颜色主题
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/9575
![DoEasy 函数库中的图形(第七十八部分):函数库中的动画原理。 图片切分](https://c.mql5.com/2/42/MQL5-avatar-doeasy-library3-2__5.png)
![DoEasy 函数库中的图形(第七十六部分):会话窗对象和预定义的颜色主题](https://c.mql5.com/2/42/MQL5-avatar-doeasy-library3-2__3.png)