Grafik in der Bibliothek DoEasy (Teil 77): Objektklasse der Schatten

Artyom Trishkin | 29 Juli, 2021

Inhalt


Konzept

Bei der Entwicklung des Form-Objekts im vorigen Artikel habe ich die Erstellung des Objektschattens leicht gestreift, indem ich ein Testobjekt als Werkstück für die Konstruktion von Schatten auf ihm erstellt habe. Heute werde ich dieses Konzept erweitern und es für die Verwendung in Verbindung mit dem Formularobjekt als dessen konstante Komponente überarbeiten. Das Formularobjekt soll die Möglichkeit haben, den Objektschatten zu verwenden, sobald er benötigt wird, indem das Objekt erstellt, die Schattenform darauf gezeichnet und auf dem Bildschirm angezeigt wird.

Ich habe zwei Methoden zum Zeichnen von Objektschatten in Betracht gezogen:

  1. direkt auf der Leinwand des Formularobjekts selbst,
  2. auf ein separates Objekt, das unter dem Formularobjekt "liegt".

Ich habe mich für die zweite Option entschieden, da sie einfacher zu implementieren ist. Der Nachteil dieser Methode ist, dass wir ein zusätzliches Objekt verwalten müssen. Der Vorteil dieser Methode besteht darin, dass wir Änderungen (z. B. Änderung der Koordinaten des Schattenorts) schnell umsetzen können, indem wir einfach das grafische Element, auf dem der Schatten gezeichnet wird, verschieben.

Hätten wir den Schatten auf das Formularobjekt gezeichnet, müssten wir das gesamte Formular mit dem Schatten komplett neu zeichnen (oder den Schatten löschen, seine neuen Koordinaten neu berechnen und ihn neu zeichnen), was rechenintensiver ist. Außerdem sollte der Schatten auf darunter liegende Objekte angewendet werden, was eine Farbverschmelzung und eine Neuberechnung der Transparenz an den Stellen erfordert, an denen der Schatten die Objekte überlappt, sowie ein pixelweises Neuzeichnen des Hintergrunds, auf den der Objektschatten geworfen wird. Bei der Verwendung eines separaten Objekts für den Schatten ist dies nicht mehr erforderlich. Ein Schatten, der auf einem separaten Objekt gezeichnet wird, hat seine eigene Farbe und Transparenz, die den darunter liegenden Objekten ohne unser Zutun überlagert werden. Das Terminal errechnet alles für uns.

Natürlich hat die Methode, Schatten direkt auf die Formularleinwand zu zeichnen, ihre Vorteile, aber ich werde die zweite Option wegen der Einfachheit ihrer Implementierung und Kontrolle verwenden. Bei der ersten Implementierung des Schattenobjekts werde ich die Methode der Gaußschen Unschärfe mit Hilfe der numerischen Analysebibliothek ALGLIB verwenden. Einige Nuancen ihrer Verwendung zur Erstellung von Schatten wurden in dem Artikel "Kennenlernen der CCanvas Klasse. Kantenglättung (Antialiasing) und Schatten" von Vladimir Karputov. Wenden wir die in seinem Artikel beschriebenen Methoden der Gaußschen Unschärfe an.

Das Schattenobjekt soll eine neue Klasse sein, die von der Objektklasse des grafischen Elements abgeleitet wird, genauso wie bei der Erstellung eines Formularobjekts. Alle diese Objekte sind, genau wie viele andere, abgeleitet vom grundlegenden grafischen Element. In dem Formularobjekt werde ich die Methoden für die schnelle Erstellung des Schattenobjekts und die Änderung seiner Eigenschaften erstellen. Wie üblich werden wir die bereits geschriebenen Bibliotheksklassen modifizieren.


Verbesserung der Klassenbibliothek

In \MQL5\Include\DoEasy\Data.mqh fügen wir die Indizes der neuen Nachrichten der Bibliothek hinzu:

   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
   
  };
//+------------------------------------------------------------------+

und die Nachrichtentexte, die den neu hinzugefügten Indizes entsprechen:

   {"Не удалось изменить размер массива рисуемых буферов","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"},
      
  };
//+---------------------------------------------------------------------+

Im vorigen Artikel habe ich einen leeren Raum um das Formularobjekt herum gelassen, der auf jeder Seite fünf Pixel groß war, um den Schatten zu zeichnen. Wie sich herausstellte, brauchen wir für ein normales, gaußsches Weichzeichnen mehr Platz. Empirisch habe ich herausgefunden, dass wir bei einem Radius beim Weichzeichnen von 4 Pixeln einen Leerraum von 16 Pixel auf jeder Seite lassen müssen. Weniger Pixel führen zu Artefakten (Hintergrundverunreinigungen, bei denen der Schatten bereits vollständig transparent und eigentlich nicht vorhanden ist) an den Rändern der Leinwand, auf der der Schatten gezeichnet wird.

In \MQL5\Include\DoEasy\Defines.mqh setzen wir die Standardgröße des freien Raums für den Schatten auf 16 (anstelle des zuvor festgelegten Werts von 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
//+------------------------------------------------------------------+

Fügen wir den neuen Typ (Schattenobjekt) in die Enumeration der grafischen Elementtypen:

//+------------------------------------------------------------------+
//| 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
  };
//+------------------------------------------------------------------+

Wir werden diesen Typ verwenden, wenn wir ein neues Schattenobjekt erstellen. So können wir später alle Schattenobjekte auswählen und sie gleichzeitig bearbeiten.

Das Schattenobjekt, das ich hier erstellen werde, soll über eigene Eigenschaften verfügen, die das Aussehen des Schattens beeinflussen, den das Formularobjekt wirft.
Lassen Sie uns diese Parameter zu den Formularstil-Einstellungen in \MQL5\Include\DoEasy\GraphINI.mqh hinzufügen:

//+------------------------------------------------------------------+
//| 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
   },
  };
//+------------------------------------------------------------------+

Die Schattenunschärfe legt den Radius der Bildunschärfe fest.
Das Abdunkeln der Schattenfarbe legt die Anzahl der Punkte fest, um die die Schattenfarbe abgedunkelt werden soll. Dies ist notwendig, wenn die Schattenfarbe von der Hintergrundfarbe des Diagramms abhängt. In diesem Fall wird die Hintergrundfarbe des Diagramms in Grau umgewandelt und dann um den hier angegebenen Wert verdunkelt.

Die Schattenverschiebungen auf der X/Y-Achse geben an, geben an, wie groß der Abstand des Schattens vom Zentrum des Objekts ist, das ihn entstehen lässt. Null bedeutet, dass sich der Schatten um das Objekt herum befindet. Positive Werte zeigen an, dass sich der Schatten relativ zum Objekt nach rechts unten verschiebt, während negative Werte bedeuten, dass er sich nach links oben verschiebt.

Da ich die Anzahl der Parameter geändert habe, muss ich dies explizit angeben. Setzen wir die neue Anzahl auf 9 anstelle der vorher verwendeten 5.

Fügen wir außerdem einen weiteren Parameter zu den Farbschemaeinstellungen hinzu — "Form outline rectangle color" (Farbe der Umrisslinie des Rechtecks).
Für eine übersichtlichere Darstellung von Formularen ergänze ich den Rahmen um das Formular (nicht zu verwechseln mit dem Formularrahmen) — ein einfaches Rechteck, das mit seiner Farbe das Formular vor dem äußeren Hintergrund hervorhebt. Die Farbe des Rechtecks ist in dieser Einstellung festzulegen.

//+------------------------------------------------------------------+
//| 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
   },
  };
//+------------------------------------------------------------------+


Verbessern wir die grafisches Elementklasse in \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh.

Der öffentliche Teil der Klasse enthält die Methode ChangeColorLightness(), die die Helligkeit der Farbe um den angegebenen Wert ändert.
Die Methode erhält die zu ändernde Farbe im ARGB-Format. Das kann manchmal unpraktisch sein, also deklarieren wir eine überladene Methode, die die Farbe im 'color'-Format und die Transparenz erhält:

//--- 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);

Außerdem benötige ich auch die Methoden zur Änderung der Farbsättigung. Um beispielsweise aus einer beliebigen Farbe eine graue Farbe zu machen, müssen wir ihre Sättigungskomponente (S in den Formaten HSL, HSI, HSV und HSB) nach links — auf Null — verschieben. Auf diese Weise wird die Farbe vollständig entsättigt — sie wird zu einem Grauton, den wir brauchen, um den Schatten zu zeichnen.

Wir deklarieren zwei überladene Methoden, die die Farbsättigung ändern:

//--- 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:

Implementieren wir die angegebenen Methoden außerhalb des Hauptteil der Klasse.

Die Methode ändert die ARGB-Farbsättigung um einen bestimmten Wert:

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

Hier werden die als uint-Wert erhaltene Farbe in ihre Komponenten zerlegt — Alphakanal, Rot, Grün und Blau.
Wir verwenden die Methode RGBtoHSL() der Klasse CColors, die im Artikel 75 beschrieben ist, um die RGB-Farbe in das HSL-Farbmodell umzuwandeln, in dem wir ihre S-Komponente — die Farbsättigung — benötigen. Als Nächstes berechnen wir die neue Sättigung, indem wir einfach den zur Methode hinzugefügten und mit 0,01 multiplizierten Wert zum Sättigungswert addieren. Prüfen wir, ob das erhaltene Ergebnis über den Bereich der akzeptablen Werte hinausgeht (0-1). Anschließend verwenden wir die Klasse CColors und ihre Methode HSLtoRGB um die H-Farbkomponenten, das neue S und L in das RGB-Format zu konvertieren.
Die Rückgabe der erhaltenen RGB-Farbe mit dem Alphakanal der Originalfarbe
.

Warum wird der Wert zur Änderung der Sättigung, der an die Methode übergeben wird, mit 0,01 multipliziert? Das geschieht nur der Einfachheit halber. Im HSL-Farbmodell ändern sich die Komponentenwerte zwischen 0 und 1. Es ist also bequemer, diese Werte in Vielfachen von 100 zu übergeben (1 statt 0,01, 10 statt 0,1, 100 statt 1). Noch wichtiger ist, dass alle Werte in den Formularstilen, in denen es Werte zur Änderung der Farbsättigung für verschiedene Formulare oder Texte geben kann, als Ganzzahlen festgelegt werden.

Die Methode ändert die Farbsättigung um einen bestimmten Wert:

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

Die Logik der Methode ist ähnlich wie die oben beschriebene. Der einzige Unterschied besteht darin, dass der Transparenz-Parameter hier nur für die Umwandlung der Farbe und ihrer Transparenz in die ARGB-Farbe erforderlich ist. Der Alphakanal wird nirgendwo anders verwendet. Deshalb können wir ihn bei der Konvertierung ignorieren und Null übergeben. Als Nächstes extrahieren wir die R-, G- und B-Komponenten aus der ARGB-Farbe, konvertieren sie in das HSL-Farbmodell, ändern die S-Komponente um den der Methode übergebenen Wert, konvertieren das HSL-Modell zurück nach RGB und geben das in die Farbe konvertierte RGB-Farbmodell im Format 'color' zurück.

Die Methode ändert die Helligkeit von COLOR um einen bestimmten Wert:

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

Die Methode ist identisch mit der oben betrachteten, außer dass hier die L-Komponente des HSL-Farbmodells verändert wird.

Da wir bei allen betrachteten Methoden den Wert, um den die Farbkomponente geändert werden soll, mit 0,01 multiplizieren, müssen wir die zuvor entwickelte Methode, die die ARGB-Farbhelligkeit um einen bestimmten Wert ändert, abändern:

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

Der Methodenblock für einen vereinfachten Zugriff auf Objekteigenschaften des öffentlichen Klassenbereichs enthält die deklarierte Methode, die das Flag setzt, das die Notwendigkeit der Verwendung des Formularschattens anzeigt. Die Methode ist jedoch aus irgendeinem Grund nicht implementiert. Beheben wird das:

//--- 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

Alle Formularobjekte, die ich derzeit erstellt habe, haben durch die hinzugefügten Rahmen einen gewissen Anschein von Dreidimensionalität. Die Rahmen glitzern an den beleuchteten Kanten und verdunkeln sich an den unbeleuchteten, was die Illusion von Dreidimensionalität erzeugt, aber das ist nicht ausreichend. Fügen wir nun die Möglichkeit hinzu, den Hintergrund dreidimensional erscheinen zu lassen. Um dies zu erreichen, benötigen wir eine Farbverlaufsfüllung des Hintergrunds mit mindestens zwei Farben — von dunkler zu heller. Eine kleine Änderung der Helligkeit der ursprünglichen Farbe, eine sanfte Überblendung der ursprünglichen Farbe mit der aufgehellten und ein Schatten reichen aus, um die Form in neuen Farben erscheinen zu lassen:


Ich habe bereits zwei Methoden implementiert, um das Formular zu löschen und es mit Farbe zu füllen. Um den Hintergrund mit einem Farbverlauf zu füllen, deklarieren wir eine weitere Löschmethode 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);                                         }

Schreiben wir seine Implementierung außerhalb des Klassenkörpers:

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

Die gesamte Logik der Methode ist in den Kommentaren des Codes beschrieben. Die Methode empfängt das Array der gefüllten Farbe, den Wert der Transparenz, das Flag für den vertikalen Farbverlauf (wenn true, wird die Füllung von unten nach oben ausgeführt, wenn false — von links nach rechts), das Flag für die Schleife (wenn es gesetzt ist, endet die Füllung mit derselben Farbe, mit der sie begonnen hat) und das Flag, das angibt, ob die Leinwand nach der Füllung neu gezeichnet werden muss. Um das Farbfeld zu erhalten, verwenden Sie die Methode Gradient() der Klasse CColors.

Damit sind die Änderungen und Ergänzungen in den Bibliotheksklassen abgeschlossen. Lassen Sie uns nun die neue Klasse für das Schattenobjekt schreiben, die ein Nachkomme der Objektklasse des grafischen Elements sein soll.


Die Objektklasse der Schatten

Im Verzeichnis \MQL5\Include\DoEasy\Objects\Graph\ der Bibliothek grafische Objekte erstellen wir die neue Datei ShadowObj.mqh der Klasse CShadowObj.

Die Datei des grafischen Elements und die Bibliotheksdatei der numerischen Analyse ALGLIB sollten in die Datei aufgenommen werden. Die Klasse sollte von der Objektklasse des grafischen Elements geerbt werden:

//+------------------------------------------------------------------+
//|                                                    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
  {
  }

Im privaten Teil der Klasse deklarieren wir die Variablen zum Speichern der Farbe und der Transparenz des Schattens sowie die Methoden für die Klassenoperation:

//+------------------------------------------------------------------+
//| 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:

Hier zeichnet die Methode DrawShadowFigureRect() eine unscharfe Form entsprechend den Abmessungen des Formularobjekts, die einen von diesem Objekt gezeichneten Schatten wirft.
Die Methode GetQuadratureWeights() wendet die ALGLIB-Bibliothek an, um das Array von Gewichtsverhältnissen zu berechnen und zurückzugeben, die verwendet werden, um die von der Methode DrawShadowFigureRect() gezeichnete Form unscharf zu machen.
Die Unschärfe der Form wird von der Methode GaussianBlur() durchgeführt.
Alle Methoden werden im Folgenden besprochen.

Im öffentlichen Teil der Klasse deklarieren wir den paramterischen Konstruktor, Methoden, die die Flags zur Unterstützung der Objekteigenschaften zurückgeben (bis beide Methoden true zurückgeben) und Methode zum Zeichnen des Schattens, sowie die Methoden eines vereinfachten Zugriffs auf die Eigenschaften des Schattenobjekts:

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;  }
  };
//+------------------------------------------------------------------+


Betrachten wir die Struktur der Methoden der Klasse im Detail.

Der parametrische Konstruktor:

//+------------------------------------------------------------------+
//| 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();
  }
//+------------------------------------------------------------------+

Der Konstruktor erhält die ID des Charts, den Index des Unterfensters, das zur Erstellung eines Schattenobjekts verwendet wird, seinen Namen, die Koordinaten des oberen linken Formularwinkels und seine Größe. In der Initialisierungsliste übergeben wir den Elementtyp — Schattenobjekt und andere Parameter, die in den Methodenargumenten übergeben wurden, an den geschützten Konstruktor der grafischen Elementklasse.

Im Konstruktorkörper setzen wir den abwesenden Objekthintergrund, seine volle Transparenz und das Objektinaktivitätsflag (das Schattenobjekt soll in keiner Weise auf äußere Einflüsse reagieren). Die Standardtransparenz des auf der Leinwand gezeichneten Schattens ist auf 127 eingestellt. Dies ist ein halbtransparenter Schatten. Als Nächstes berechnen wir die Standard-Schattenfarbe. Dies ist eine Hintergrundfarbe des Chart, die um 50 von 100 Einheiten verdunkelt wird. Hier wird zunächst die Hintergrundfarbe des Charts in einen Grauton umgewandelt und dann die resultierende Farbe verdunkelt. Das Objekt, auf das der Schatten gezeichnet wird, sollte ihn nicht werfen, daher setzen wir sein Schatten-Flag auf false, setzen das Objektsichtbarkeits-Flag auf true und löschen die Leinwand (Hintergrund).

Die Methode zeichnet das Schattenobjekt:

//+------------------------------------------------------------------+
//| 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();
  }
//+------------------------------------------------------------------+

Die gesamte Methodenlogik ist in den Kommentaren zum Code beschrieben. Die Methode zeichnet zunächst ein gewöhnliches Rechteck, das mit der Schattenfarbe gefüllt wird. Die Breite und Höhe des Rechtecks werden so berechnet, dass es die Größe des Formularobjekts hat, das den Schatten wirft. Anschließend wird das gezeichnete Rechteck mit der Gauß-Methode weichgezeichnet, das Schattenobjekt um den angegebenen Versatz relativ zum Formobjekt, das diesen Schatten wirft, verschoben und die Leinwand des Schattenobjekts aktualisiert.

Die Methode zum Zeichnen des Objekts Formulars mit Schatten:

//+------------------------------------------------------------------+
//| 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();
  }
//+------------------------------------------------------------------+

Hier wird das Rechteck in den X/Y-Koordinaten gezeichnet, die dem konstanten Wert OUTER_AREA_SIZE entsprechen. Die zweite X/Y-Koordinate wird als Versatz zur ersten Koordinate + Breite (Höhe) minus 1 berechnet. Nachdem die Form gezeichnet wurde, wird die Leinwand aktualisiert.

Die Methode verwischt die gezeichnete Form mit Hilfe der Gaußschen Methode:

//+------------------------------------------------------------------+
//| Gaussian blur                                                    |
//| https://www.mql5.com/de/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;
  }
//+------------------------------------------------------------------+

Die Logik der Methode wird in den Code-Kommentaren beschrieben. Weitere Details finden Sie im Artikel, dem die Methode entnommen wurde.

Die Methode gibt das Array mit den Gewichtsverhältnissen zurück:

//+------------------------------------------------------------------+
//| Return the array of weight ratios                                |
//| https://www.mql5.com/de/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;
  }
//+------------------------------------------------------------------+

Die Methode berechnet die Unschärfeverhältnisse mit Hilfe der ALGLIB-Bibliothek für numerische Analyse und setzt sie in das Array der Gewichte, das ihr über den Link übergeben wird. Die Details befinden sich im folgenden Artikelabschnitt.

Damit ist die Entwicklung der ersten Version der Schattenobjektklasse abgeschlossen.

Jetzt müssen wir die Möglichkeit implementieren, den Schatten schnell zu erstellen und direkt aus dem Formularobjekt zu zeichnen.

Wir öffnen \MQL5\Include\DoEasy\Objects\Graph\Form.mqh der Formularobjektklasse und nehmen die notwendigen Verbesserungen vor.

Damit die Formularobjektklasse die Schattenobjektklasse sehen kann, binden wir die Datei der kürzlich erstellten Schattenklasse in diese ein:

//+------------------------------------------------------------------+
//|                                                         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                                                |
//+------------------------------------------------------------------+

Aus dem privaten Bereich der Klasse entfernen wir die Variable, die die Farbe des Formularschattens speichert:

   color             m_color_shadow;                           // Form shadow color

Jetzt wird die Schattenfarbe in der Klasse des Schattenobjekts gespeichert.

Das Ergebnis ist, dass unser Formularobjekt es erlaubt, neue grafische Elementobjekte darin zu erstellen und sie an die Liste der abhängigen Objekte anzuhängen. Mit anderen Worten, diese neu erstellten Objekte hängen vollständig von dem Form-Objekt ab und gehören zu ihm. Das Formularobjekt ist in der Lage, sie zu verwalten. Um solche Objekte zu erstellen, müssen wir ihren Namen erstellen, der den Namen des Formularobjekts mit dem Zusatz seines eigenen Namens am Ende enthalten sollte. Um dies zu erreichen, fügen wir die Methode, die den Namen des abhängigen Objekts erzeugt, in den privaten Bereich der Klasse ein:

//--- 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

Das Anlegen eines solchen Namens für das Objekt habe ich bereits im vorherigen Artikel bei der Beschreibung des Formularobjekts implementiert:

... die Endung aus dem Namensobjekt abrufen (der Name setzt sich aus dem Programmnamen und dem bei der Erstellung vergebenen Objektnamen zusammen). Wir müssen den Objektnamen bei seiner Erstellung abfragen und den an die Methode übergebenen Namen hinzufügen.
Im Falle eines Namens wie beispielsweise "Programm_name_Form01" extrahieren wir die Teilzeichenkette "Form01" und fügen den an die Methode übergebenen Namen hinzu. Wenn wir ein Schattenobjekt erstellen und den Namen"Shadow" (Schatten) übergeben, lautet der Objektname "Form01_ Shadow", während der endgültige Name des erstellten Objekts wie folgt lautet: ...Program_name_Form01_Shadow"...

Dies wird nun in einer separaten Methode durchgeführt, da es mehr als einmal benötigt wird.

Außerdem deklarieren wir im privaten Abschnitt die Methode zur Erstellung des Schattenobjekts:

//--- 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:

Aus dem öffentlichen Abschnitt der Klasse entfernen wir die Methodendeklaration:

//--- 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

Nun wird diese Methode nicht mehr öffentlich zugänglich sein, und die Farbe des Schattens sowie seine Transparenz werden zusätzlich an sie übertragen.

Die öffentliche Methode, die den Objektschatten zeichnet, wird nun auch mehr Argumente haben:

//--- 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

Dies geschieht, damit wir nicht erst das Schattenobjekt erstellen müssen, sondern bereits nach dem Rendering die Methode zum Zeichnen des Schattens aufrufen können. Die Logik hier ist einfach. Wenn wir die Methode zum Zeichnen des Schattens aufrufen, dann brauchen wir sie nicht. Wenn wir das Schattenobjekt nicht erstellt haben, erstellt die neue Methode zuerst das Objekt, zeichnet den Schatten darauf und zeigt ihn auf dem Bildschirm an.

Entfernen wir also die Implementierung der Methoden zum Setzen und Zurückgeben der Schattenfarbe aus dem Methodenblock für einen vereinfachten Zugriff auf die Objekteigenschaften:

//+------------------------------------------------------------------+
//| 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; }

Jetzt werden diese Methoden außerhalb des Klassenkörpers verschoben (die Prüfung auf das Vorhandensein des Schattenobjekts wird dort benötigt), während nur ihre Deklaration hier bleibt. Außerdem fügen wir die Deklaration der Methoden zum Setzen und Zurückgeben der Transparenz des Schattens hinzu:

//+------------------------------------------------------------------+
//| 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;

  };
//+------------------------------------------------------------------+

Wir ersetzen die folgenden Zeichenfolgen in der Methode zur Erstellung eines neuen grafischen Elements

   int pos=::StringLen(::MQLInfoString(MQL_PROGRAM_NAME));
   string pref=::StringSubstr(NameObj(),pos+1);
   string name=pref+"_"+obj_name;

durch den Aufruf der Methode zum Erstellen eines abhängigen Objektnamens:

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

Nach der Erstellung eines Schattenobjekts sollten die Standardparameter sofort auf das Objekt gesetzt werden.

Um dies zu erreichen, verbessern wir ein bisschen die Methode zur Erstellung eines Schattenobjekts:

//+------------------------------------------------------------------+
//| 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();
  }
//+------------------------------------------------------------------+

Die Verbesserung der Methode zum Zeichnen des Schattens, so dass sie zunächst in Abwesenheit des Schattenobjekts erstellt wird und der Schatten anschließend auf dieses gezeichnet wird:

//+------------------------------------------------------------------+
//| 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();
     }
  }
//+------------------------------------------------------------------+

Die Logik der Methode ist in den Codekommentaren beschrieben und sollte keine Schwierigkeiten verursachen.

In der Methode, die das Farbschema festlegt, prüfen wir das Flag der Verwendung des Schattens und das Vorhandensein des erstellten Schattenobjekts, bevor wir die Zeichenfarbe für das Schattenobjekt festlegen:

//+------------------------------------------------------------------+
//| 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]);
  }
//+------------------------------------------------------------------+

Die Methode zum Einstellen des Formularstils erhält die neue Eingabe, die die Notwendigkeit anzeigt, die Hintergrundfarbe des Charts für die Erstellung einer Schattenfarbe zu verwenden, sowie den Schatten dazu zu zeichnen:

//+------------------------------------------------------------------+
//| 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());
  }
//+------------------------------------------------------------------+

Die Logik der Methode ist in den Kommentaren beschrieben. Kurz gesagt, erstellen wir zuerst das Schattenobjekt. Nach dem Einstellen der Farbpalette berechnen wir die gewünschte Farbe zum Zeichnen des Schattens. Wenn das Flag für die Verwendung der Hintergrundfarbe gesetzt ist, verwenden wir die Hintergrundfarbe des Charts, die in einfarbig umgewandelt und mit dem Wert des Verdunkelungsparameters, der im Formularstil in der Datei GraphINI.mqh festgelegt wurde, abgedunkelt wird. Wenn das Flag nicht gesetzt ist, wird die Farbe auf die gleiche Weise abgedunkelt. Die Farbe wird in den Farbschemata des Formulars in der Datei GraphINI.mqh festgelegt. Als Nächstes rufen wir die Methode zum Zeichnen des Schattens auf, die den Schatten nur dann zeichnet, wenn das Flag für die Verwendung von Schatten für das Formularobjekt gesetzt ist.

In allen Methoden zum Aufhellen/Abdunkeln der Formularrahmen, ersetzen wir die in reellen Zahlen angegebenen Werte

      //--- 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));
        }

mit den entsprechenden ganzzahligen Werten, die hundertmal größer sind (in den Methoden, die in diesen Zeichenketten aufgerufen werden, die hinzugefügt wurden, sodass der übergebene Wert durch 100 dividiert wird):

      //--- 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));
        }

Dies ist bereits bei allen Methoden geschehen, bei denen die Werte ersetzt werden mussten. Wir werden diese Änderungen hier nicht wiederholen — Sie finden die Codes in den unten angehängten Dateien.

Die Methode zur Einstellung der Schattenfarbe des Formulars:

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

Hier wird das Vorhandensein des Schattenobjekts geprüft und seine Schattenfarbe nur dann eingestellt, wenn das Objekt vorhanden ist. Andernfalls wird eine Journalmeldung angezeigt, die über das Fehlen des Schattenobjekts informiert und dazu auffordert, es zuerst zu erstellen.

Die Methode gibt die Schattenfarbe des Formulars zurück:

//+------------------------------------------------------------------+
//| 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();
  }
//+------------------------------------------------------------------+

Hier wird zunächst geprüft, ob das Objekt vorhanden ist und die Schattenfarbe zurückgegeben.

Die Methoden zum Setzen und Zurückgeben der Transparenz des Schattens:

//+------------------------------------------------------------------+
//| 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();
  }
//+------------------------------------------------------------------+

Die Logik dieser Methoden ist identisch mit der Logik der beiden oben beschriebenen Methoden.

Wir sind nun bereit, das Erstellen eines Schattenobjekts für die Formulare zu testen.


Test

Testen wir das Erstellen von Schatten für Formularobjekte. Zwei Formulare werden mit den Parametern erstellt, die in den Formularstilen und Farbschemata stehen (ähnlich wie im vorherigen Artikel). Das dritte Formular wird "manuell" erstellt, was ein weiteres Beispiel dafür ist, wie man ein benutzerdefiniertes Formular zeichnet. Da die Schattenobjekte für die Formulare erst nach der Erstellung des Formulars selbst gezeichnet werden, müsste man herausfinden, welches der Objekte auf einen Klick reagiert: Wenn das Formularobjekt über dem Objekt steht, auf dem sein Schatten gezeichnet ist, wird beim Klicken auf das Formular der Formularname im Journal angezeigt. Befindet sich das Schattenobjekt immer noch über dem Formularobjekt, dann zeigt das Journal den Namen des Formularschattenobjekts an.

Um den Test durchzuführen, verwenden wir den EA aus dem vorigen Artikel und speichern ihn in \MQL5\Experts\TestDoEasy\Part77\ als TestDoEasyPart77.mq5.

In der Liste der EA-Eingaben die Einstellung hinzufügen, die es uns erlaubt, die Schattenfarbe zu wählen — die Hintergrundfarbe des Charts oder eine bestimmte Farbe, die in der nächsten Eingabe angegeben werden kann. In der Liste der globalen Variablen fügen wir das Array hinzu, das die Farben für die Füllung des Formulars mit einem Farbverlauf speichert:

//+------------------------------------------------------------------+
//|                                             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[];
//+------------------------------------------------------------------+

In OnInit() ergänzen wir das Erstellen des dritten Formularobjekts:

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

Die gesamte Logik der Formularerstellung wird in den Codekommentaren festgelegt. Dies ist eine weitere Möglichkeit, eigene Formularobjekte zu erstellen.

Wir fügen in OnChartEvent() die Anzeige eines grafischen Objektnamens im Journal beim Anklicken hinzu:

//+------------------------------------------------------------------+
//| 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);
     }
  }
//+------------------------------------------------------------------+

Kompilieren Sie den EA, starten Sie ihn auf dem Chart und ändern Sie die Standardeinstellung der Schatten:


Leider können wir mit einem GIF-Bild nicht die gesamte Farbpalette sehen.

Nachfolgend sehen Sie, wie sich das Formular mit dem Farbverlaufshintergrund im PNG-Bildformat präsentiert:


Wenn Sie auf die einzelnen Formulare klicken, werden die Namen der Formulare (und nicht die Namen ihrer Schattenobjekte) im Journal angezeigt:

TestDoEasyPart77_Form_01
TestDoEasyPart77_Form_02
TestDoEasyPart77_Form_03

Dies bedeutet, dass das Schattenobjekt nach seiner Erstellung aus dem Formularobjekt noch in den Hintergrund verschoben werden kann, um das Formular, das es erstellt hat, nicht zu beeinträchtigen.

Was kommt als Nächstes?

Im nächsten Artikel werde ich die Entwicklung der Klasse der Formularobjekte fortsetzen und damit beginnen, statische Bilder schrittweise zu "animieren".

Alle Dateien der aktuellen Version der Bibliothek sind unten zusammen mit der Test-EA-Datei für MQL5 zum Testen und Herunterladen angehängt.
Ihre Fragen und Vorschläge schreiben Sie bitte in den Kommentarteil.

Zurück zum Inhalt

*Frühere Artikel dieser Serie:

Grafiken in der Bibliothek DoEasy (Teil 73): Das Formularobjekt eines grafischen Elements
Grafiken in der Bibliothek DoEasy (Teil 74): Das grafisches Basiselement, das von der Klasse CCanvas unterstützt wird
Grafiken in der Bibliothek DoEasy (Teil 75): Methoden zur Handhabung von Primitiven und Text im grafischen Grundelement
Grafiken in der Bibliothek DoEasy (Teil 76): Das Formularobjekt und vordefinierte Farbschemata