![Grafik in der Bibliothek DoEasy (Teil 78): Animationsprinzipien in der Bibliothek. Schneiden von Bildern](https://c.mql5.com/i/articles/overlay.png)
Grafik in der Bibliothek DoEasy (Teil 78): Animationsprinzipien in der Bibliothek. Schneiden von Bildern
Inhalt
- Konzept
- Verbesserung der Klassenbibliothek
- Klasse zum Kopieren und Einfügen von Bildteilen
- Test
- Was kommt als Nächstes?
Konzept
Eine grafische Schnittstelle impliziert von Natur aus das Vorhandensein von nicht-statischen Bildern. Die angezeigten Daten (z. B. in Tabellen) können sich mit der Zeit ändern. GUI-Elemente können auf Benutzeraktionen reagieren, indem sie verschiedene visuelle Effekte verwenden, usw.
Ich werde die Methoden erstellen, die verschiedene visuelle Effekte arrangieren und die Bibliothek mit der Fähigkeit ausstatten, mit Sprite-Animation zu arbeiten. Die Animation basiert auf der Verwendung einer sich ändernden Sequenz (Bild für Bild) von statischen Bildern.
Die Klasse CCanvas ermöglicht das Zeichnen von Bildern auf der Leinwand (canvas). Aus einer Reihe von gezeichneten und in Bild-Arrays gespeicherten Bildern können wir eine bestimmte Sequenz aufbauen, die schließlich ein animiertes Bild sein wird. Wenn wir jedoch jedes Bild einzeln auf die Leinwand zeichnen, werden sie sich einfach überlappen, was zu einem chaotischen Pixelhaufen führt, wie im folgenden Bild (hier zeige ich einfach den Text an verschiedenen Stellen des Formularobjekts an):
Um dies zu vermeiden, müssen wir das vorherige Bild vollständig löschen, den Hintergrund neu zeichnen und den Text darauf anzeigen (ich habe dies in einem der vorherigen Artikel getan, als ich den Text im Formular platzierte und die Textankermethode beschrieb). Diese Option ist nur dann praktikabel, wenn die Größe und Komplexität des neu gezeichneten Formulars gering ist. Eine andere Möglichkeit besteht darin, einen Teil des Hintergrunds, auf dem der Text eingeblendet werden soll, im Speicher zu sichern (in einem Array) und dann den Text hinzuzufügen. Wenn der Text an neue Koordinaten verschoben werden soll, überschreiben wir den gezeichneten Text mit dem zuvor gespeicherten Hintergrundbild aus dem Array (um den Hintergrund wiederherzustellen) und zeichnen den Text an der neuen Stelle (wobei wir den Teil des Hintergrunds der Stelle, an die der Text verschoben wird, zuvor speichern). Auf diese Weise wird der Hintergrund der Stelle, an der ein Bild eingeblendet werden soll, ständig im Speicher gehalten und wiederhergestellt, wenn das Bild geändert werden muss.
Dies ist das minimale Element des Sprite-Animationskonzepts, das ich in der Bibliothek einführen werde:
- Das Speichern eines Hintergrunds mit den erforderlichen Koordinaten.
- Die Anzeige eines Bildes unter Verwendung der Koordinaten.
- Die Wiederherstellung des Hintergrunds, wenn das Bild neu gezeichnet wird.
Um all dies zu erreichen, werde ich eine kleine Klasse zum Speichern der Bildkoordinaten und der Größe erstellen. Die Methode, die einen Teil eines Hintergrundbildes mit diesen Koordinaten und der Größe speichert, wird ebenfalls in dieser Klasse erstellt. Außerdem brauchen wir die zweite Methode, die den im Array gespeicherten Hintergrund speichert (die Größe und die Koordinaten werden in den Klassenvariablen gespeichert, wenn der Hintergrund im Array gespeichert wird).
Warum erstelle ich eine Klasse, anstatt zwei solche Methoden für das Formularobjekt zu erstellen? Das ist ganz einfach: Wenn wir nur einen Text oder ein einziges animiertes Bild anzeigen wollen, reichen zwei Methoden aus. Wenn wir aber mehrere Texte an verschiedenen Stellen des Formulars anzeigen müssen, ist die Klasse praktischer. Jedes animierte Bild erhält seine eigenen Klasseninstanzen, die separat verwaltet werden können.
Ein solches Konzept ermöglicht es, etwas zu zeichnen und dabei ein zuvor gezeichnetes Bild als Hintergrund zu verwenden — wir werden sowohl den Hintergrund als auch das gezeichnete Bild speichern, das dann wiederum vom Hintergrund entfernt werden kann.
Ich werde dieses Konzept nutzen, um die Klasse zum Erstellen, Speichern und Anzeigen verschiedener Sprite-Animationen auf dem Formularobjekt zu entwickeln — jede Klasseninstanz enthält eine Folge von Bildern, die dynamisch zur Liste hinzugefügt und bearbeitet werden können.
Verbesserung der Klassenbibliothek
Wie üblich fügen wir neue Nachrichtenindizes zu \MQL5\Include\DoEasy\Data.mqh hinzu:
//--- 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 //--- CGCnvElement MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY, // Error! Empty array //--- 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 new shadow object MSG_FORM_OBJECT_ERR_FAILED_CREATE_PC_OBJ, // Failed to create new pixel copier object MSG_FORM_OBJECT_PC_OBJ_ALREADY_IN_LIST, // Pixel copier object with ID already present in the list MSG_FORM_OBJECT_PC_OBJ_NOT_EXIST_LIST, // No pixel copier object with ID in the list //--- 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:
//--- 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"}, //--- CGCnvElement {"Ошибка! Пустой массив","Error! Empty array"}, //--- CForm {"Отсутствует объект тени. Необходимо сначала его создать при помощи метода CreateShadowObj()","There is no shadow object. You must first create it using the CreateShadowObj () method"}, {"Не удалось создать новый объект для тени","Failed to create new object for shadow"}, {"Не удалось создать новый объект-копировщик пикселей","Failed to create new pixel copier object"}, {"В списке уже есть объект-копировщик пикселей с идентификатором ","There is already a pixel copier object in the list with ID "}, {"В списке нет объекта-копировщика пикселей с идентификатором ","No pixel copier object with ID "}, //--- CShadowObj {"Ошибка! Размер изображения очень маленький или очень большое размытие","Error! Image size is very small or very large blur"}, }; //+---------------------------------------------------------------------+
Da wir Bilder und Texte auf vorgefertigte Formularobjekte, die vom grafischen Elementobjekt geerbt wurden, oder auf andere GUI-Objekte von benutzerdefinierten Programmen zeichnen werden, müssen wir immer das ursprüngliche Aussehen des Objekts zur Hand haben, damit wir es jederzeit in seine ursprüngliche Form zurückversetzen können.
Natürlich können wir es neu zeichnen, aber es wird viel schneller sein, einfach ein Array in ein anderes zu kopieren.
Zu diesem Zweck werde ich einige Änderungen und Verbesserungen in \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh der Objektklasse für grafische Elemente vornehmen.
Im geschützten Abschnitt der Klasse deklarieren wir das Array, das alle Pixel des Ausgangsobjekts (sein Aussehen) unmittelbar nach seiner Erstellung enthalten soll, und die Methode, die die grafische Ressource der CCanvas-Klasse-Instanz im Array speichert:
//+------------------------------------------------------------------+ //| Class of the graphical element object | //+------------------------------------------------------------------+ class CGCnvElement : public CGBaseObj { protected: CCanvas m_canvas; // CCanvas class object CPause m_pause; // Pause class object bool m_shadow; // Shadow presence color m_chart_color_bg; // Chart background color uint m_data_array[]; // Array for storing resource data copy //--- Return the cursor position relative to the (1) entire element and (2) the element's active area bool CursorInsideElement(const int x,const int y); bool CursorInsideActiveArea(const int x,const int y); //--- Create (1) the object structure and (2) the object from the structure virtual bool ObjectToStruct(void); virtual void StructToObject(void); //--- Save the graphical resource to the array bool ResourceCopy(const string source); private:
Auf diese Weise können wir mit geringem Speicheraufwand das Aussehen eines beliebigen Elements der Programmoberfläche schnell wiederherstellen, indem wir einfach ein Array in ein anderes kopieren.
Im privaten Abschnitt der Klasse deklarieren wir zwei Variablen zum Speichern der X und Y-Koordinaten des zuletzt gezeichneten Textes:
long m_long_prop[ORDER_PROP_INTEGER_TOTAL]; // Integer properties double m_double_prop[ORDER_PROP_DOUBLE_TOTAL]; // Real properties string m_string_prop[ORDER_PROP_STRING_TOTAL]; // String properties ENUM_TEXT_ANCHOR m_text_anchor; // Current text alignment int m_text_x; // Text last X coordinate int m_text_y; // Text last Y coordinate color m_color_bg; // Element background color uchar m_opacity; // Element opacity //--- Return the index of the array the order's (1) double and (2) string properties are located at
Im öffentlichen Abschnitt der Klasse schreiben wir die Methode, die den Zeiger auf den aktuellen Klassenmoment zurückgibt und deklarieren die Methode zum Speichern eines Bildes in dem angegebenen Array:
//--- Return the flag of the object supporting this property virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER property) { return true; } virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_DOUBLE property) { return false; } virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING property) { return true; } //--- Return itself CGCnvElement *GetObject(void) { return &this; } //--- Compare CGCnvElement objects with each other by all possible properties (for sorting the lists by a specified object property) virtual int Compare(const CObject *node,const int mode=0) const; //--- Compare CGCnvElement objects with each other by all properties (to search equal objects) bool IsEqual(CGCnvElement* compared_obj) const; //--- (1) Save the object to file and (2) upload the object from the file virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); //--- Create the element bool Create(const long chart_id, const int wnd_num, const string name, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool redraw=false); //--- Return the pointer to a canvas object CCanvas *GetCanvasObj(void) { return &this.m_canvas; } //--- Set the canvas update frequency void SetFrequency(const ulong value) { this.m_pause.SetWaitingMSC(value); } //--- Update the coordinates (shift the canvas) bool Move(const int x,const int y,const bool redraw=false); //--- Save an image to the array bool ImageCopy(const string source,uint &array[]);
Die Methode, die es der Klasse erlaubt, den Zeiger auf sich selbst zurückzugeben, ist notwendig, um den Zeiger auf die Klasse an die unten betrachtete Pixelkopiererklasse zu übergeben, während die Methode, die die grafische Ressource der CCanvas-Instanz kopiert, notwendig ist, um das Erscheinungsbild des Formulars schnell in das notwendige Array im bibliotheksbasierten Programm zu kopieren.
Fügen wir in den Codeblock der Methoden für die Arbeit mit einem Text zwei Methoden für die Rückgabe der X und Y-Koordinaten des zuletzt gezeichneten Textes ein:
//+------------------------------------------------------------------+ //| Methods of working with text | //+------------------------------------------------------------------+ //--- Return (1) alignment type (anchor method), the last (2) X and (3) Y text coordinate ENUM_TEXT_ANCHOR TextAnchor(void) const { return this.m_text_anchor; } int TextLastX(void) const { return this.m_text_x; } int TextLastY(void) const { return this.m_text_y; } //--- Set the current font
Die Methoden geben einfach die Werte der entsprechenden Variablen zurück.
Damit diese Werte immer relevant bleiben, weisen wir die Koordinaten, die den Methodenargumenten übergeben werden, den Variablen der Methode zu, die den Text mit der aktuellen Schriftart anzeigt:
//--- Display the text in the current font void Text(int x, // X coordinate of the text anchor point int y, // Y coordinate of the text anchor point string text, // Display text const color clr, // Color const uchar opacity=255, // Opacity uint alignment=0) // Text anchoring method { this.m_text_anchor=(ENUM_TEXT_ANCHOR)alignment; this.m_text_x=x; this.m_text_y=y; this.m_canvas.TextOut(x,y,text,::ColorToARGB(clr,opacity),alignment); }
Der gezeichnete Text kann neun Ankerpunkte haben:
Befindet sich der Textankerpunkt beispielsweise in der rechten unteren Ecke (Right|Bottom), so ist dies die XY-Startkoordinate. Alle Anfangskoordinaten in der Bibliothek entsprechen der linken oberen Ecke des Rechtecks (Links|Oben). Wenn wir also das Bild mit den Anfangskoordinaten des Textes speichern, befindet sich der Text rechts unten im gespeicherten Bild. Dadurch lässt sich der Bereich des Hintergrunds, der vom Text überlagert wird, nicht korrekt speichern.
Daher müssen wir die Offsets der Koordinaten des Textumriss-Rechtecks berechnen, wo der Hintergrund für seine spätere Wiederherstellung im Array gespeichert werden muss. Die Breite und Höhe des zukünftigen Textes werden im Voraus berechnet — bevor der Text gezeichnet wird. Wir müssen nur den Text selbst angeben. Die Methode TextSize() der Klasse CCanvas gibt die Breite und Höhe des umrahmenden Rechtecks zurück.
Im öffentlichen Abschnitt der Klasse deklarieren wie die Methode, die X/Y-Offsets in Abhängigkeit von der Textausrichtungsmethode zurückgibt:
//--- Return coordinate offsets relative to the text anchor point void TextGetShiftXY(const string text, // Text for calculating the size of its outlining rectangle const ENUM_TEXT_ANCHOR anchor,// Text anchor point, relative to which the offsets are calculated int &shift_x, // X coordinate of the rectangle upper left corner int &shift_y); // Y coordinate of the rectangle upper left corner }; //+------------------------------------------------------------------+ //| Parametric constructor | //+------------------------------------------------------------------+
Die Methode wird im Folgenden besprochen.
Wir initialisieren die Koordinaten des zuletzt gezeichneten Textes im Konstruktor der parametrischen Klasse:
//+------------------------------------------------------------------+ //| Parametric constructor | //+------------------------------------------------------------------+ CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type, const int element_id, const int element_num, const long chart_id, const int wnd_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=true, const bool activity=true, const bool redraw=false) : m_shadow(false) { this.m_chart_color_bg=(color)::ChartGetInteger(chart_id,CHART_COLOR_BACKGROUND); this.m_name=this.m_name_prefix+name; this.m_chart_id=chart_id; this.m_subwindow=wnd_num; this.m_type=element_type; this.SetFont("Calibri",8); this.m_text_anchor=0; this.m_text_x=0; this.m_text_y=0; this.m_color_bg=colour; this.m_opacity=opacity; if(this.Create(chart_id,wnd_num,this.m_name,x,y,w,h,colour,opacity,redraw)) { this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,this.m_canvas.ResourceName()); // Graphical resource name this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,CGBaseObj::ChartID()); // Chart ID this.SetProperty(CANV_ELEMENT_PROP_WND_NUM,CGBaseObj::SubWindow()); // Chart subwindow index this.SetProperty(CANV_ELEMENT_PROP_NAME_OBJ,CGBaseObj::Name()); // Element object name this.SetProperty(CANV_ELEMENT_PROP_TYPE,element_type); // Graphical element type this.SetProperty(CANV_ELEMENT_PROP_ID,element_id); // Element ID this.SetProperty(CANV_ELEMENT_PROP_NUM,element_num); // Element index in the list this.SetProperty(CANV_ELEMENT_PROP_COORD_X,x); // Element's X coordinate on the chart this.SetProperty(CANV_ELEMENT_PROP_COORD_Y,y); // Element's Y coordinate on the chart this.SetProperty(CANV_ELEMENT_PROP_WIDTH,w); // Element width this.SetProperty(CANV_ELEMENT_PROP_HEIGHT,h); // Element height this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_LEFT,0); // Active area offset from the left edge of the element this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_TOP,0); // Active area offset from the upper edge of the element this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT,0); // Active area offset from the right edge of the element this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM,0); // Active area offset from the bottom edge of the element this.SetProperty(CANV_ELEMENT_PROP_MOVABLE,movable); // Element moveability flag this.SetProperty(CANV_ELEMENT_PROP_ACTIVE,activity); // Element activity flag this.SetProperty(CANV_ELEMENT_PROP_RIGHT,this.RightEdge()); // Element right border this.SetProperty(CANV_ELEMENT_PROP_BOTTOM,this.BottomEdge()); // Element bottom border this.SetProperty(CANV_ELEMENT_PROP_COORD_ACT_X,this.ActiveAreaLeft()); // X coordinate of the element active area this.SetProperty(CANV_ELEMENT_PROP_COORD_ACT_Y,this.ActiveAreaTop()); // Y coordinate of the element active area this.SetProperty(CANV_ELEMENT_PROP_ACT_RIGHT,this.ActiveAreaRight()); // Right border of the element active area this.SetProperty(CANV_ELEMENT_PROP_ACT_BOTTOM,this.ActiveAreaBottom()); // Bottom border of the element active area } else { ::Print(CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.m_name); } } //+------------------------------------------------------------------+
Initialisierung der Variablen im geschützten Konstruktor auf die gleiche Weise:
//+------------------------------------------------------------------+ //| Protected constructor | //+------------------------------------------------------------------+ CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type, const long chart_id, const int wnd_num, const string name, const int x, const int y, const int w, const int h) : m_shadow(false) { this.m_chart_color_bg=(color)::ChartGetInteger(chart_id,CHART_COLOR_BACKGROUND); this.m_name=this.m_name_prefix+name; this.m_chart_id=chart_id; this.m_subwindow=wnd_num; this.m_type=element_type; this.SetFont("Calibri",8); this.m_text_anchor=0; this.m_text_x=0; this.m_text_y=0; this.m_color_bg=NULL_COLOR; this.m_opacity=0; if(this.Create(chart_id,wnd_num,this.m_name,x,y,w,h,this.m_color_bg,this.m_opacity,false)) { ...
Besprechen wir nun die Implementierung der oben angegebenen Methoden.
Implementierung der Methode zum Speichern des Bildes im Array:
//+------------------------------------------------------------------+ //| Save the image to the array | //+------------------------------------------------------------------+ bool CGCnvElement::ImageCopy(const string source,uint &array[]) { ::ResetLastError(); int w=0,h=0; if(!::ResourceReadImage(this.NameRes(),array,w,h)) { CMessage::ToLog(source,MSG_LIB_SYS_FAILED_GET_DATA_GRAPH_RES,true); return false; } return true; } //+------------------------------------------------------------------+
Die Methode erhält den Namen der Methode oder Funktion, von der sie aufgerufen wurde (um einen möglichen Fehler zu finden) und den Link zu dem Array, in das die grafischen Ressourcendaten (Bildpixel) geschrieben werden sollen.
Verwenden wir die Funktion ResourceReadImage(), um die Daten der grafischen Ressource, die von der CCanvas-Klasse erstellt wurde und das Bild des Formulars enthält, in das Array zu lesen. Im Falle eines Fehlers beim Lesen der Ressource wird dies gemeldet und false zurückgegeben. Wenn alles in Ordnung ist, wird true zurückgegeben. Alle in der Ressource gespeicherten Bildpunkte werden in das Array geschrieben, das der Methode übergeben wird.
Die Methode speichert die grafische Ressource in dem Array:
//+------------------------------------------------------------------+ //| Save the graphical resource to the array | //+------------------------------------------------------------------+ bool CGCnvElement::ResourceCopy(const string source) { return this.ImageCopy(DFUN,this.m_data_array); } //+------------------------------------------------------------------+
Die Methode gibt das Ergebnis des Aufrufs der oben genannten Methode zurück. Der einzige Unterschied besteht darin, dass die grafischen Ressourcendaten in das zuvor deklarierte spezielle Array zur Speicherung der Kopie des Bildes des gesamten Formularobjekts geschrieben werden, anstatt in das Array, das durch den Link übergeben wird.
Die Methode gibt die Koordinatenoffsets relativ zum Textankerpunkt zurück:
//+------------------------------------------------------------------+ //| Return coordinate offsets relative to the text anchor point | //+------------------------------------------------------------------+ void CGCnvElement::TextGetShiftXY(const string text,const ENUM_TEXT_ANCHOR anchor,int &shift_x,int &shift_y) { int tw=0,th=0; this.TextSize(text,tw,th); switch(anchor) { case TEXT_ANCHOR_LEFT_TOP : shift_x=0; shift_y=0; break; case TEXT_ANCHOR_LEFT_CENTER : shift_x=0; shift_y=-th/2; break; case TEXT_ANCHOR_LEFT_BOTTOM : shift_x=0; shift_y=-th; break; case TEXT_ANCHOR_CENTER_TOP : shift_x=-tw/2; shift_y=0; break; case TEXT_ANCHOR_CENTER : shift_x=-tw/2; shift_y=-th/2; break; case TEXT_ANCHOR_CENTER_BOTTOM : shift_x=-tw/2; shift_y=-th; break; case TEXT_ANCHOR_RIGHT_TOP : shift_x=-tw; shift_y=0; break; case TEXT_ANCHOR_RIGHT_CENTER : shift_x=-tw; shift_y=-th/2; break; case TEXT_ANCHOR_RIGHT_BOTTOM : shift_x=-tw; shift_y=-th; break; default: shift_x=0; shift_y=0; break; } } //+------------------------------------------------------------------+
Hier wird zunächst die Größe des an die Methode übergebenen Textes ermittelt (die Größe ist in den deklarierten Variablen festgelegt) und dann einfach die Anzahl der Pixel berechnet, die erforderlich sind, um die X- und Y-Koordinaten relativ zu den ursprünglichen Textkoordinaten zu verschieben in Abhängigkeit von der Ankermethode des an die Methode übergebenen Textes.
Nun ist es an der Zeit, die Klasse der Schattenobjekte zu verbessern. Da ich nur die Methoden zum Lesen der grafischen Ressource und ein konstantes Array hinzugefügt habe, in dem ich die Kopie der grafischen Ressource speichern kann, können überflüssige Variablen, Arrays und Codeblöcke aus der Schattenobjektklasse entfernt werden.
Verbessern wir nun die Datei \MQL5\Include\DoEasy\Objects\Graph\ShadowObj.mqh.
Wir entfernen das Array und die unnötigen Variablen aus der Gaußschen Unschärfe-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
Im Block zum Lesen der grafischen Ressourcendaten, ersetzen wir die Zeilen mit dem Aufruf der oben gezeigten Methode:
//--- 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' //--- Read graphical resource data. If failed, return false if(!CGCnvElement::ResourceCopy(DFUN)) return false;
Anstelle der entfernten Variablen res_w und res_h verwende ich im gesamten Code die Methoden Width() (Breite) und Height() (Höhe) der Objektklasse des grafischen Elements. Anstelle des Arrays res_data verwende ich das Array m_data_array, das nun zum Speichern der Kopie der grafischen Ressource verwendet wird.
Im Allgemeinen laufen alle Verbesserungen darauf hinaus, unnötige und entfernte Variablen durch die Methoden der Objektklasse des grafischen Elements zu ersetzen:
//+------------------------------------------------------------------+ //| Gaussian blur | //| https://www.mql5.com/de/articles/1612#chapter4 | //+------------------------------------------------------------------+ bool CShadowObj::GaussianBlur(const uint radius) { //--- int n_nodes=(int)radius*2+1; //--- Read graphical resource data. If failed, return false if(!CGCnvElement::ResourceCopy(DFUN)) return false; //--- Check the blur amount. If the blur radius exceeds half of the width or height, return 'false' if((int)radius>=this.Width()/2 || (int)radius>=this.Height()/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(this.m_data_array); //--- 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::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"a_h_data\""); return false; } if(::ArrayResize(r_h_data,size)==-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"r_h_data\""); return false; } if(::ArrayResize(g_h_data,size)==-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"g_h_data\""); return false; } if(ArrayResize(b_h_data,size)==-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"b_h_data\""); return false; } if(::ArrayResize(a_v_data,size)==-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"a_v_data\""); return false; } if(::ArrayResize(r_v_data,size)==-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"r_v_data\""); return false; } if(::ArrayResize(g_v_data,size)==-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"g_v_data\""); return false; } if(::ArrayResize(b_v_data,size)==-1) { CMessage::ToLog(DFUN,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(this.m_data_array[i]); r_h_data[i]=GETRGBR(this.m_data_array[i]); g_h_data[i]=GETRGBG(this.m_data_array[i]); b_h_data[i]=GETRGBB(this.m_data_array[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(int Y=0;Y<this.Height();Y++) { //--- Loop by the image height for(uint X=radius;X<this.Width()-radius;X++) { XY=Y*this.Width()+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*this.Width()+x; a_h_data[XY]=a_h_data[Y*this.Width()+radius]; r_h_data[XY]=r_h_data[Y*this.Width()+radius]; g_h_data[XY]=g_h_data[Y*this.Width()+radius]; b_h_data[XY]=b_h_data[Y*this.Width()+radius]; } //--- Remove blur artifacts to the right by copying adjacent pixels for(int x=int(this.Width()-radius);x<this.Width();x++) { XY=Y*this.Width()+x; a_h_data[XY]=a_h_data[(Y+1)*this.Width()-radius-1]; r_h_data[XY]=r_h_data[(Y+1)*this.Width()-radius-1]; g_h_data[XY]=g_h_data[(Y+1)*this.Width()-radius-1]; b_h_data[XY]=b_h_data[(Y+1)*this.Width()-radius-1]; } } //--- Blur vertically (along the Y axis) the image already blurred horizontally int dxdy=0; //--- Loop by the image height for(int X=0;X<this.Width();X++) { //--- Loop by the image width for(uint Y=radius;Y<this.Height()-radius;Y++) { XY=Y*this.Width()+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)this.Width(); 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*this.Width()+X; a_v_data[XY]=a_v_data[X+radius*this.Width()]; r_v_data[XY]=r_v_data[X+radius*this.Width()]; g_v_data[XY]=g_v_data[X+radius*this.Width()]; b_v_data[XY]=b_v_data[X+radius*this.Width()]; } //--- Remove blur artifacts at the bottom by copying adjacent pixels for(int y=int(this.Height()-radius);y<this.Height();y++) { XY=y*this.Width()+X; a_v_data[XY]=a_v_data[X+(this.Height()-1-radius)*this.Width()]; r_v_data[XY]=r_v_data[X+(this.Height()-1-radius)*this.Width()]; g_v_data[XY]=g_v_data[X+(this.Height()-1-radius)*this.Width()]; b_v_data[XY]=b_v_data[X+(this.Height()-1-radius)*this.Width()]; } } //--- Set the twice blurred (horizontally and vertically) image pixels to the graphical resource data array for(int i=0;i<size;i++) this.m_data_array[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(int X=0;X<this.Width();X++) { for(uint Y=radius;Y<this.Height()-radius;Y++) { XY=Y*this.Width()+X; this.m_canvas.PixelSet(X,Y,this.m_data_array[XY]); } } //--- Done return true; } //+------------------------------------------------------------------+
Jetzt ist alles bereit für die Entwicklung der Klasse. Ihr Objekt wird es uns ermöglichen, das Zeichnen beliebiger grafischer Elemente auf der Leinwand zu verwalten, so dass wir später den Hintergrund des Bildes, dem die neue Zeichnung überlagert wurde, leicht wiederherstellen können. Außerdem kann ich damit die Klasse für die Arbeit mit Sprite-Animationen erstellen.
Klasse zum Kopieren und Einfügen von Bildteilen
Die Klasse des Formularobjekts soll das minimale Objekt in der Vererbungshierarchie sein, in dem wir mit Animationen arbeiten können sollen.
Da die Klasse zum Speichern und Wiederherstellen eines Bildausschnittes klein sein soll, werden wir sie direkt in die Formularobjektklassendatei \MQL5\Include\DoEasy\Objects\Graph\Form.mqh einfügen. Ich werde die Klasse 'Pixelkopierer' nennen, was ihren Zweck klar beschreibt.
Jedes Objekt der Klasse Pixelkopierer soll eine eigene ID haben, die es uns ermöglicht, ein Bild zu definieren, mit dem das Objekt arbeitet. Es wird möglich sein, auf das benötigte Klassenobjekt durch seine ID zu verweisen, so dass jedes animierte Objekt separat behandelt werden kann. Wenn wir zum Beispiel drei Bilder gleichzeitig verwalten und ändern müssen, von denen zwei Texte und eines ein Bild sind, dann müssen wir beim Erstellen eines Kopierobjekts für jedes Bild einfach unterschiedliche IDs zuweisen — text1 = ID0, text2 = ID1, image = ID2. In diesem Fall speichert jedes der Objekte alle übrigen Parameter für die Arbeit mit ihm, nämlich:
- das Array von Pixeln, das den Teil des Hintergrunds speichert, auf dem ein Bild überlagert wird,
- die X- und Y-Koordinaten der oberen linken Ecke des rechteckigen Bereichs des Hintergrunds, auf dem das Bild überlagert wird,
- die Breite und Höhe des rechteckigen Bereichs
- und die berechnete Breite und Höhe des Bereichs.
Die berechnete Breite und Höhe benötigen wir, um genau zu wissen, wie breit und hoch der rechteckige Kopierbereich ist, falls das Rechteck über den Bereich des Formulars hinausgeht, dessen Pixel gespeichert werden sollen. Wenn wir später den Hintergrund wiederherstellen, brauchen wir die Breite und Höhe des tatsächlich kopierten rechteckigen Hintergrundbereichs nicht mehr neu zu berechnen, sondern verwenden einfach die bereits berechneten Werte, die in den Objektvariablen gespeichert sind.
Im privaten Teil der Klasse deklarieren wir den Zeiger auf die Objektklasse des grafischen Elements (ich werde ihn an das neu erstellte Objekt der Pixelkopiererklasse übergeben, um die Daten des Formulars nutzen zu können, in dem wir die Instanz des Kopierobjekts erstellen), das Array zum Speichern des Teils des Formularbildes, der gespeichert und wiederhergestellt werden soll, und alle oben beschriebenen Variablen:
//+------------------------------------------------------------------+ //| 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" //+------------------------------------------------------------------+ //| Pixel copier class | //+------------------------------------------------------------------+ class CPixelCopier : public CObject { private: CGCnvElement *m_element; // Pointer to the graphical element uint m_array[]; // Pixel array int m_id; // ID int m_x; // X coordinate of the upper left corner int m_y; // Y coordinate of the upper left corner int m_w; // Copied image width int m_h; // Copied image height int m_wr; // Calculated copied image width int m_hr; // Calculated copied image height public:
In den öffentlichen Teil der Klasse schreiben wir die Methode zum Vergleichen zweier Kopierobjekte, die Methoden zum Setzen und Empfangen von Objekteigenschaften, Klassenkonstruktoren — Standard und parametrische, und deklarieren zwei Methoden zum Speichern und Wiederherstellen des Hintergrundteils:
public: //--- Compare CPixelCopier objects by a specified property (to sort the list by an object property) virtual int Compare(const CObject *node,const int mode=0) const { const CPixelCopier *obj_compared=node; return(mode==0 ? (this.ID()>obj_compared.ID() ? 1 : this.ID()<obj_compared.ID() ? -1 : 0) : WRONG_VALUE); } //--- Set the properties void SetID(const int id) { this.m_id=id; } void SetCoordX(const int value) { this.m_x=value; } void SetCoordY(const int value) { this.m_y=value; } void SetWidth(const int value) { this.m_w=value; } void SetHeight(const int value) { this.m_h=value; } //--- Get the properties int ID(void) const { return this.m_id; } int CoordX(void) const { return this.m_x; } int CoordY(void) const { return this.m_y; } int Width(void) const { return this.m_w; } int Height(void) const { return this.m_h; } int WidthReal(void) const { return this.m_wr; } int HeightReal(void) const { return this.m_hr; } //--- Copy the part or the entire image to the array bool CopyImgDataToArray(const uint x_coord,const uint y_coord,uint width,uint height); //--- Copy the part or the entire image from the array to the canvas bool CopyImgDataToCanvas(const int x_coord,const int y_coord); //--- Constructors CPixelCopier (void){;} CPixelCopier (const int id, const int x, const int y, const int w, const int h, CGCnvElement *element) : m_id(id), m_x(x),m_y(y),m_w(w),m_wr(w),m_h(h),m_hr(h) { this.m_element=element; } ~CPixelCopier (void){;} }; //+------------------------------------------------------------------+
Betrachten wir die Methoden im Detail.
Die Methode vergleicht zwei Kopierobjekte:
//--- Compare CPixelCopier objects by a specified property (to sort the list by an object property) virtual int Compare(const CObject *node,const int mode=0) const { const CPixelCopier *obj_compared=node; return(mode==0 ? (this.ID()>obj_compared.ID() ? 1 : this.ID()<obj_compared.ID() ? -1 : 0) : WRONG_VALUE); }
Hier ist alles Standard, wie in anderen Bibliotheksklassen. Wenn der Vergleichsmodus (mode) gleich 0 ist (standardmäßig), werden die IDs des aktuellen Objekts und des Objekts, dessen Zeiger an die Methode übergeben wird, verglichen. Wenn die ID des aktuellen Objekts größer ist, wird 1 zurückgegeben, wenn kleiner -1, wenn gleich - 0. In allen anderen Fällen (wenn mode != 0), wird -1 zurückgegeben. Derzeit kann die Methode nur Objekt-IDs vergleichen.
In der Initialisierungsliste des parametrischen Klassenkonstruktors werden die in den Argumenten übergebenen Werte allen Mitgliedsvariablen der Klasse zugewiesen, während im Klassenkörper der Zeigerwert der Variablen zugewiesen wird, die auf die Objektklasse des grafischen Elements zeigt. Der Wert des Zeigers wird auch in den Argumenten übergeben:
CPixelCopier (const int id, const int x, const int y, const int w, const int h, CGCnvElement *element) : m_id(id), m_x(x),m_y(y),m_w(w),m_wr(w),m_h(h),m_hr(h) { this.m_element=element; }
Jetzt "weiß" das neu erstellte Kopierobjekt, von welchem Objekt es erstellt wurde, und hat Zugriff auf dessen Methoden und Parameter.
Die Methode Kopieren eines Teils oder des gesamten Bildes in das Array:
//+------------------------------------------------------------------+ //| Copy part or all of the image to the array | //+------------------------------------------------------------------+ bool CPixelCopier::CopyImgDataToArray(const uint x_coord,const uint y_coord,uint width,uint height) { //--- Assign coordinate values, passed to the method, to the variables int x1=(int)x_coord; int y1=(int)y_coord; //--- If X coordinates goes beyond the form on the right or Y coordinate goes beyond the form at the bottom, //--- there is nothing to copy, the copied area is outside the form. Return 'false' if(x1>this.m_element.Width()-1 || y1>this.m_element.Height()-1) return false; //--- Assign the width and height values of the copied area to the variables //--- If the passed width and height are equal to zero, assign the form width and height to them this.m_wr=int(width==0 ? this.m_element.Width() : width); this.m_hr=int(height==0 ? this.m_element.Height() : height); //--- If X and Y coordinates are equal to zero (the upper left corner of the form), as well as the width and height are equal to the form width and height, //--- the copied area is equal to the entire form area. Copy the entire form (returning it from the method) using the ImageCopy() method if(x1==0 && y1==0 && this.m_wr==this.m_element.Width() && this.m_hr==this.m_element.Height()) return this.m_element.ImageCopy(DFUN,this.m_array); //--- Calculate the right X coordinate and lower Y coordinate of the rectangle area int x2=int(x1+this.m_wr-1); int y2=int(y1+this.m_hr-1); //--- If the calculated X coordinate goes beyond the form, the right edge of the form will be used as the coordinate if(x2>=this.m_element.Width()-1) x2=this.m_element.Width()-1; //--- If the calculated Y coordinate goes beyond the form, the bottom edge of the form will be used as the coordinate if(y2>=this.m_element.Height()-1) y2=this.m_element.Height()-1; //--- Calculate the copied width and height this.m_wr=x2-x1+1; this.m_hr=y2-y1+1; //--- Define the necessary size of the array, which is to store all image pixels with calculated width and height int size=this.m_wr*this.m_hr; //--- If failed to set the array size, inform of that and return 'false' if(::ArrayResize(this.m_array,size)!=size) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE,true); return false; } //--- Set the index in the array for recording the image pixel int n=0; //--- In a loop by the calculated height of the copied area, starting from the specified Y coordinate for(int y=y1;y<y1+this.m_hr;y++) { //--- in a loop by the calculated width of the copied area, starting from the specified X coordinate for(int x=x1;x<x1+this.m_wr;x++) { //--- Copy the next image pixel to the array and increase the array index this.m_array[n]=this.m_element.GetCanvasObj().PixelGet(x,y); n++; } } //--- Successful - return 'true' return true; } //+------------------------------------------------------------------+
Jede Zeile der Methode ist im Code ausführlich beschrieben. Kurz gesagt, wenn die Anfangskoordinaten des kopierten Bereichs außerhalb des Formulars liegen, gibt es nichts zu kopieren — es wird false zurückgegeben. Wenn die Anfangskoordinaten des kopierten Bereichs mit den Formularkoordinaten übereinstimmen, während die Breite und Höhe des kopierten Bereichs entweder gleich Null sind oder mit der Breite und Höhe des Formulars übereinstimmen, wird das Formularbild vollständig kopiert. Soll nur ein Teil des Bildes gespeichert werden, so sind zunächst die kopierte Breite und Höhe so zu berechnen, dass sie nicht über das Formular hinausgehen, und alle in den kopierten Bereich fallenden Bildpunkte des Formulars zu kopieren.
Die Methode kopiert den Teil oder das gesamte Bild aus dem Array auf die Leinwand:
//+------------------------------------------------------------------+ //| Copy the part or the entire image from the array to the canvas | //+------------------------------------------------------------------+ bool CPixelCopier::CopyImgDataToCanvas(const int x_coord,const int y_coord) { //--- If the array of saved pixels is empty, inform of that and return 'false' int size=::ArraySize(this.m_array); if(size==0) { CMessage::ToLog(DFUN,MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY,true); return false; } //--- Set the index of the array for reading the image pixel int n=0; //--- In a loop by the previously calculated height of the copied area, starting from the specified Y coordinate for(int y=y_coord;y<y_coord+this.m_hr;y++) { //--- in a loop by the previously calculated width of the copied area, starting from the specified X coordinate for(int x=x_coord;x<x_coord+this.m_wr;x++) { //--- Restore the next image pixel from the array and increase the array index this.m_element.GetCanvasObj().PixelSet(x,y,this.m_array[n]); n++; } } return true; } //+------------------------------------------------------------------+
Die Logik der Methode ist ebenfalls in den Code-Kommentaren ausführlich beschrieben. Im Gegensatz zur Methode, die einen Teil des Bildes speichert, brauchen wir hier die Koordinaten und die Größe des kopierten Bereichs nicht mehr zu berechnen, da sie alle nach der ersten Methodenoperation in den Klassenvariablen gespeichert sind. Hier müssen wir nur jede Zeile des wiederhergestellten Bereichs Pixel für Pixel in einer Schleife nach Höhe auf die Leinwand kopieren und so einen Teil des Bildes wiederherstellen, das mit der vorherigen Methode gespeichert wurde.
Nun müssen wir den Zugriff auf eine neu geschriebene Klasse aus der Klasse der Formularobjekte organisieren.
Da ich die erforderliche Anzahl von Kopierobjekten dynamisch erstellen werde, ist es notwendig, die Liste solcher Objekte in der Formularobjektklasse zu deklarieren. Jedes neu erstellte Kopierobjekt wird zu der Liste hinzugefügt, aus der wir die Zeiger auf die benötigten Objekte erhalten und mit ihnen arbeiten können.
Deklarieren wir die folgende Liste im privaten Teil der Klasse:
//+------------------------------------------------------------------+ //| Form object class | //+------------------------------------------------------------------+ class CForm : public CGCnvElement { private: CArrayObj m_list_elements; // List of attached elements CArrayObj m_list_pc_obj; // List of pixel copier objects CShadowObj *m_shadow_obj; // Pointer to the shadow object color m_color_frame; // Form frame color int m_frame_width_left; // Form frame width to the left int m_frame_width_right; // Form frame width to the right int m_frame_width_top; // Form frame width at the top int m_frame_width_bottom; // Form frame width at the bottom
Da wir nicht mehrere Kopierobjekte mit gleichen IDs haben können, brauchen wir die Methode, die das Flag der Objekt-Präsenz in der Liste mit der angegebenen ID zurückgibt. Lassen Sie uns die Methode deklarieren:
//--- Create a shadow object void CreateShadowObj(const color colour,const uchar opacity); //--- Return the flag indicating the presence of the copier object with the specified ID in the list bool IsPresentPC(const int id); public:
In den öffentlichen Teil der Klasse schreiben wir die Methode, die den Zeiger auf das aktuelle Formularobjekt zurückgibt und die Methode, die die Liste der Kopierobjekte zurückgibt:
public: //--- Constructors CForm(const long chart_id, const int subwindow, const string name, const int x, const int y, const int w, const int h); CForm(const int subwindow, const string name, const int x, const int y, const int w, const int h); CForm(const string name, const int x, const int y, const int w, const int h); CForm() { this.Initialize(); } //--- Destructor ~CForm(); //--- Supported form 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; } //--- Return (1) itself, the list of (2) attached objects, (3) pixel copier objects and (4) the shadow object CForm *GetObject(void) { return &this; } CArrayObj *GetList(void) { return &this.m_list_elements; } CArrayObj *GetListPC(void) { return &this.m_list_pc_obj; } CGCnvElement *GetShadowObj(void) { return this.m_shadow_obj; }
Als Nächstes deklarieren wir die Methode, die ein neues Bildpixel-Kopierobjekt erstellt:
//--- Create a new pixel copier object CPixelCopier *CreateNewPixelCopier(const int id,const int x_coord,const int y_coord,const int width,const int height); //--- 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);
Wir fügen den Codeblock für die Arbeit mit Bildpixeln vor dem Codeblock mit den Methoden für einen vereinfachten Zugriff auf Objekteigenschaften ein:
//+------------------------------------------------------------------+ //| Methods of working with image pixels | //+------------------------------------------------------------------+ //--- Return the pixel copier object by ID CPixelCopier *GetPixelCopier(const int id); //--- Copy the part or the entire image to the array bool ImageCopy(const int id,const uint x_coord,const uint y_coord,uint &width,uint &height); //--- Copy the part or the entire image from the array to the canvas bool ImagePaste(const int id,const uint x_coord,const uint y_coord); //+------------------------------------------------------------------+
Implementieren wir die deklarierten Methoden außerhalb des Klassenkörpers.
Die Methode gibt das Flag zurück, das das Vorhandensein des Kopierobjekts mit der angegebenen ID in der Liste anzeigt:
//+------------------------------------------------------------------+ //| Return the flag indicating the presence | //| of the copier object with the specified ID in the list | //+------------------------------------------------------------------+ bool CForm::IsPresentPC(const int id) { for(int i=0;i<this.m_list_pc_obj.Total();i++) { CPixelCopier *pc=this.m_list_pc_obj.At(i); if(pc==NULL) continue; if(pc.ID()==id) return true; } return false; } //+------------------------------------------------------------------+
Hier erhalten wir das nächste Objekt in einer einfachen Schleife durch die Liste der Kopierobjekte. Wenn seine ID gleich derjenigen ist, die der Methode übergeben wurde, wird true zurückgegeben. Nach Beendigung der Schleife wird false zurückgegeben.
Die Methode erstellt ein neues Bildpixel-Kopierobjekt:
//+------------------------------------------------------------------+ //| Create a new pixel copier object | //+------------------------------------------------------------------+ CPixelCopier *CForm::CreateNewPixelCopier(const int id,const int x_coord,const int y_coord,const int width,const int height) { //--- If the object with such an ID is already present, inform of that in the journal and return NULL if(this.IsPresentPC(id)) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_PC_OBJ_ALREADY_IN_LIST),(string)id); return NULL; } //--- Create a new copier object with the specified parameters CPixelCopier *pc=new CPixelCopier(id,x_coord,y_coord,width,height,CGCnvElement::GetObject()); //--- If failed to create an object, inform of that and return NULL if(pc==NULL) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_FAILED_CREATE_PC_OBJ)); return NULL; } //--- If failed to add the created object to the list, inform of that, remove the object and return NULL if(!this.m_list_pc_obj.Add(pc)) { ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST)," ID: ",id); delete pc; return NULL; } //--- Return the pointer to a newly created object return pc; } //+------------------------------------------------------------------+
Die gesamte Methodenlogik ist in den Kommentaren zum Code beschrieben. Wenn Sie Fragen haben, können Sie diese gerne im Kommentarteil unten stellen.
Die Methode gibt den Zeiger auf das Pixelkopierobjekt nach ID zurück:
//+------------------------------------------------------------------+ //| Return the pixel copier object by ID | //+------------------------------------------------------------------+ CPixelCopier *CForm::GetPixelCopier(const int id) { for(int i=0;i<this.m_list_pc_obj.Total();i++) { CPixelCopier *pc=m_list_pc_obj.At(i); if(pc==NULL) continue; if(pc.ID()==id) return pc; } return NULL; } //+------------------------------------------------------------------+
Hier ist alles einfach. In einer Schleife durch die Liste der Kopierobjekte wird der Zeiger auf das nächste Objekt ermittelt. Wenn seine ID mit der gewünschten ID übereinstimmt, gib den Zeiger zurück. Nach Beendigung der Schleife wird NULL zurückgegeben — das Objekt mit der angegebenen ID wird in der Liste nicht gefunden.
Die Methode Kopieren eines Teils oder des gesamten Bildes in das Array:
//+------------------------------------------------------------------+ //| Copy part or all of the image to the array | //+------------------------------------------------------------------+ bool CForm::ImageCopy(const int id,const uint x_coord,const uint y_coord,uint &width,uint &height) { CPixelCopier *pc=this.GetPixelCopier(id); if(pc==NULL) { pc=this.CreateNewPixelCopier(id,x_coord,y_coord,width,height); if(pc==NULL) return false; } return pc.CopyImgDataToArray(x_coord,y_coord,width,height); } //+------------------------------------------------------------------+
Hier erhalten wir den Zeiger auf das Kopierobjekt per ID. Wenn das Objekt nicht gefunden wird, informieren wir darüber und geben false zurück. Wenn der Zeiger auf das Objekt erfolgreich empfangen wurde, geben wir das Ergebnis der Methode CopyImgDataToArray() der oben betrachteten Kopierobjektklasse zurück.
Die Methode kopiert den Teil oder das gesamte Bild aus dem Array auf die Leinwand:
//+------------------------------------------------------------------+ //| Copy the part or the entire image from the array to the canvas | //+------------------------------------------------------------------+ bool CForm::ImagePaste(const int id,const uint x_coord,const uint y_coord) { CPixelCopier *pc=this.GetPixelCopier(id); if(pc==NULL) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_PC_OBJ_NOT_EXIST_LIST),(string)id); return false; } return pc.CopyImgDataToCanvas(x_coord,y_coord); } //+------------------------------------------------------------------+
Die Logik der Methode ist identisch mit der oben beschriebenen, mit dem Unterschied, dass wir jetzt den Bereich nicht im Array speichern, sondern ihn aus dem Array wiederherstellen.
Alles ist bereit, um das Bildpixelkopierobjekt in Aktion zu testen.
Test
Stellen wir sicher, dass das Pixelkopierobjekt korrekt funktioniert. Das GIF-Bild am Anfang des Artikels zeigt deutlich, wie jedes nachfolgende Bild, das vor dem Hintergrund des Formobjekts gezeichnet wird, die zuvor gezeichneten Bilder überlagert. Nun müssen wir den Pixelkopierer verwenden, um zunächst den Hintergrund zu speichern, auf dem der Text überlagert werden soll. Vor dem Zeichnen eines neuen Textes (visuelles Verschieben des gezeichneten Textes) wird zunächst der Hintergrund, auf dem der Text gezeichnet wurde, wiederhergestellt (der Text wird überschrieben), ein Teil des Bildes unter Verwendung der neuen Koordinaten gespeichert und der nächste Text dort angezeigt. Dies geschieht für jeden der neun angezeigten Texte, die unterschiedliche Ankerpunkte haben und an den Formularseiten angezeigt werden sollen, die den Textankerpunkten entsprechen. Auf diese Weise können wir die Gültigkeit der Berechnung der gespeicherten Bildteil-Koordinaten-Offsets unter dem Text überprüfen.
Um den Test durchzuführen, verwenden wir den EA aus dem vorigen Artikel und speichern ihn in \MQL5\Experts\TestDoEasy\Part78\ als TestDoEasyPart78.mq5.
Der EA zeigt drei Formen auf dem Chart an. Der Hintergrund mit dem vertikalen Farbverlauf wird im untersten Formular gezeichnet. Hier werde ich noch ein weiteres Formular zeichnen — das vierte, in dem die horizontale Farbverlaufsfüllung implementiert ist. In diesem Formular sollen die getesteten Texte angezeigt werden.
Im Bereich der EA-Globalvariablen wird auf die Notwendigkeit hingewiesen, vier Formularobjekte zu erstellen:
//+------------------------------------------------------------------+ //| TestDoEasyPart78.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 (4) // 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() werden die Koordinaten eines neuen Formulars in Abhängigkeit von den Koordinaten des vorherigen Formulars berechnet. Es besteht keine Notwendigkeit, das gesamte Chart nach der Erstellung jedes nachfolgenden Formulars neu zu zeichnen. Deshalb wird remove an die Formularaktualisierungsmethoden in den angegebenen Strings (vorher habe ich explizit true übergeben) übergeben. Nun wird dieser Wert ganz am Ende — nach der Erstellung des letzten Formulars — übergeben im neuen Codeblock zur Erstellung des vierten Formulars:
//+------------------------------------------------------------------+ //| 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++) { int y=40; if(i>0) { CForm *form_prev=list_forms.At(i-1); if(form_prev==NULL) continue; y=form_prev.BottomEdge()+10; } //--- When creating an object, pass all the required parameters to it CForm *form=new CForm("Form_0"+(string)(i+1),300,y,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(); } //--- If this is the second 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(); } //--- If this is the third 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,-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(); } //--- If this is the fourth (bottom - tested) form if(i==3) { //--- 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,-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 horizontal gradient form.Erase(array_clr,form.Opacity(),false); //--- 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 //--- Specify the text parameters (text coordinates in the center of the form) and the anchor point (located at the center as well) string text=TextByLanguage("H-Градиент","H-Gradient"); int text_x=form.Width()/2; int text_y=form.Height()/2; ENUM_TEXT_ANCHOR anchor=TEXT_ANCHOR_CENTER; //--- Find out the width and height of the outlining text rectangle (to be used as the size of the saved area) int text_w=0,text_h=0; form.TextSize(text,text_w,text_h); //--- Calculate coordinate offsets for the saved area depending on the text anchor point int shift_x=0,shift_y=0; form.TextGetShiftXY(text,anchor,shift_x,shift_y); //--- If a background area with calculated coordinates and size under the future text is successfully saved if(form.ImageCopy(0,text_x+shift_x,text_y+shift_y,text_w,text_h)) { //--- Draw the text and update the form together with redrawing a chart form.Text(text_x,text_y,text,C'211,233,149',255,anchor); form.Update(true); } } //--- Add objects to the list if(!list_forms.Add(form)) { delete form; continue; } } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Jeder neue Code-String für die Formularerstellung wird hier mit ausführlichen Kommentaren versehen. Nach dem Erstellen des Formulars und vor dem Zeichnen eines Textes auf dem Formular müssen wir den Hintergrundbereich, auf dem sich der Text befinden soll, speichern. Später, in einer anderen Funktion, stellen wir zunächst den Formularhintergrund wieder her, indem wir den Text damit überschreiben, und zeigen die Texte an den neuen Stellen an, wobei wir den Hintergrund unter ihnen auf dieselbe Weise beibehalten und ihn bei jeder neuen Bewegung des Textes zu neuen Koordinaten wiederherstellen.
All dies wird im OnChartEvent() im neuen Code-Block durchgeführt:
//+------------------------------------------------------------------+ //| 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) { //--- If the clicked object belongs to the EA if(StringFind(sparam,MQLInfoString(MQL_PROGRAM_NAME))==0) { //--- Get the object ID from it int form_id=(int)StringToInteger(StringSubstr(sparam,StringLen(sparam)-1))-1; //--- Find this form object in the loop by all forms created in the EA for(int i=0;i<list_forms.Total();i++) { CForm *form=list_forms.At(i); if(form==NULL) continue; //--- If the clicked object has the ID of 3 and the form has the same ID if(form_id==3 && form.ID()==3) { //--- Set the text parameters string text=TextByLanguage("H-Градиент","H-Gradient"); //--- Get the size of the future text int text_w=0,text_h=0; form.TextSize(text,text_w,text_h); //--- Get the anchor point of the last drawn text (this is the form which contained the last drawn text) ENUM_TEXT_ANCHOR anchor=form.TextAnchor(); //--- Get the coordinates of the last drawn text int text_x=form.TextLastX(); int text_y=form.TextLastY(); //--- Calculate the coordinate offset of the saved rectangle area depending on the text anchor point int shift_x=0,shift_y=0; form.TextGetShiftXY(text,anchor,shift_x,shift_y); //--- Set the text anchor initial point (0 = LEFT_TOP) out of nine possible ones static int n=0; //--- If the previously copied form background image is successfully restored when creating the form object in OnInit() if(form.ImagePaste(0,text_x+shift_x,text_y+shift_y)) { //--- Depending on the n variable, set the new text anchor point switch(n) { case 0 : anchor=TEXT_ANCHOR_LEFT_TOP; text_x=1; text_y=1; break; case 1 : anchor=TEXT_ANCHOR_CENTER_TOP; text_x=form.Width()/2; text_y=1; break; case 2 : anchor=TEXT_ANCHOR_RIGHT_TOP; text_x=form.Width()-2; text_y=1; break; case 3 : anchor=TEXT_ANCHOR_LEFT_CENTER; text_x=1; text_y=form.Height()/2; break; case 4 : anchor=TEXT_ANCHOR_CENTER; text_x=form.Width()/2; text_y=form.Height()/2; break; case 5 : anchor=TEXT_ANCHOR_RIGHT_CENTER; text_x=form.Width()-2; text_y=form.Height()/2; break; case 6 : anchor=TEXT_ANCHOR_LEFT_BOTTOM; text_x=1; text_y=form.Height()-2; break; case 7 : anchor=TEXT_ANCHOR_CENTER_BOTTOM;text_x=form.Width()/2; text_y=form.Height()-2; break; case 8 : anchor=TEXT_ANCHOR_RIGHT_BOTTOM; text_x=form.Width()-2; text_y=form.Height()-2; break; default: anchor=TEXT_ANCHOR_CENTER; text_x=form.Width()/2; text_y=form.Height()/2; break; } //--- According to the new anchor point, get the new offsets of the saved area coordinates form.TextGetShiftXY(text,anchor,shift_x,shift_y); //--- If the background area is successfully saved at new coordinates if(form.ImageCopy(0,text_x+shift_x,text_y+shift_y,text_w,text_h)) { //--- Draw the text in new coordinates and update the form form.Text(text_x,text_y,text,C'211,233,149',255,anchor); form.Update(); } //--- Increase the object click counter (and also the pointer to the text anchor point), //--- and if the value exceeds 8, reset the value to zero (from 0 to 8 = nine anchor points) n++; if(n>8) n=0; } } } } } } //+------------------------------------------------------------------+
Ausführliche Beschreibungen finden Sie in den Kommentaren zum Code. Wenn Sie Fragen haben, können Sie diese gerne im Kommentarteil unten stellen.
Kompilieren Sie den EA und starten Sie ihn auf dem Chart.
Klicken Sie auf das untere Formular und vergewissern Sie sich, dass alles wie vorgesehen funktioniert:
Was kommt als Nächstes?
Im nächsten Artikel werde ich die Entwicklung des Animationskonzepts in der Bibliothek fortsetzen und mit der Arbeit an der Sprite-Animation beginnen.
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
Grafik in der Bibliothek DoEasy (Teil 77): Objektklasse der Schatten
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/9612
![Besser Programmieren (Teil 02): Hören Sie auf, diese 5 Dinge zu tun, um ein erfolgreicher MQL5-Programmierer zu werden](https://c.mql5.com/2/43/Article_image.png)
![Besser Programmieren (Teil 01): Diese 5 Dinge müssen Sie unterlassen, um ein erfolgreicher MQL5-Programmierer zu werden](https://c.mql5.com/2/42/Article_image.png)
![Muster mit Beispielel (Taiul I): Multiple-Tops](https://c.mql5.com/2/42/gdij.png)
![Grafik in der Bibliothek DoEasy (Teil 77): Objektklasse der Schatten](https://c.mql5.com/2/42/MQL5-avatar-doeasy-library3-2__4.png)
![MQL5 - Sprache von Handelsstrategien, eingebaut ins Kundenterminal MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- 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.