Grafik in der Bibliothek DoEasy (Teil 77): Objektklasse der Schatten
Inhalt
- Konzept
- Verbesserung der Klassenbibliothek
- Die Objektklasse der Schatten
- Test
- Was kommt als Nächstes?
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:
- direkt auf der Leinwand des Formularobjekts selbst,
- 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.
*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
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/9575
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.