English Русский 中文 Español 日本語 Português
Grafik in der Bibliothek DoEasy (Teil 78): Animationsprinzipien in der Bibliothek. Schneiden von Bildern

Grafik in der Bibliothek DoEasy (Teil 78): Animationsprinzipien in der Bibliothek. Schneiden von Bildern

MetaTrader 5Beispiele | 9 August 2021, 08:07
310 0
Artyom Trishkin
Artyom Trishkin

Inhalt


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:

  1. Das Speichern eines Hintergrunds mit den erforderlichen Koordinaten.
  2. Die Anzeige eines Bildes unter Verwendung der Koordinaten.
  3. 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.

Zurück zum Inhalt

*Frühere Artikel dieser Serie:

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

Beigefügte Dateien |
MQL5.zip (4044.2 KB)
Besser Programmieren (Teil 02): Hören Sie auf, diese 5 Dinge zu tun, um ein erfolgreicher MQL5-Programmierer zu werden Besser Programmieren (Teil 02): Hören Sie auf, diese 5 Dinge zu tun, um ein erfolgreicher MQL5-Programmierer zu werden
Dieser Artikel ist ein Muss für alle, die ihre Programmierkarriere verbessern wollen. Diese Artikelserie zielt darauf ab, Sie zum besten Programmierer zu machen, der Sie sein können, unabhängig davon, wie erfahren Sie sind. Die besprochenen Ideen eignen sich sowohl für MQL5-Programmierneulinge als auch für Profis.
Besser Programmieren (Teil 01): Diese 5 Dinge müssen Sie unterlassen, um ein erfolgreicher MQL5-Programmierer zu werden Besser Programmieren (Teil 01): Diese 5 Dinge müssen Sie unterlassen, um ein erfolgreicher MQL5-Programmierer zu werden
Es gibt eine Menge schlechter Angewohnheiten, die Neulinge und sogar fortgeschrittene Programmierer tun, die sie davon abhalten, das Beste aus ihrer Programmierkarriere zu machen. Wir werden sie in diesem Artikel diskutieren und ansprechen. Dieser Artikel ist ein Muss für jeden, der ein erfolgreicher Entwickler in MQL5 werden will. Dieser Artikel ist ein Muss für jeden, der ein erfolgreicher Entwickler in MQL5 werden will.
Muster mit Beispielel (Taiul I): Multiple-Tops Muster mit Beispielel (Taiul I): Multiple-Tops
Dies ist der erste Artikel einer Serie, die sich mit Umkehrmustern im Rahmen des algorithmischen Handels beschäftigt. Wir werden mit der interessantesten Musterfamilie beginnen, die aus den Mustern Doppel-Top (Hochs) und Doppel-Bottom (Tiefs) hervorgegangen ist.
Grafik in der Bibliothek DoEasy (Teil 77): Objektklasse der Schatten Grafik in der Bibliothek DoEasy (Teil 77): Objektklasse der Schatten
In diesem Artikel werde ich eine separate Klasse für das Schattenobjekt erstellen, das ein Nachkomme des grafischen Elementobjekts ist, und die Möglichkeit hinzufügen, den Objekthintergrund mit einem Farbverlauf zu füllen.