DoEasy. Steuerung (Teil 4): Paneel-Steuerung, Parameter für Padding und Dock

Artyom Trishkin | 27 Juni, 2022

Inhalt


Konzept

Ich setze meine Arbeit an der Funktionsweise der Steuerelemente des Paneels (engl. panel) von WinForms fort. Im aktuellen Artikel werde ich die Eigenschaften von Padding- und Dock behandeln.

Das Objekt des „Panel WinForm“ ist im Wesentlichen ein normaler Container zum Platzieren anderer WinForm-Objekte darin. Beim Platzieren solcher Objekte können wir selbstständig die erforderlichen Koordinaten zum Platzieren eines Objekts angeben, damit es sich an den angegebenen Koordinaten befindet. Wir können aber auch angeben, wie das im Container platzierte Objekt gebunden werden soll, nachdem es im Container erstellt wurde. Es gibt sechs Methoden zum Binden eines Objekts in seinem Container (Dock-Eigenschaft des Objekts):

  1. Verankerung am oberen Rand und Ausdehnen auf Containerbreite,
  2. Verankerung am unteren Rand und Ausdehnen auf Containerbreite,
  3. Verankerung am linken Rand und Ausdehnen auf Containerhöhe,
  4. Anbringen an der rechten Bordüre und Ausdehnen auf Containerhöhe,
  5. Ausdehnen über die gesamte Containerbreite und -höhe (Befüllung),
  6. Das Objekt wird an die angegebenen Koordinaten angehängt und seine Größe ändert sich nicht.

Wenn wir eine der Bindungsmethoden auswählen, bei der ein Objekt an einem oder allen Containerrändern haftet, werden seine Kanten unter Berücksichtigung des für den Container festgelegten Padding-Werts an den Containerrand gebunden — der Rand des platzierten Objekts ist nicht an den Container gebunden Rand, ist aber um einen Abstand vom Container entfernt, der im Container-Padding-Wert angegeben ist.

Die Art und Weise, wie sich ein Objekt innerhalb des Containers befindet, wird in seinem Dock-Wert angegeben. Wenn der Container mehr als ein Objekt enthält, "klebt" jedes nachfolgende Objekt nicht am Containerrand im Padding-Abstand, sondern an dem vorherigen Objekt, das von derselben Seite des Containers angezogen wird.

Das Bild zeigt, wie Objekte in MS Visual Studio zum oberen Rand ihres Containers gezogen werden, wobei der Wert von Padding auf 20 gesetzt ist, wenn die Anziehung zum oberen Rand des Containers für sie eingestellt ist:


Im aktuellen Artikel werde ich alle möglichen Optionen zum Auffinden eines Objekts in seinem Ein-Objekt-Container implementieren.
Um den Padding-Wert des Containers zu berücksichtigen, füge ich dem Paneel-Objekt selbst ein weiteres Objekt auf der Leinwand hinzu, das als Unterlage für die Platzierung aller erforderlichen Objekte im Paneel dient. anstatt die Koordinaten des Objekts zu verschieben, das sich innerhalb des Paneel befindet.

Ich denke, dies ist bequemer zum Anordnen von Objekten unter Berücksichtigung des Padding-Werts des Containers. Das Unterlageobjekt selbst wird um den Eigenschaftswert verschoben. Außerdem, wenn wir etwas auf die Leinwand zeichnen müssen, tun wir das auf der Unterlage — es wird bereits um den Padding-Wert verschoben und es besteht keine Notwendigkeit, die Koordinaten und die Größe des Objektbildes zu berechnen. Außerdem gibt es einige andere Bequemlichkeiten, die wir später bei der Entwicklung anderer WinForm-Objekte evaluieren werden.


Verbesserung des Bibliotheksklassen

Wir haben die Möglichkeit, verschiedene Arten von Formularobjekten zu erstellen. Eine separate Bibliotheksdatei ermöglicht es uns, nach und nach weitere Stilparameter einzustellen. Da das Formularobjekt auch mit einer Verlaufsfüllung gefüllt werden kann, stellen wir die neuen Stilparameter für die Verlaufsfüllung des Hintergrunds des Formularobjekts in \MQL5\Include\DoEasy\GraphINI.mqh ein:

//+------------------------------------------------------------------+
//| List of form style parameter indices                             |
//+------------------------------------------------------------------+
enum ENUM_FORM_STYLE_PARAMS
  {
   //--- CForm
   FORM_STYLE_FRAME_SHADOW_OPACITY,             // Shadow opacity
   FORM_STYLE_FRAME_SHADOW_BLUR,                // Shadow blur
   FORM_STYLE_DARKENING_COLOR_FOR_SHADOW,       // Form shadow color darkening
   FORM_STYLE_FRAME_SHADOW_X_SHIFT,             // Shadow X axis shift
   FORM_STYLE_FRAME_SHADOW_Y_SHIFT,             // Shadow Y axis shift
   FORM_STYLE_GRADIENT_V,                       // Vertical gradient filling flag
   FORM_STYLE_GRADIENT_C,                       // Cyclic gradient filling flag
   //--- CPanel
   FORM_STYLE_FRAME_WIDTH_LEFT,                 // Panel frame width to the left
   FORM_STYLE_FRAME_WIDTH_RIGHT,                // Panel frame width to the right
   FORM_STYLE_FRAME_WIDTH_TOP,                  // Panel frame width on top
   FORM_STYLE_FRAME_WIDTH_BOTTOM,               // Panel frame width below
  };
#define TOTAL_FORM_STYLE_PARAMS        (11)     // Number of form style parameters
//+------------------------------------------------------------------+
//| Array containing form style parameters                           |
//+------------------------------------------------------------------+
int array_form_style[TOTAL_FORM_STYLES][TOTAL_FORM_STYLE_PARAMS]=
  {
//--- "Flat form" style parameters
   {
      //--- CForm
      80,                                       // Shadow opacity
      4,                                        // Shadow blur
      80,                                       // Form shadow color darkening
      2,                                        // Shadow X axis shift
      2,                                        // Shadow Y axis shift
      false,                                    // Vertical gradient filling flag
      false,                                    // Cyclic gradient filling flag
      //--- CPanel
      3,                                        // Panel frame width to the left
      3,                                        // Panel frame width to the right
      3,                                        // Panel frame width on top
      3,                                        // Panel frame width below
   },
//--- "Embossed form" style parameters
   {
      //--- CForm
      80,                                       // Shadow opacity
      4,                                        // Shadow blur
      80,                                       // Form shadow color darkening
      2,                                        // Shadow X axis shift
      2,                                        // Shadow Y axis shift
      true,                                     // Vertical gradient filling flag
      false,                                    // Cyclic gradient filling flag
      //--- CPanel
      3,                                        // Panel frame width to the left
      3,                                        // Panel frame width to the right
      3,                                        // Panel frame width on top
      3,                                        // Panel frame width below
   },
  };
//+------------------------------------------------------------------+

Wir erhöhen die Anzahl der Stilparameter von 9 auf 11.


Wir verschieben in \MQL5\Include\DoEasy\Defines.mqh, genauer in die Enumeration der Grenzen der Steuerung, die an den Container gebunden sind, zuerst die Konstante, die angibt, dass das Objekt überhaupt nicht an Containergrenzen gebunden ist.

//+------------------------------------------------------------------+
//| Control borders bound to the container                           |
//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_DOCK_MODE
  {
   CANV_ELEMENT_DOCK_MODE_NONE,                       // Attached to the specified coordinates, size does not change
   CANV_ELEMENT_DOCK_MODE_TOP,                        // Attaching to the top and stretching along the container width
   CANV_ELEMENT_DOCK_MODE_BOTTOM,                     // Attaching to the bottom and stretching along the container width
   CANV_ELEMENT_DOCK_MODE_LEFT,                       // Attaching to the left and stretching along the container height
   CANV_ELEMENT_DOCK_MODE_RIGHT,                      // Attaching to the right and stretching along the container height
   CANV_ELEMENT_DOCK_MODE_FILL,                       // Stretching along the entire container width and height
  };
//+------------------------------------------------------------------+

Zuvor belegte die Konstante den letzten Platz in der Liste. Dies ist nicht sehr praktisch, wenn wir sicherstellen möchten, dass das Objekt nicht an die Containergrenzen gebunden ist. Jetzt können wir einfach die Methode überprüfen, die die Art der Bindung eines Objekts an den Container zurückgibt. Der Wert 0, dem die Konstante entspricht, ist im booleschen Sinne dem Wert false.

Fügen wir in E:\MetaQuotes\MetaTrader 5\MQL5\Include\DoEasy\Data.mqh die neuen Nachrichtenindizes hinzu:

   MSG_LIB_SYS_FAILED_COLORS_ARRAY_RESIZE,            // Failed to change the color array size
   MSG_LIB_SYS_FAILED_ARRAY_RESIZE,                   // Failed to change the array size
   MSG_LIB_SYS_FAILED_ARRAY_COPY,                     // Failed to copy the array
   MSG_LIB_SYS_FAILED_ADD_BUFFER,                     // Failed to add buffer object to the list
   MSG_LIB_SYS_FAILED_CREATE_BUFFER_OBJ,              // Failed to create \"Indicator buffer\" object

...

//--- CGCnvElement
   MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY,                  // Error! Empty array
   MSG_CANV_ELEMENT_ERR_ARRAYS_NOT_MATCH,             // Error! Array-copy of the resource does not match the original
   MSG_CANV_ELEMENT_ERR_FAILED_SET_WIDTH,             // Error! Failed to set the canvas width
   MSG_CANV_ELEMENT_ERR_FAILED_SET_HEIGHT,            // Error! Failed to set the canvas height

...

//--- CGStdGraphObjExtToolkit
   MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA,     // Failed to change the size of the pivot point time data array
   MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA,    // Failed to change the size of the pivot point price data array
   MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM,   // Failed to create a form object to manage a pivot point
   
//--- CPanel
   MSG_PANEL_OBJECT_ERR_FAILED_CREATE_UNDERLAY_OBJ,   // Failed to create the underlay object
   
  };
//+------------------------------------------------------------------+

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

   {"Не удалось изменить размер массива цветов","Failed to resize color array"},
   {"Не удалось изменить размер массива ","Failed to resize array "},
   {"Не удалось скопировать массив","Failed to copy array"},
   {"Не удалось добавить объект-буфер в список","Failed to add buffer object to list"},
   {"Не удалось создать объект \"Индикаторный буфер\"","Failed to create object \"Indicator buffer\""},

...

//--- CGCnvElement
   {"Ошибка! Пустой массив","Error! Empty array"},
   {"Ошибка! Массив-копия ресурса не совпадает с оригиналом","Error! Array-copy of the resource does not match the original"},
   {"Ошибка! Не удалось установить ширину канваса","Error! Failed to set canvas width"},
   {"Ошибка! Не удалось установить высоту канваса","Error! Failed to set canvas height"},

...

//--- CGStdGraphObjExtToolkit
   {"Не удалось изменить размер массива данных времени опорной точки","Failed to resize pivot point time data array"},
   {"Не удалось изменить размер массива данных цены опорной точки","Failed to resize pivot point price data array"},
   {"Не удалось создать объект-форму для контроля опорной точки","Failed to create form object to control pivot point"},
   
//--- CPanel
   {"Не удалось создать объект-подложку","Failed to create underlay object"},
   
  };
//+---------------------------------------------------------------------+


Wenn wir die Größe des Formularobjekts und seiner Nachkommen ändern, müssen wir das Objekt komplett neu zeichnen. Um später seine ursprüngliche Größe wiederherzustellen, müssen wir seine ursprünglichen Koordinaten und Größe kennen. Da das Formular außerdem entweder mit einer einzelnen Farbe oder mit einer Farbverlaufsfüllung gefüllt werden kann, ist es besser, ein Array zu haben, das alle Farben der Farbverlaufsfüllung enthält. Wenn eine Farbe vorhanden ist, wird sie nicht im Array festgelegt. Daher haben wir in den Objekteigenschaften immer nur eine Anfangsfarbe, während das Array alle Farben enthält, die für den Verlauf verwendet werden. Daher müssen wir die Methode zum Festlegen einer einzelnen Farbe verbessern und eine weitere Methode zum Festlegen von Verlaufsfarben hinzufügen. Beide Methoden füllen die Werte sowohl für eine Farbe als auch für den Farbverlauf aus.

Außerdem können verschiedene Objekte aneinander befestigt werden. Beispielsweise verfügt ein Paneel-Objekt über zwei angehängte WinForm-Objekte. Das Paneelobjekt selbst ist an noch eine andere Paneel angehängt. Für die beiden an das erste Paneel angehängten Objekte ist es das Basisobjekt, daher wird es in ihren Eigenschaften als das Basisobjekt angegeben, das als Container für sie verwendet wird. Wenn wir diesen Container mit zwei Objekten in einem weiteren Container platzieren, wird das neue Paneel-Objekt zum Basiscontainer für den ersten Container. Damit die beiden im ersten Container platzierten Objekte wissen, welches Objekt ein Basisobjekt in dieser Hierarchie ist, müssen wir noch eine weitere Eigenschaft hinzufügen — das Hauptobjekt der gesamten Hierarchie. In diesem Fall kennen zwei Objekte, die sich im ersten Container befinden, ihren Basiscontainer und das Hauptobjekt — der zweite Container, an den ihr Basiscontainer gebunden ist.

Verbessern wir noch die Objektklasse der grafischen Elemente in \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh.

Wir deklarieren die neuen Variablen in ihrem geschützten Abschnitt:

//+------------------------------------------------------------------+
//| Class of the graphical element object                            |
//+------------------------------------------------------------------+
class CGCnvElement : public CGBaseObj
  {
protected:
   CGCnvElement     *m_element_main;                           // Pointer to the initial parent element within all the groups of bound objects
   CGCnvElement     *m_element_base;                           // Pointer to the parent element within related objects of the current group
   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_duplicate_res[];                        // Array for storing resource data copy
   color             m_array_colors_bg[];                      // Array of element background colors
   bool              m_gradient_v;                             // Vertical gradient filling flag
   bool              m_gradient_c;                             // Cyclic gradient filling flag
   int               m_init_relative_x;                        // Initial relative X coordinate
   int               m_init_relative_y;                        // Initial relative Y coordinate

//--- Create (1) the object structure and (2) the object from the structure
   virtual bool      ObjectToStruct(void);
   virtual void      StructToObject(void);
   
private:


Im privaten Abschnitt der Klasse deklarieren wir die Methode zum Speichern des Arrays von Verlaufsfüllfarben:

//--- Return the index of the array the order's (1) double and (2) string properties are located at
   int               IndexProp(ENUM_CANV_ELEMENT_PROP_DOUBLE property)  const { return(int)property-CANV_ELEMENT_PROP_INTEGER_TOTAL;                                 }
   int               IndexProp(ENUM_CANV_ELEMENT_PROP_STRING property)  const { return(int)property-CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_DOUBLE_TOTAL;  }

//--- Save the colors to the background color array
   void              SaveColorsBG(color &colors[]);
   
public:


Im öffentlichen Abschnitt der Klasse schreiben wir die Methodeneinstellung und geben Sie die Werte neu hinzugefügter Variablen zurück:

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

//--- (1) Set and (2) return the initial shift of the (1) X and (2) Y coordinate relative to the base object
   void              SetCoordXRelativeInit(const int value)                            { this.m_init_relative_x=value;              }
   void              SetCoordYRelativeInit(const int value)                            { this.m_init_relative_y=value;              }
   int               CoordXRelativeInit(void)                                    const { return this.m_init_relative_x;             }
   int               CoordYRelativeInit(void)                                    const { return this.m_init_relative_y;             }
   
//--- (1) Set and (2) return the pointer to the parent element within related objects of the current group
   void              SetBase(CGCnvElement *element)                                    { this.m_element_base=element;               }
   CGCnvElement     *GetBase(void)                                                     { return this.m_element_base;                }
//--- (1) Set and (2) return the pointer to the parent element within all groups of related objects
   void              SetMain(CGCnvElement *element)                                    { this.m_element_main=element;               }
   CGCnvElement     *GetMain(void)                                                     { return this.m_element_main;                }
   
//--- Return the pointer to a canvas object
   CCanvas          *GetCanvasObj(void)                                                { return &this.m_canvas;                     }

Wir verwenden im Standardkonstruktor den Wert NULL , um den Zeiger auf das Hauptobjekt der Hierarchie verwandter Objekte zu initialisieren:

//--- Default constructor/Destructor
                     CGCnvElement() : m_shadow(false),m_chart_color_bg((color)::ChartGetInteger(::ChartID(),CHART_COLOR_BACKGROUND))
                        {
                         this.m_type=OBJECT_DE_TYPE_GELEMENT;
                         this.m_element_main=NULL;
                         this.m_element_base=NULL;
                         this.m_shift_coord_x=0;
                         this.m_shift_coord_y=0;
                        }
                    ~CGCnvElement()
                        { this.m_canvas.Destroy();             }


Verbessern wir die Methode zum Festlegen der Hintergrundfarbe des grafischen Elements:

   void              SetActiveAreaShift(const int left_shift,const int bottom_shift,const int right_shift,const int top_shift);
   void              SetColorBackground(const color colour)
                       {
                        this.m_color_bg=colour;
                        color arr[1];
                        arr[0]=colour;
                        this.SaveColorsBG(arr);
                       }

Jetzt legt es nicht nur die an die Methode übergebene Hintergrundfarbe in den Objekteigenschaften fest, sondern ruft auch die Methode auf, die die einzige Farbe auf das neue Array von Verlaufsfüllfarben festlegt. Um dies zu erreichen, deklarieren wir das Array, legen den Farbwert fest und übergeben das Array an die Methode SaveColorsBG(), die etwas später beschrieben wird.

Lassen Sie uns nun die Methode festlegen, die die Farben der Hintergrundverlaufsfüllung des grafischen Elements festlegt:

   void              SetOpacity(const uchar value,const bool redraw=false);
   void              SetColorsBackground(color &colors[])
                       {
                        this.SaveColorsBG(colors);
                        this.m_color_bg=this.m_array_colors_bg[0];
                       }

Die Methode empfängt das Array von Verlaufsfüllfarben, die wir an die Methode SaveColorsBG() übergeben. Die allererste Farbe aus dem Farbarray wird dann auf den Hintergrundfarbwert gesetzt. Wir setzen also mehrere verschiedene Farben auf einmal: Wenn nur eine Farbe für den Hintergrund des grafischen Elements verwendet wird, ist dies die erste Farbe aus dem Array. Im Fall der Verlaufsfüllung werden alle an die Array-Methode übergebenen Farben in der SaveColorsBG()-Methode auf das Array der Verlaufsfüllfarben gesetzt.

Im Folgenden werde ich zwei weitere Methoden festlegen – die Methode, die die Anzahl der Verlaufsfüllfarben zurückgibt, und die Methode, die die Farbe aus dem Farbarray nach dem angegebenen Index zurückgibt:

//--- Return the number of colors set for the background gradient filling
   uint              ColorsBackgroundTotal(void)         const { return this.m_array_colors_bg.Size();                                 }
//--- Return (1) the background color, (2) the opacity, coordinate (3) of the right and (4) bottom element edge
   color             ColorBackground(void)               const { return this.m_color_bg;                                               }
   color             ColorBackground(const uint index)   const
                       {
                        uint total=this.m_array_colors_bg.Size();
                        if(total==0)
                           return this.m_color_bg;
                        return(index>total-1 ? this.m_array_colors_bg[total-1] : this.m_array_colors_bg[index]);
                       }
   uchar             Opacity(void)                       const { return this.m_opacity;                                                }

Die erste Methode gibt einfach die Größe des Objektfarbarrays zurück.

Bei der zweiten Methode wird, wenn die Größe des Objektfarbenarrays Null ist, der Wert von der m_color_bg- Variablen zurückgegeben. Wenn der Index ungültig ist (die Arraygröße überschreitet), wird die allerletzte Farbe zurückgegeben, andernfalls die Farbe, die sich im Array durch den angegebenen Index befindet.

Im parametrischen Konstruktor initialisieren wir den Zeiger auf das Hauptobjekt der angehängten Objekthierarchie und setzen die Farbe aus der Variablen m_color_bg auf das Farbarray der Hintergrundverlaufsfüllung des Objekts:

//+------------------------------------------------------------------+
//| 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_type=OBJECT_DE_TYPE_GELEMENT; 
   this.m_element_main=NULL;
   this.m_element_base=NULL;
   this.m_chart_color_bg=(color)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_COLOR_BACKGROUND);
   this.m_name=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
   this.m_chart_id=(chart_id==NULL || chart_id==0 ? ::ChartID() : chart_id);
   this.m_subwindow=wnd_num;
   this.m_type_element=element_type;
   this.SetFont(DEF_FONT,DEF_FONT_SIZE);
   this.m_text_anchor=0;
   this.m_text_x=0;
   this.m_text_y=0;
   this.m_color_bg=colour;
   this.m_opacity=opacity;
   this.m_shift_coord_x=0;
   this.m_shift_coord_y=0;
   if(::ArrayResize(this.m_array_colors_bg,1)==1)
      this.m_array_colors_bg[0]=this.m_color_bg;
   if(this.Create(chart_id,wnd_num,this.m_name,x,y,w,h,colour,opacity,redraw))
     {

Setzen Sie dieselben Zeichenfolgen im geschützten Konstruktor (es hat keinen Sinn, dies hier noch einmal zu zeigen).


Entfernen wir die Objektunbeweglichkeitsprüfung aus der Methode zum Aktualisieren der Objektkoordinaten:

//+------------------------------------------------------------------+
//| Update the coordinate elements                                   |
//+------------------------------------------------------------------+
bool CGCnvElement::Move(const int x,const int y,const bool redraw=false)
  {
//--- Leave if the element is not movable or inactive
   if(!this.Movable())
      return false;
//--- If failed to set new values into graphical object properties, return 'false'
   if(!this.SetCoordX(x) || !this.SetCoordY(y))
      return false;
   //--- If the update flag is activated, redraw the chart.
   if(redraw)
      ::ChartRedraw(this.ChartID());
   //--- Return 'true'
   return true;
  }
//+------------------------------------------------------------------+

Die Tatsache, dass sich diese Prüfung innerhalb dieser bestimmten Methode befindet, ist sehr logisch, aber dies führt zu erheblichen Problemen, wenn Objekte mit Bezug zu einer gemeinsamen Hierarchie verschoben werden. Wenn wir ein Elternobjekt bewegen, das verschachtelte Objekte enthält, während einige von ihnen nicht beweglich sind, wird die Kontrolle ihres Flags der Unbeweglichkeits durch das Vorhandensein eines Zeigers auf Eltern- und Hauptobjekte sehr schwierig.

Ich habe mich vorerst dafür entschieden, die Unbeweglichkeit eines Objekts zu verfolgen, wenn es tatsächlich versucht, es mit der Maus zu bewegen, anstatt zahlreiche Überprüfungen durchzuführen und Bewegungsverbote für alle in der Hierarchie verschachtelten Objekte zu ignorieren.

Verbessen wir noch die Methoden, die eine neue Breite und Höhe festlegen:

//+------------------------------------------------------------------+
//| Set a new width                                                  |
//+------------------------------------------------------------------+
bool CGCnvElement::SetWidth(const int width)
  {
   if(this.GetProperty(CANV_ELEMENT_PROP_WIDTH)==width)
      return true;
   if(!this.m_canvas.Resize(width,this.m_canvas.Height()))
     {
      CMessage::ToLog(DFUN,MSG_CANV_ELEMENT_ERR_FAILED_SET_WIDTH);
      return false;
     }
   this.SetProperty(CANV_ELEMENT_PROP_WIDTH,width);
   return true;
  }
//+------------------------------------------------------------------+
//| Set a new height                                                 |
//+------------------------------------------------------------------+
bool CGCnvElement::SetHeight(const int height)
  {
   if(this.GetProperty(CANV_ELEMENT_PROP_HEIGHT)==height)
      return true;
   if(!this.m_canvas.Resize(this.m_canvas.Width(),height))
     {
      CMessage::ToLog(DFUN,MSG_CANV_ELEMENT_ERR_FAILED_SET_HEIGHT);
      return false;
     }
   this.SetProperty(CANV_ELEMENT_PROP_HEIGHT,height);
   return true;
  }
//+------------------------------------------------------------------+

Bisher haben beide Methoden sofort das Ergebnis der Änderung der Leinwandhöhe oder -breite zurückgegeben:

return this.m_canvas.Resize(width,this.m_canvas.Height());

Dies änderte jedoch nicht die in den Objekteigenschaften eingestellten Werte.

Deshalb überprüfen wir zuerst, ob der in der Objekteigenschaft festgelegte Wert gleich dem an die Methode übergebenen Wert ist. Wenn die Werte gleich sind, gibt es nichts zu ändern – wir geben sofort true zurück.
Als Nächstes, wenn die Leinwandgröße nicht geändert werden konnte, informieren wir darüber und geben false zurück.
Bei Erfolg legen wir den neuen Wert in den Objekteigenschaften fest und geben true zurück.

Wenn wir die MethodeErase() der Klasse CCanvas aufrufen, füllen wir damit das Formular mit der angegebenen Farbe und Deckkraft. Wenn sich also die angegebene Farbe von der in der Variablen m_color_bg (oder im Farbarray) festgelegten Farbe unterscheidet, wird das Formular mit dieser Farbe gefüllt. Wenn wir Farben an die Array-Methode übergeben, speichern wir diese Farben im internen Array der beiden Erase()-Methoden im grafischen Elementobjekt:

//+------------------------------------------------------------------+
//| Clear the element filling it with color and opacity              |
//+------------------------------------------------------------------+
void CGCnvElement::Erase(const color colour,const uchar opacity,const bool redraw=false)
  {
   color arr[1];
   arr[0]=colour;
   this.SaveColorsBG(arr);
   this.m_canvas.Erase(::ColorToARGB(colour,opacity));
   this.Update(redraw);
  }
//+------------------------------------------------------------------+

In der Methode zum Füllen des Formulars legen wir mit einer einzigen Farbe dieselbe Farbe für das Farbarray der Objektverlaufsfüllung fest. So können wir die anfängliche Objektfarbe ändern, indem wir das Formular mit einer anderen Farbe als der Originalfarbe bemalen.

In einer anderen Methode speichern wir die an die Methode übergebenen Werte in den Variablen, die den Gradientenfüllungstyp speichern. Dann speichern wir das Farbarray im Objektarray:

//+------------------------------------------------------------------+
//| Clear the element with a gradient fill                           |
//+------------------------------------------------------------------+
void CGCnvElement::Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false)
  {
//--- Set the vertical and cyclic gradient filling flags
   this.m_gradient_v=vgradient;
   this.m_gradient_c=cycle;
//--- Check the size of the color array
   int size=::ArraySize(colors);
//--- ...

//--- ... 

//--- Save the background color array
   this.SaveColorsBG(colors);
//--- If specified, update the canvas
   this.Update(redraw);
  }
//+------------------------------------------------------------------+

Die in der Methode gespeicherten Flags der Verlaufsfüllungstypen ermöglichen es uns, das Objekt mit denselben Verlaufswerten neu zu erstellen, die es hatte, bevor die Form neu gezeichnet und in der Größe geändert wurde.

Die Methode zum Speichern der Farben im Hintergrundfarbenarray:

//+------------------------------------------------------------------+
//| Save the colors to the background color array                    |
//+------------------------------------------------------------------+
void CGCnvElement::SaveColorsBG(color &colors[])
  {
   if(this.m_array_colors_bg.Size()!=colors.Size())
     {
      ::ResetLastError();
      if(::ArrayResize(this.m_array_colors_bg,colors.Size())!=colors.Size())
        {
         CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_COLORS_ARRAY_RESIZE);
         CMessage::ToLog(::GetLastError(),true);
         return;
        }
     }
   ::ArrayCopy(this.m_array_colors_bg,colors);
  }
//+------------------------------------------------------------------+

Wenn hier die Größe des an die Methode übergebenen Farbarrays nicht mit der Größe des Objektfarbarrays übereinstimmt, ändern wir die Größe des Objektfarbarrays zum Füllen des Farbverlaufs und kopieren das an die Methode übergebene Array in das Objektfarbarray.

Da wir jetzt die Methoden haben, die die Verschiebung der Objektkoordinaten relativ zu einem anderen Objekt in Pixeln speichern und zurückgeben, entfernen Sie die Variablen, die die Werte der Schattenverschiebung relativ zu dem Objekt speichern, das den Schatten wirft, und ersetzen wir sie durch diese Methoden in \MQL5 \Include\DoEasy\Objects\Graph\ShadowObj.mqh Schattenobjekt-Klassendatei.

Entfernen wir die Variablen aus dem privaten Abschnitt der Klasse:

//+------------------------------------------------------------------+
//| Shadows object class                                             |
//+------------------------------------------------------------------+
class CShadowObj : public CGCnvElement
  {
private:
   color             m_color_shadow;                  // Shadow color
   uchar             m_opacity_shadow;                // Shadow opacity
   int               m_offset_x;                      // Shadow X axis shift
   int               m_offset_y;                      // Shadow Y axis shift
   
//--- Gaussian blur
   bool              GaussianBlur(const uint radius);

Entfernen wir auch die Methoden, die die Werte entfernter Variablen aus dem öffentlichen Abschnitt der Klasse zurückgeben:

public:
//--- Constructor
                     CShadowObj(const long chart_id,
                                const int subwindow,
                                const string name,
                                const int x,
                                const int y,
                                const int w,
                                const int h);

//--- Supported object properties (1) integer and (2) string ones
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER property) { return true; }
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING property)  { return true; }
   
//--- Return the shadow shift by (1) X and (2) Y
   int               OffsetX(void)                                      const { return this.m_offset_x;        }
   int               OffsetY(void)                                      const { return this.m_offset_y;        }

//--- Draw an object shadow
   void              DrawShadow(const int shift_x,const int shift_y,const uchar blur_value);

Entfernen wir die Initialisierung dieser Variablen im Klassenkonstruktor:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CShadowObj::CShadowObj(const long chart_id,
                       const int subwindow,
                       const string name,
                       const int x,
                       const int y,
                       const int w,
                       const int h) : CGCnvElement(GRAPH_ELEMENT_TYPE_SHADOW_OBJ,chart_id,subwindow,name,x,y,w,h)
  {
   this.m_type=OBJECT_DE_TYPE_GSHADOW; 
   CGCnvElement::SetColorBackground(clrNONE);
   CGCnvElement::SetOpacity(0);
   CGCnvElement::SetActive(false);
   this.m_opacity_shadow=127;
   color gray=CGCnvElement::ChangeColorSaturation(ChartColorBackground(),-100);
   this.m_color_shadow=CGCnvElement::ChangeColorLightness(gray,-50);
   this.m_shadow=false;
   this.m_visible=true;
   this.m_offset_x=0;
   this.m_offset_y=0;
   CGCnvElement::Erase();
  }
//+------------------------------------------------------------------+

Wir ersetzen in der Methode zum Zeichnen des Objektschattens die Zeichenfolgen, die die Werte auf Variablen setzen

   this.m_offset_x=shift_x;
   this.m_offset_y=shift_y;

und die Zeichenkette, in dem die für die Variablen gesetzten Werte verwendet werden

   CGCnvElement::Move(this.CoordX()+this.m_offset_x,this.CoordY()+this.m_offset_y);

mit dem Setzen der Werte durch Aufrufen von Methoden und dem Lesen von Werten der in der übergeordneten Klasse angegebenen Verschiebung, anstatt die Werte der jetzt entfernten Variablen zu lesen:

//+------------------------------------------------------------------+
//| Draw the object shadow                                           |
//+------------------------------------------------------------------+
void CShadowObj::DrawShadow(const int shift_x,const int shift_y,const uchar blur_value)
  {
//--- Set the shadow shift values to the variables by X and Y axes
   this.SetCoordXRelative(shift_x);
   this.SetCoordYRelative(shift_y);
//--- Calculate the height and width of the drawn rectangle
   int w=this.Width()-OUTER_AREA_SIZE*2;
   int h=this.Height()-OUTER_AREA_SIZE*2;
//--- Draw a filled rectangle with calculated dimensions
   this.DrawShadowFigureRect(w,h);
//--- Calculate the blur radius, which cannot exceed a quarter of the OUTER_AREA_SIZE constant
   int radius=(blur_value>OUTER_AREA_SIZE/4 ? OUTER_AREA_SIZE/4 : blur_value);
//--- If failed to blur the shape, exit the method (GaussianBlur() displays the error on the journal)
   if(!this.GaussianBlur(radius))
      return;
//--- Shift the shadow object by X/Y offsets specified in the method arguments and update the canvas
   CGCnvElement::Move(this.CoordX()+this.CoordXRelative(),this.CoordY()+this.CoordYRelative());
   CGCnvElement::Update();
  }
//+------------------------------------------------------------------+

Jetzt sind die Werte der Koordinatenverschiebungen relativ zum Basisobjekt immer für jedes Objekt verfügbar, dessen Elternobjekt eine Objektklasse für grafische Elemente ist. Ich habe diese Variablen hier entfernt, da sie nicht mehr benötigt werden.


Nun soll auch die Formularobjektklasse in \MQL5\Include\DoEasy\Objects\Graph\Form.mqh verbessert werden.

Da die WinForms-Objekte von der Klasse geerbt werden, müssen wir die Liste zum Speichern der Zeiger auf Objekte, die innerhalb des Formulars erstellt wurden, vom privaten Abschnitt in den geschützten verschieben. Deklarieren wir außerdem die Methode zum Aktualisieren der Koordinaten gebundener Objekte:

protected:
   CArrayObj         m_list_tmp;                               // List for storing the pointers
   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
//--- Initialize the variables
   void              Initialize(void);
   void              Deinitialize(void);
//--- Create a shadow object
   void              CreateShadowObj(const color colour,const uchar opacity);
//--- Return the name of the dependent object
   string            CreateNameDependentObject(const string base_name)  const
                       { return ::StringSubstr(this.NameObj(),::StringLen(::MQLInfoString(MQL_PROGRAM_NAME))+1)+"_"+base_name;   }
//--- Update coordinates of bound objects
   virtual bool      MoveDependentObj(const int x,const int y,const bool redraw=false);
   
public:


Beim Erstellen eines neuen an das Formular gebundenen Objekts müssen wir ein Objekt angeben, an das es gebunden ist (sein Hauptobjekt).
Dazu übergeben wir in der Methode zum Erstellen eines neuen Objekts den Zeiger auf das Objekt, aus dem er erstellt wurde:

//--- Create a new attached element
   bool              CreateNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                      CGCnvElement *main,
                                      const int x,
                                      const int y,
                                      const int w,
                                      const int h,
                                      const color colour,
                                      const uchar opacity,
                                      const bool activity);


In der Variableninitialisierungsmethode initialisieren wir die Flags des Fülltyps für den Hintergrundverlauf des Formulars mit den Standardwerten:

//+------------------------------------------------------------------+
//| Initialize the variables                                         |
//+------------------------------------------------------------------+
void CForm::Initialize(void)
  {
   this.m_list_elements.Clear();
   this.m_list_elements.Sort();
   this.m_list_tmp.Clear();
   this.m_list_tmp.Sort();
   this.m_shadow_obj=NULL;
   this.m_shadow=false;
   this.m_frame_width_right=DEF_FRAME_WIDTH_SIZE;
   this.m_frame_width_left=DEF_FRAME_WIDTH_SIZE;
   this.m_frame_width_top=DEF_FRAME_WIDTH_SIZE;
   this.m_frame_width_bottom=DEF_FRAME_WIDTH_SIZE;
   this.m_gradient_v=true;
   this.m_gradient_c=false;
   this.m_mouse_state_flags=0;
   this.m_offset_x=0;
   this.m_offset_y=0;
   CGCnvElement::SetInteraction(false);
   this.m_animations=new CAnimations(CGCnvElement::GetObject());
   this.m_list_tmp.Add(m_animations);
  }
//+------------------------------------------------------------------+

Standardmäßig soll für die Gradientenfüllung der vertikale nichtzyklische Gradient verwendet werden.

Ich habe vorher vergessen, die Verschiebbarkeits-Flag in der Methode anzugeben, die ein neues graphisches Objekt erzeugt. Beheben wir das:

//+------------------------------------------------------------------+
//| Create a new graphical object                                    |
//+------------------------------------------------------------------+
CGCnvElement *CForm::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                      const int obj_num,
                                      const string obj_name,
                                      const int x,
                                      const int y,
                                      const int w,
                                      const int h,
                                      const color colour,
                                      const uchar opacity,
                                      const bool movable,
                                      const bool activity)
  {
   string name=this.CreateNameDependentObject(obj_name);
   CGCnvElement *element=NULL;
   //--- ...


   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name);
   element.SetMovable(movable);
   return element;
  }
//+------------------------------------------------------------------+

Fügen wir in der Methode zum Erstellen eines neuen gebundenen Elements den Zeiger zu seinem übergeordneten Hauptobjekt der Hierarchie gebundener Objekte hinzu und legen die Eigenschaften fest, die ich zuvor übersehen habe:

//+------------------------------------------------------------------+
//| Create a new attached element                                    |
//+------------------------------------------------------------------+
bool CForm::CreateNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                             CGCnvElement *main,
                             const int x,
                             const int y,
                             const int w,
                             const int h,
                             const color colour,
                             const uchar opacity,
                             const bool activity)
  {
//--- If the type of a created graphical element is less than the "element", inform of that and return 'false'
   if(element_type<GRAPH_ELEMENT_TYPE_ELEMENT)
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_NOT_INTENDED),::StringSubstr(::EnumToString(element_type),19));
      return false;
     }
//--- Specify the element index in the list
   int num=this.m_list_elements.Total()+1;
//--- Create a graphical element name
   string ns=(::StringLen((string)num)<2 ? ::IntegerToString(num,2,'0') : (string)num);
   string name="Elm"+ns;
//--- Get the screen coordinates of the object relative to the coordinate system of the base object
   int elm_x=x;
   int elm_y=y;
   this.GetCoords(elm_x,elm_y);
//--- Create a new graphical element
   CGCnvElement *obj=this.CreateNewGObject(element_type,num,name,elm_x,elm_y,w,h,colour,opacity,false,activity);
   if(obj==NULL)
      return false;
//--- and add it to the list of bound graphical elements
   if(!this.AddNewElement(obj,elm_x,elm_y))
     {
      delete obj;
      return false;
     }
//--- Set the minimum properties for a bound graphical element
   obj.SetColorBackground(colour);
   obj.SetOpacity(opacity);
   obj.SetActive(activity);
   obj.SetMain(main);
   obj.SetBase(this.GetObject());
   obj.SetID(this.ID());
   obj.SetCoordXRelative(x);
   obj.SetCoordYRelative(y);
   obj.SetZorder(this.Zorder(),false);
   obj.SetCoordXRelativeInit(x);
   obj.SetCoordYRelativeInit(y);
//--- Draw an added object and return 'true'
   obj.Erase(colour,opacity,true);
   return true;
  }
//+------------------------------------------------------------------+


Da ich den Formularstilen zwei neue Werte hinzugefügt habe, fügen wir diese Werte der Methode hinzu, die den Formularstil angibt, und rufen die Erase()-Methode auf, die das Formular mit dem Farbverlauf füllt. Wenn das Farbarray für die Verlaufsfüllung eine einzelne Farbe enthält, wird das Formular mit dieser einen Farbe ohne Verlauf gezeichnet:

//+------------------------------------------------------------------+
//| Set the form style                                               |
//+------------------------------------------------------------------+
void CForm::SetFormStyle(const ENUM_FORM_STYLE style,
                         const ENUM_COLOR_THEMES theme,
                         const uchar opacity,
                         const bool shadow=false,
                         const bool use_bg_color=true,
                         const bool redraw=false)
  {
//--- Set opacity parameters and the size of the form frame side
   this.m_shadow=shadow;
   this.m_frame_width_top=array_form_style[style][FORM_STYLE_FRAME_WIDTH_TOP];
   this.m_frame_width_bottom=array_form_style[style][FORM_STYLE_FRAME_WIDTH_BOTTOM];
   this.m_frame_width_left=array_form_style[style][FORM_STYLE_FRAME_WIDTH_LEFT];
   this.m_frame_width_right=array_form_style[style][FORM_STYLE_FRAME_WIDTH_RIGHT];
   this.m_gradient_v=array_form_style[style][FORM_STYLE_GRADIENT_V];
   this.m_gradient_c=array_form_style[style][FORM_STYLE_GRADIENT_C];
//--- Create the shadow object
   this.CreateShadowObj(clrNONE,(uchar)array_form_style[style][FORM_STYLE_FRAME_SHADOW_OPACITY]);
   
//--- Set a color scheme
   this.SetColorTheme(theme,opacity);
//--- Calculate a shadow color with color darkening
   color clr=array_color_themes[theme][COLOR_THEME_COLOR_FORM_SHADOW];
   color gray=CGCnvElement::ChangeColorSaturation(ChartColorBackground(),-100);
   color color_shadow=CGCnvElement::ChangeColorLightness((use_bg_color ? gray : clr),-fabs(array_form_style[style][FORM_STYLE_DARKENING_COLOR_FOR_SHADOW]));
   this.SetColorShadow(color_shadow);
   
//--- Draw a rectangular shadow
   int shift_x=array_form_style[style][FORM_STYLE_FRAME_SHADOW_X_SHIFT];
   int shift_y=array_form_style[style][FORM_STYLE_FRAME_SHADOW_Y_SHIFT];
   this.DrawShadow(shift_x,shift_y,color_shadow,this.OpacityShadow(),(uchar)array_form_style[style][FORM_STYLE_FRAME_SHADOW_BLUR]);
   
//--- Fill in the form background with color and opacity
   this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c);
//--- Depending on the selected form style, draw the corresponding form frame and the outer bounding frame
   switch(style)
     {
      case FORM_STYLE_BEVEL   :
        this.DrawFormFrame(this.m_frame_width_top,this.m_frame_width_bottom,this.m_frame_width_left,this.m_frame_width_right,this.ColorFrame(),this.Opacity(),FRAME_STYLE_BEVEL);
        break;
      //---FORM_STYLE_FLAT
      default:
        this.DrawFormFrame(this.m_frame_width_top,this.m_frame_width_bottom,this.m_frame_width_left,this.m_frame_width_right,this.ColorFrame(),this.Opacity(),FRAME_STYLE_FLAT);
        break;
     }
   this.DrawRectangle(0,0,Width()-1,Height()-1,array_color_themes[theme][COLOR_THEME_COLOR_FORM_RECT_OUTER],this.Opacity());
  }
//+------------------------------------------------------------------+


In der Methode zum Aktualisieren der Objektkoordinaten haben wir zuvor alle an das Formular gebundenen Objekte durchlaufen und ihre Koordinaten den aktuellen Objektkoordinaten folgend verschoben. Jetzt haben wir die neue Methode MoveDependentObj() deklariert, in der die Schleife angeordnet werden soll.
Bearbeiten wir daher die Methode zum Aktualisieren der Elementkoordinaten:

//+------------------------------------------------------------------+
//| Update the coordinate elements                                   |
//+------------------------------------------------------------------+
bool CForm::Move(const int x,const int y,const bool redraw=false)
  {
   CGCnvElement *base=this.GetBase();
   CGCnvElement *main=this.GetMain();
   bool res=true;
//--- If the element is not movable and is a base object, leave
   if(!this.Movable() && base==NULL)
      return false;
//--- If the object has a shadow and we failed to set new coordinate values to the properties of the shadow object, return 'false'
   if(this.m_shadow)
     {
      if(this.m_shadow_obj==NULL || !this.m_shadow_obj.Move(x-OUTER_AREA_SIZE+this.m_shadow_obj.CoordXRelative(),y-OUTER_AREA_SIZE+this.m_shadow_obj.CoordYRelative(),false))
         return false;
     }
//--- If failed to set new values into graphical object properties, return 'false'
   if(!this.SetCoordX(x) || !this.SetCoordY(y))
      return false;
//--- Shift all bound objects
   if(!this.MoveDependentObj(x,y,false))
      return false;
   //--- If the update flag is set and this is a base object, redraw the chart.
   if(redraw && main==NULL)
      ::ChartRedraw(this.ChartID());
   //--- Return 'true'
   return true;
  }
//+------------------------------------------------------------------+

Zuerst fügen wir das Abrufen des Zeigers auf das Hauptobjekt der gesamten Hierarchie gebundener Objekte hinzu. Dies ist genau das Objekt, dessen Verschiebung zur Verschiebung aller anderen damit (und untereinander) in Beziehung stehenden Elemente führt.
Beim Verschieben des Schattenobjekts verwenden wir nun die Objektmethoden des grafischen Elements, um die Schattenverschiebung relativ zu dem Objekt zu erhalten, das es wirft.
Statt der Schleife rufen wir nun die unten betrachtete neue Methode MoveDependentObj() auf.
Am Ende stellen wir jetzt sicher, dass dies das Hauptobjekt der gesamten Kette der zugehörigen Objekthierarchieist und nicht das Basisobjekt nur einer der Hierarchieketten.


Die Methode zum Aktualisieren der Koordinaten gebundener Objekte:

//+------------------------------------------------------------------+
//| Update coordinates of bound objects                              |
//+------------------------------------------------------------------+
bool CForm::MoveDependentObj(const int x,const int y,const bool redraw=false)
  {
//--- In the loop by all bound objects,
   for(int i=0;i<this.m_list_elements.Total();i++)
     {
      //--- get the next object and shift it
      CGCnvElement *obj=m_list_elements.At(i);
      if(obj==NULL)
         continue;
      if(!obj.Move(x+obj.CoordXRelative(),y+obj.CoordYRelative(),false))
         return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

Holen wir uns hier in der Schleife aller gebundenen Objekte das nächste Objekt und rufen Sie die Methode Move() des Objekts auf. Dementsprechend werden alle Move()-Methoden auch im Objekt für alle damit in Beziehung stehenden Objekte aufgerufen. Dies bewirkt eine Verschiebung der gesamten Hierarchie von Verbindungen aller Objekte, die an das verschobene Objekt angehängt sind. 

Lassen Sie uns nun die Klasse des Objekts „Panel WinForms“ in \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Panel.mqh verbessern.

Deklarieren wir im privaten Abschnitt der Klasse den Zeiger auf das Unterlagenobjekt, die Variablen zum Speichern der Anfangskoordinaten und des neu erstellten Paneel-Overlays und zum Binden des Elements an den Container:

//+------------------------------------------------------------------+
//| Panel object class of WForms controls                            |
//+------------------------------------------------------------------+
class CPanel : public CForm
  {
private:
   CGCnvElement     *m_underlay;                                     // Underlay for placing elements
   color             m_fore_color;                                   // Default text color for all panel objects
   ENUM_FW_TYPE      m_bold_type;                                    // Font width type
   ENUM_FRAME_STYLE  m_border_style;                                 // Panel frame style
   bool              m_autoscroll;                                   // Auto scrollbar flag
   int               m_autoscroll_margin[2];                         // Array of fields around the control during an auto scroll
   bool              m_autosize;                                     // Flag of the element auto resizing depending on the content
   ENUM_CANV_ELEMENT_AUTO_SIZE_MODE m_autosize_mode;                 // Mode of the element auto resizing depending on the content
   ENUM_CANV_ELEMENT_DOCK_MODE m_dock_mode;                          // Mode of binding element borders to the container
   int               m_margin[4];                                    // Array of gaps of all sides between the fields of the current and adjacent controls
   int               m_padding[4];                                   // Array of gaps of all sides inside controls
   int               m_init_x;                                       // Newly created panel X coordinate
   int               m_init_y;                                       // Newly created panel Y coordinate
   int               m_init_w;                                       // Newly created panel width
   int               m_init_h;                                       // Newly created panel height
//--- Return the font flags
   uint              GetFontFlags(void);
//--- Create a new graphical object
   virtual CGCnvElement *CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                          const int element_num,
                                          const string name,
                                          const int x,
                                          const int y,
                                          const int w,
                                          const int h,
                                          const color colour,
                                          const uchar opacity,
                                          const bool movable,
                                          const bool activity);
//--- Return the initial coordinates of a bound object
   virtual void      GetCoords(int &x,int &y);
//--- Create the underlay object
   bool              CreateUnderlayObj(void);
//--- Bind the element to the container
   bool              SetDockingToContainer(void);
protected:


Legen wir im geschützten Abschnitt der Klasse die Methoden zum Umgang mit den Objektkoordinaten der Unterlage fest:

protected:
//--- Set (1) X, (2) Y coordinate, (3) width, (4) height and (5) all underlay parameters
   bool              SetCoordXUnderlay(const int value)              { return(this.m_underlay!=NULL ? this.m_underlay.SetCoordX(value) : false);   }
   bool              SetCoordYUnderlay(const int value)              { return(this.m_underlay!=NULL ? this.m_underlay.SetCoordY(value) : false);   }
   bool              SetWidthUnderlay(const int value)               { return(this.m_underlay!=NULL ? this.m_underlay.SetWidth(value)  : false);   }
   bool              SetHeightUnderlay(const int value)              { return(this.m_underlay!=NULL ? this.m_underlay.SetHeight(value) : false);   }
   bool              SetUnderlayParams(void);
//--- Return the underlay (1) X, (2) Y coordinate, (3) width and (4) height
   int               GetCoordXUnderlay(void)                   const { return(this.m_underlay!=NULL ?  this.m_underlay.CoordX() : 0);              }
   int               GetCoordYUnderlay(void)                   const { return(this.m_underlay!=NULL ?  this.m_underlay.CoordY() : 0);              }
   int               GetWidthUnderlay(void)                    const { return(this.m_underlay!=NULL ?  this.m_underlay.Width()  : 0);              }
   int               GetHeightUnderlay(void)                   const { return(this.m_underlay!=NULL ?  this.m_underlay.Height() : 0);              }
//--- Return the underlay (1) X and (2) Y coordinate relative to the panel
   int               GetCoordXUnderlayRelative(void)           const { return(this.m_underlay!=NULL ?  this.m_underlay.CoordXRelative() : 0);      }
   int               GetCoordYUnderlayRelative(void)           const { return(this.m_underlay!=NULL ?  this.m_underlay.CoordYRelative() : 0);      }

public:


Im öffentlichen Abschnitt der Klasse schreiben wir die Methode, die den Zeiger auf das Unterlagenobjekt zurückgibt, und deklarieren die virtuelle Methode, die das Paneel-Objekt bewegt :

public:
//--- Return the underlay
   CGCnvElement     *GetUnderlay(void)                               { return this.m_underlay;              }
//--- Update the coordinates (shift the canvas)
   virtual bool      Move(const int x,const int y,const bool redraw=false);

//--- (1) Set and (2) return the default text color of all panel objects
   void              ForeColor(const color clr)                      { this.m_fore_color=clr;               }
   color             ForeColor(void)                           const { return this.m_fore_color;            }


Lassen Sie uns einige öffentliche Methoden verbessern.

Die Methode, die den Modus zum Binden der Elementgrenzen an den Container festlegt:

//--- (1) Set and (2) return the mode of binding element borders to the container
   void              DockMode(const ENUM_CANV_ELEMENT_DOCK_MODE mode)
                       {
                        if(m_dock_mode==mode)
                           return;
                        this.m_dock_mode=mode;
                        this.SetDockingToContainer();
                       }

Bisher hat die Methode einfach den ihr übergebenen Wert auf die entsprechende Klassenvariable gesetzt.

Jetzt werde ich nicht nur den Wert auf die Variable setzen, sondern das Paneel auch sofort mit der unten beschriebenen Methode SetDockingToContainer() an die gewünschten Kanten seines Containers anhängen.

Verbessern wir die Methoden zum Einstellen des Abstands nach links, oben, rechts und unten innerhalb des Steuerelements auf die gleiche Weise:

//--- Set the gap (1) to the left, (2) at the top, (3) to the right, (4) at the bottom and (5) on all sides inside the control
   void              PaddingLeft(const uint value)
                       {
                        this.m_padding[0]=((int)value<this.m_frame_width_left ? this.m_frame_width_left : (int)value);
                        if(this.m_underlay!=NULL)
                          {
                           //--- Set the underlay shift along the X axis
                           this.m_underlay.SetCoordXRelative(this.PaddingLeft());
                           //--- Set the X coordinate and the underlay width
                           this.SetCoordXUnderlay(this.CoordX()+this.PaddingLeft());
                           this.SetWidthUnderlay(this.Width()-this.PaddingLeft()-this.PaddingRight());
                          }
                       }
   void              PaddingTop(const uint value)
                       {
                        this.m_padding[1]=((int)value<this.m_frame_width_top ? this.m_frame_width_top : (int)value);
                        if(this.m_underlay!=NULL)
                          {
                           //--- Set the underlay shift along the Y axis
                           this.m_underlay.SetCoordYRelative(this.PaddingTop());
                           //--- Set the Y coordinate and underlay height
                           this.SetCoordYUnderlay(this.CoordY()+this.PaddingTop());
                           this.SetHeightUnderlay(this.Height()-this.PaddingTop()-this.PaddingBottom());
                          }
                       }
   void              PaddingRight(const uint value)
                       {
                        this.m_padding[2]=((int)value<this.m_frame_width_right ? this.m_frame_width_right : (int)value);
                        if(this.m_underlay!=NULL)
                          {
                           //--- Set the underlay width
                           this.SetWidthUnderlay(this.Width()-this.PaddingLeft()-this.PaddingRight());
                          }
                       }
   void              PaddingBottom(const uint value)
                       {
                        this.m_padding[3]=((int)value<this.m_frame_width_bottom ? this.m_frame_width_bottom : (int)value);
                        if(this.m_underlay!=NULL)
                          {
                           //--- Set the underlay height
                           this.SetHeightUnderlay(this.Height()-this.PaddingTop()-this.PaddingBottom());
                          }
                       }
   void              PaddingAll(const uint value)
                       {
                        this.PaddingLeft(value); this.PaddingTop(value); this.PaddingRight(value); this.PaddingBottom(value);
                       }

Zusätzlich zum Festlegen der an die Methode übergebenen Werte auf die entsprechenden Variablen ändern wir diese Eigenschaften für das Unterlagenobjekt sofort.

Die Methoden zum Einstellen der Breite des Formularrahmens links, oben, rechts und unterhalb des Steuerelements werden auf die gleiche Weise behandelt:

//--- Set the width of the form frame (1) to the left, (2) at the top, (3) to the right, (4) at the bottom and (5) on all sides of the control
   void              FrameWidthLeft(const uint value)
                       {
                        this.m_frame_width_left=(int)value;
                        if(PaddingLeft()<FrameWidthLeft())
                           PaddingLeft(FrameWidthLeft());
                       }
   void              FrameWidthTop(const uint value)
                       {
                        this.m_frame_width_top=(int)value;
                        if(this.PaddingTop()<this.FrameWidthTop())
                           this.PaddingTop(this.FrameWidthTop());
                       }
   void              FrameWidthRight(const uint value)
                       {
                        this.m_frame_width_right=(int)value;
                        if(this.PaddingRight()<this.FrameWidthRight())
                           this.PaddingRight(this.FrameWidthRight());
                       }
   void              FrameWidthBottom(const uint value)
                       {
                        this.m_frame_width_bottom=(int)value;
                        if(this.PaddingBottom()<this.FrameWidthBottom())
                           this.PaddingBottom(this.FrameWidthBottom());
                       }
   void              FrameWidthAll(const uint value)
                       {
                        this.FrameWidthLeft(value); this.FrameWidthTop(value); this.FrameWidthRight(value); this.FrameWidthBottom(value);
                       }

Sobald nun die Rahmenbreite auf einer beliebigen Seite des Paneels geändert wird, werden die entsprechenden Eigenschaften des Unterlagenobjekts so geändert, dass die Unterlage immer entweder in den Paneelrahmen passt (wenn der Padding-Wert der Paneelseite kleiner ist als die Rahmenbreite auf derselben Seite) oder dem Padding-Wert der Paneelseite entspricht.

Entfernen wir zwei redundante Konstruktoren aus der Auflistung — deren Deklaration und Implementierung außerhalb des Klassenkörpers:

//--- Constructors
                     CPanel(const long chart_id,
                           const int subwindow,
                           const string name,
                           const int x,
                           const int y,
                           const int w,
                           const int h);
                     CPanel(const int subwindow,
                           const string name,
                           const int x,
                           const int y,
                           const int w,
                           const int h);
                     CPanel(const string name,
                           const int x,
                           const int y,
                           const int w,
                           const int h);
                     CPanel(const string name) : CForm(::ChartID(),0,name,0,0,0,0)

Diese Konstruktoren haben sich als unnötig herausgestellt.

Fügen wir im parametrischen Konstruktor die Initialisierung der Paneel-Objekteigenschaften hinzu, die zuvor noch nicht durchgeführt wurde, und der neuen Variablen:

                     CPanel(const string name) : CForm(::ChartID(),0,name,0,0,0,0)
                       {
                        CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_PANEL);
                        CGCnvElement::SetProperty(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_PANEL);
                        this.m_type=OBJECT_DE_TYPE_GWF_PANEL; 
                        this.m_fore_color=CLR_DEF_FORE_COLOR;
                        this.m_bold_type=FW_TYPE_NORMAL;
                        this.MarginAll(3);
                        this.PaddingAll(0);
                        this.DockMode(CANV_ELEMENT_DOCK_MODE_NONE);
                        this.BorderStyle(FRAME_STYLE_BEVEL);
                        this.AutoScroll(false);
                        this.AutoScrollMarginAll(0);
                        this.AutoSize(false);
                        this.AutoSizeMode(CANV_ELEMENT_AUTO_SIZE_MODE_GROW);
                        this.Initialize();
                        this.CreateUnderlayObj();
                        this.m_init_x=0;
                        this.m_init_y=0;
                        this.m_init_w=0;
                        this.m_init_h=0;
                       }
//--- Destructor
                    ~CPanel();
  };
//+------------------------------------------------------------------+

Initialisieren wir dieselben Variablen im Konstruktor, indem wir die Chart- und Unterfenster-ID angeben.

Die Methode, die die Anfangskoordinaten des gebundenen Objekts zurückgibt, gibt jetzt die Koordinaten relativ zu den Koordinaten des Unterlageobjekts zurück, anstatt wie zuvor relativ zum Paneel selbst und seiner Rahmenbreite:

//+------------------------------------------------------------------+
//| Return the initial coordinates of a bound object                 |
//+------------------------------------------------------------------+
void CPanel::GetCoords(int &x,int &y)
  {
   x=this.m_underlay.CoordX()+x;
   y=this.m_underlay.CoordY()+y;
  }
//+------------------------------------------------------------------+


Die Methode zum Erstellen des Unterlageobjekts:

//+------------------------------------------------------------------+
//| Create the underlay object                                       |
//+------------------------------------------------------------------+
bool CPanel::CreateUnderlayObj(void)
  {
   this.m_underlay=new CGCnvElement(GRAPH_ELEMENT_TYPE_ELEMENT,this.ID(),this.Number(),this.ChartID(),this.SubWindow(),this.CreateNameDependentObject("Undl"),
                                     this.CoordX()+this.PaddingLeft(),this.CoordY()+this.PaddingTop(),
                                     this.Width()-this.PaddingLeft()-this.PaddingRight(),
                                     this.Height()-this.PaddingTop()-this.PaddingBottom(),
                                     CLR_CANV_NULL,0,false,false);
   if(m_underlay==NULL)
     {
      CMessage::ToLog(DFUN,MSG_PANEL_OBJECT_ERR_FAILED_CREATE_UNDERLAY_OBJ);
      return false;
     }
   if(!this.m_list_tmp.Add(this.m_underlay))
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
      delete this.m_underlay;
      return false;
     }
   this.SetUnderlayParams();
   return true;
  }
//+------------------------------------------------------------------+

Hier erstellen wir ein neues grafisches Elementobjekt mit Koordinaten und Größe, die relativ zu den Padding-Werten aller Paneelseiten berechnet werden, sodass das Unterlageobjekt genau in den Bereich passt, der durch die Padding-Werte aller Paneelseiten begrenzt ist.
Wenn das Objekt nicht erstellt werden konnte, informieren wir darüber und geben false zurück.
Wenn das neu erstellte Objekt nicht zur Paneel-Objektliste hinzugefügt werden konnte, informieren wir darüber, entfernen das neu erstellte Objekt und geben false zurück.
Bei Erfolg verlassen wir die Methode zum Setzen aller Parameter der erstellten Unterlage zurückgeben und geben true zurück.


Die Methode zum Einstellen aller Parameter der Unterlage:

//+------------------------------------------------------------------+
//| Set all underlay parameters                                      |
//+------------------------------------------------------------------+
bool CPanel::SetUnderlayParams(void)
  {
//--- Set the underlay shift values to the variables by X and Y axes
   this.m_underlay.SetCoordXRelative(this.PaddingLeft());
   this.m_underlay.SetCoordYRelative(this.PaddingTop());
//--- Set the underlay coordinates and size
   bool res=true;
   res &=this.SetCoordXUnderlay(this.CoordX()+this.PaddingLeft());
   res &=this.SetCoordYUnderlay(this.CoordY()+this.PaddingTop());
   res &=this.SetWidthUnderlay(this.Width()-this.PaddingLeft()-this.PaddingRight());
   res &=this.SetHeightUnderlay(this.Height()-this.PaddingTop()-this.PaddingBottom());
   return res;
  }
//+------------------------------------------------------------------+

Hier ist alles ganz einfach: Wir legen die Koordinatenverschiebungen der Unterlage relativ zu den Plattenkoordinaten sowie die Objektkoordinaten und -größe der Unterlage fest.


Die Methode zum Aktualisieren der Elementkoordinaten:

//+------------------------------------------------------------------+
//| Update the coordinate elements                                   |
//+------------------------------------------------------------------+
bool CPanel::Move(const int x,const int y,const bool redraw=false)
  {
   if(!this.m_underlay.Move(x+this.GetCoordXUnderlayRelative(),y+this.GetCoordYUnderlayRelative()))
      return false;
//--- Get the pointers to the base and main objects in the bound objects hierarchy, as well as the shadow object
   CGCnvElement *base=this.GetBase();
   CGCnvElement *main=this.GetMain();
   CShadowObj   *shadow=this.GetShadowObj();
//--- If the element is not movable and is a base object, leave
   if(!this.Movable() && main==NULL)
      return false;
//--- If the object has a shadow and we failed to set new coordinate values to the properties of the shadow object, return 'false'
   if(this.m_shadow)
     {
      if(shadow==NULL || !shadow.Move(x-OUTER_AREA_SIZE+shadow.CoordXRelative(),y-OUTER_AREA_SIZE+shadow.CoordYRelative(),false))
         return false;
     }
//--- If failed to set new values into graphical object properties, return 'false'
   if(!this.SetCoordX(x) || !this.SetCoordY(y))
      return false;
//--- Shift all bound objects
   if(!this.MoveDependentObj(x+this.GetCoordXUnderlayRelative(),y+this.GetCoordYUnderlayRelative(),false))
      return false;
   //--- If the update flag is set and this is the hierarchy main object, redraw the chart.
   if(redraw && main==NULL)
      ::ChartRedraw(this.ChartID());
   //--- Return 'true'
   return true;
  }
//+------------------------------------------------------------------+

Die Methodenlogik wurde in den Codekommentaren ausführlich beschrieben. Hier prüfen wir die Objektbeweglichkeit. Wenn es nicht beweglich ist, verlassen wir die Methode. Als Nächstes verschieben wir den Schatten und das Objekt selbst. Rufen Sie danach die Methode zum Verschieben aller gebundenen Objekte ihrer Hierarchie auf. Wir stellen nach Abschluss sicher, dass dies das Hauptobjekt der Hierarchie der gebundenen Objekte ist, und aktualisieren das Chart.


Die Methode, die ein Element an einen Container bindet:

//+------------------------------------------------------------------+
//| Bind the element to the container                                |
//+------------------------------------------------------------------+
bool CPanel::SetDockingToContainer(void)
  {
//--- Get the pointer to the pnael object the object is bound to
   CPanel *base=this.GetBase();
   if(base==NULL)
      return false;
//--- Declare the variables and get the base object coordinates abd size to it
   int x=base.GetCoordXUnderlay();
   int y=base.GetCoordYUnderlay();
   int w=base.GetWidthUnderlay();
   int h=base.GetHeightUnderlay();
//--- Depending on the specified mode of binding to a container, move the object to the necessary base object edges
   switch(this.DockMode())
     {
      //--- Attach to the top and stretch along the container width
      case CANV_ELEMENT_DOCK_MODE_TOP :
        this.SetWidth(w);
        this.SetHeight(this.m_init_h);
        this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c);
        if(this.BorderStyle()!=FRAME_STYLE_NONE)
           this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),this.Opacity(),this.BorderStyle());
        this.Update();
        this.Done();
        x=base.GetCoordXUnderlay();
        y=base.GetCoordYUnderlay();
        this.Move(x,y);
        this.SetCoordXRelative(0);
        this.SetCoordYRelative(0);
        break;
      
      //--- Attach to the bottom and stretch along the container width
      case CANV_ELEMENT_DOCK_MODE_BOTTOM :
        this.SetWidth(w);
        this.SetHeight(this.m_init_h);
        this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c);
        if(this.BorderStyle()!=FRAME_STYLE_NONE)
           this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),this.Opacity(),this.BorderStyle());
        this.Update();
        this.Done();
        x=base.GetCoordXUnderlay();
        y=base.GetCoordYUnderlay()+(base.GetHeightUnderlay()-this.Height());
        this.Move(x,y);
        this.SetCoordXRelative(0);
        this.SetCoordYRelative(base.GetHeightUnderlay()-this.Height());
        break;
      
      //--- Attach to the left and stretch along the container height
      case CANV_ELEMENT_DOCK_MODE_LEFT :
        this.SetHeight(h);
        this.SetWidth(this.m_init_w);
        this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c);
        if(this.BorderStyle()!=FRAME_STYLE_NONE)
           this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),this.Opacity(),this.BorderStyle());
        this.Update();
        this.Done();
        x=base.GetCoordXUnderlay();
        y=base.GetCoordYUnderlay();
        this.Move(x,y);
        this.SetCoordXRelative(0);
        this.SetCoordYRelative(0);
        break;
      
      //--- Attach to the right and stretch along the container height
      case CANV_ELEMENT_DOCK_MODE_RIGHT :
        this.SetHeight(h);
        this.SetWidth(this.m_init_w);
        this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c);
        if(this.BorderStyle()!=FRAME_STYLE_NONE)
           this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),this.Opacity(),this.BorderStyle());
        this.Update();
        this.Done();
        x=base.GetCoordXUnderlay()+(base.GetWidthUnderlay()-this.Width());
        y=base.GetCoordYUnderlay();
        this.Move(x,y);
        this.SetCoordXRelative(base.GetWidthUnderlay()-this.Width());
        this.SetCoordYRelative(0);
        break;
      
      //--- Stretch along the entire container width and height
      case CANV_ELEMENT_DOCK_MODE_FILL :
        this.SetWidth(w);
        this.SetHeight(h);
        this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c);
        if(this.BorderStyle()!=FRAME_STYLE_NONE)
           this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),this.Opacity(),this.BorderStyle());
        this.Update();
        this.Done();
        x=base.GetCoordXUnderlay();
        y=base.GetCoordYUnderlay();
        this.Move(x,y);
        this.SetCoordXRelative(0);
        this.SetCoordYRelative(0);
        break;
      
      //--- Attached to the specified coordinates, size does not change
      default: // CANV_ELEMENT_DOCK_MODE_NONE
        this.SetHeight(this.m_init_h);
        this.SetWidth(this.m_init_w);
        this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c);
        if(this.BorderStyle()!=FRAME_STYLE_NONE)
           this.DrawFormFrame(this.FrameWidthTop(),this.FrameWidthBottom(),this.FrameWidthLeft(),this.FrameWidthRight(),this.ColorFrame(),this.Opacity(),this.BorderStyle());
        this.Update();
        this.Done();
        x=base.GetCoordXUnderlay()+this.CoordXRelativeInit();
        y=base.GetCoordYUnderlay()+this.CoordYRelativeInit();
        this.Move(x,y);
        this.SetCoordXRelative(this.CoordXRelativeInit());
        this.SetCoordYRelative(this.CoordYRelativeInit());
        break;
     }
   ::ChartRedraw(this.ChartID());
   return true;
  }
//+------------------------------------------------------------------+

Die Methodenlogik wird auch in den Codekommentaren beschrieben. Je nachdem, wie ein Objekt an einen Container gebunden ist, berechnen wir die erforderlichen Koordinaten und die Größe und setzen Sie das Objekt auf die neuen Koordinaten. Wenn das Objekt nicht an die Containerseiten gebunden ist, rufen wir seine anfänglichen Koordinaten und seine Größe ab.

Die Methode wird immer aufgerufen, wenn ein neuer Wert für die DockMode-Eigenschaft des Objekts festgelegt wird.

Passen wir nun die Methoden in der Sammlungsklasse der grafischen Elemente in \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh an.

Da wir jetzt die Möglichkeit haben, sowohl die einzelne Farbe des Formularhintergrunds als auch die Farben seiner Verlaufsfüllung gleichzeitig festzulegen, ersetzen wir die Angabe der Farbe

obj.SetColorBackground(clr[0]);

mit SetColorsBackground() in allen Methoden zum Erstellen von Formularobjekten mit Verlaufsfüllung:

//--- Create a graphical object form object on canvas on a specified chart and subwindow with the vertical gradient filling
   int               CreateFormVGradient(const long chart_id,
                                         const int subwindow,
                                         const string name,
                                         const int x,
                                         const int y,
                                         const int w,
                                         const int h,
                                         color &clr[],
                                         const uchar opacity,
                                         const bool movable,
                                         const bool activity,
                                         const bool shadow=false,
                                         const bool redraw=false)
                       {
                        int id=this.m_list_all_canv_elm_obj.Total();
                        CForm *obj=new CForm(chart_id,subwindow,name,x,y,w,h);
                        ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id);
                        if(res==ADD_OBJ_RET_CODE_ERROR)
                           return WRONG_VALUE;
                        obj.SetID(id);
                        obj.SetActive(activity);
                        obj.SetMovable(movable);
                        obj.SetColorsBackground(clr);
                        obj.SetColorFrame(clr[0]);
                        obj.SetOpacity(opacity,false);
                        obj.SetShadow(shadow);
                        obj.DrawRectangle(0,0,obj.Width()-1,obj.Height()-1,obj.ColorFrame(),obj.Opacity());
                        obj.Done();
                        obj.Erase(clr,opacity,true,false,redraw);
                        return obj.ID();
                       }

Jetzt erhält die Methode das Array selbst und nicht die erste Farbe aus dem Farbarray.

Solche Änderungen wurden bereits in allen Methoden zum Erstellen von Formularobjekten vorgenommen.

Lassen Sie uns ähnliche Verbesserungen an den Methoden zum Erstellen von Bedienfeldobjekten vornehmen:

//--- Create a WinForms Panel object graphical object on canvas on a specified chart and subwindow with the vertical gradient filling
   int               CreatePanelVGradient(const long chart_id,
                                          const int subwindow,
                                          const string name,
                                          const int x,
                                          const int y,
                                          const int w,
                                          const int h,
                                          color &clr[],
                                          const uchar opacity,
                                          const bool movable,
                                          const bool activity,
                                          const int  frame_width=-1,
                                          ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL,
                                          const bool shadow=false,
                                          const bool redraw=false)
                       {
                        int id=this.m_list_all_canv_elm_obj.Total();
                        CPanel *obj=new CPanel(chart_id,subwindow,name,x,y,w,h);
                        ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id);
                        if(res==ADD_OBJ_RET_CODE_ERROR)
                           return WRONG_VALUE;
                        obj.SetID(id);
                        obj.SetActive(activity);
                        obj.SetMovable(movable);
                        obj.SetColorsBackground(clr);
                        obj.SetColorFrame(clr[0]);
                        obj.BorderStyle(frame_style);
                        obj.SetOpacity(opacity,false);
                        //--- Draw the shadow drawing flag
                        obj.SetShadow(shadow);
                        if(shadow)
                          {
                           //--- Calculate the shadow color as the chart background color converted to the monochrome one
                           //--- and darken the monochrome color by 20 units
                           color clrS=obj.ChangeColorLightness(obj.ChangeColorSaturation(obj.ColorBackground(),-100),-20);
                           //--- Draw the form shadow with the right-downwards offset from the form by three pixels along all axes
                           //--- Set the shadow opacity to the default value, while the blur radius is equal to 4
                           obj.DrawShadow(3,3,clrS,CLR_DEF_SHADOW_OPACITY,DEF_SHADOW_BLUR);
                          }
                        obj.DrawRectangle(0,0,obj.Width()-1,obj.Height()-1,obj.ColorFrame(),obj.Opacity());
                        obj.Erase(clr,opacity,true,false,redraw);
                        if(frame_width>0)
                           obj.FrameWidthAll(frame_width);
                        obj.SetActiveAreaShift(obj.FrameWidthLeft(),obj.FrameWidthBottom(),obj.FrameWidthRight(),obj.FrameWidthTop());
                        obj.DrawFormFrame(obj.FrameWidthTop(),obj.FrameWidthBottom(),obj.FrameWidthLeft(),obj.FrameWidthRight(),obj.ColorFrame(),obj.Opacity(),obj.BorderStyle());
                        obj.Done();
                        return obj.ID();
                       }

Hier übergeben wir auch das Array der Farbverlaufsfüllung an die Objekteigenschaften. Außerdem legen wir den geprägten Rahmentyp in den Objekteigenschaften fest und verwenden den Typ beim Zeichnen des Rahmens. Bisher habe ich einfach den Rahmen mit dem an die Methode übergebenen Typ gezeichnet, ohne den Rahmentyp im Objekt selbst festzulegen. Dies führte dazu, dass der Rahmen beim Neuzeichnen des Objekts übersprungen wurde, da die Standardeigenschaft, die das Fehlen des Rahmens anzeigt, bestehen blieb. Jetzt habe ich das behoben.

Solche Änderungen wurden bereits an allen Methoden zum Erstellen von Paneelen mit Verlaufsfüllung vorgenommen. Sie finden sie in den unten angehängten Dateien.

Lassen Sie uns vorübergehend den Code zum Anzeigen von Texten auf Objekten festlegen, die an das Paneel gebunden sind, damit wir sicherstellen können, dass IDs und ZOrder-Eigenschaften dem Paneel korrekt zugewiesen werden.

Fügen wir die folgenden Codeblöcke zur Methode hinzu, die ZOrder auf das angegebene Element setzt und es in allen anderen Elementen anpasst:

//+------------------------------------------------------------------+
//| Set ZOrde to the specified element                               |
//| and adjust it in other elements                                  |
//+------------------------------------------------------------------+
bool CGraphElementsCollection::SetZOrderMAX(CGCnvElement *obj)
  {
//--- Get the maximum ZOrder of all graphical elements
   long max=this.GetZOrderMax();
//--- If an invalid pointer to the object has been passed or the maximum ZOrder has not been received, return 'false'
   if(obj==NULL || max<0)
      return false;
//--- Declare the variable for storing the method result
   bool res=true;
//--- If the maximum ZOrder is zero, ZOrder is equal to 1,
//--- if the maximum ZOrder is less than (the total number of graphical elements)-1, ZOrder will exceed it by 1,
//--- otherwise, ZOrder will be equal to (the total number of graphical elements)-1
   long value=(max==0 ? 1 : max<this.m_list_all_canv_elm_obj.Total()-1 ? max+1 : this.m_list_all_canv_elm_obj.Total()-1);
//--- If failed to set ZOrder for an object passed to the method, return 'false'
   if(!obj.SetZorder(value,false))
      return false;
//--- Temporarily declare a form object for drawing a text for visually displaying its ZOrder
   CForm *form=obj;
//--- and draw a text specifying ZOrder on the form
   form.TextOnBG(0,form.TypeElementDescription()+": ID "+(string)form.ID()+", ZD "+(string)form.Zorder(),form.Width()/2,form.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',255,true,true);
   
   //--- Temporarily (for the test purpose), if the element is a form or higher
   if(form.Type()>=OBJECT_DE_TYPE_GFORM)
     {
      for(int j=0;j<form.ElementsTotal();j++)
        {
         CForm *pnl=form.GetElement(j);
         if(pnl==NULL || pnl.TypeGraphElement()!=GRAPH_ELEMENT_TYPE_PANEL)
            continue;
         pnl.TextOnBG(0,"ID "+(string)pnl.ID()+", ZD "+(string)pnl.Zorder(),pnl.Width()/2,pnl.Height()/2,FRAME_ANCHOR_CENTER,C'0x3A,0x57,0x74',pnl.Opacity());
        }
     }
   
//--- Sort the list of graphical elements by an element ID
   this.m_list_all_canv_elm_obj.Sort(SORT_BY_CANV_ELEMENT_ID);
//--- Get the list of graphical elements without an object whose ID is equal to the ID of the object passed to the method
   CArrayObj *list=CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),CANV_ELEMENT_PROP_ID,obj.ID(),NO_EQUAL);
//--- If failed to obtain the list and the list size exceeds one,
//--- which indicates the presence of other objects in it in addition to the one sorted by ID, return 'false'
   if(list==NULL && this.m_list_all_canv_elm_obj.Total()>1)
      return false;
//--- In the loop by the obtained list of remaining graphical element objects
   for(int i=0;i<list.Total();i++)
     {
      //--- get the next object
      CGCnvElement *elm=list.At(i);
      //--- If failed to get the object or if this is a control form for managing pivot points of an extended standard graphical object
      //--- or, if the object's ZOrder is zero, skip the object since there is no need in changing its ZOrder as it is the bottom one
      if(elm==NULL || elm.Type()==OBJECT_DE_TYPE_GFORM_CONTROL || elm.Zorder()==0)
         continue;
      //--- If failed to set the object's ZOrder to 1 less than it already is (decreasing ZOrder by 1), add 'false' to the 'res' value
      if(!elm.SetZorder(elm.Zorder()-1,false))
         res &=false;
      //--- Temporarily (for the test purpose), if the element is a form or higher
      if(elm.Type()>=OBJECT_DE_TYPE_GFORM)
        {
         //--- assign the pointer to the element for the form and draw a text specifying ZOrder on the form 
         form=elm;
         form.TextOnBG(0,form.TypeElementDescription()+": ID "+(string)form.ID()+", ZD "+(string)form.Zorder(),form.Width()/2,form.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',255,true,true);
         
         for(int j=0;j<form.ElementsTotal();j++)
           {
            CForm *pnl=form.GetElement(j);
            if(pnl==NULL || pnl.TypeGraphElement()!=GRAPH_ELEMENT_TYPE_PANEL)
               continue;
            pnl.TextOnBG(0,"ID "+(string)pnl.ID()+", ZD "+(string)pnl.Zorder(),pnl.Width()/2,pnl.Height()/2,FRAME_ANCHOR_CENTER,C'0x3A,0x57,0x74',pnl.Opacity());
           }
        }
     }
//--- Upon the loop completion, return the result set in 'res'
   return res;
  }
//+------------------------------------------------------------------+

Der Code ermöglicht es uns, ein an das Paneel gebundenes Objekt zu finden und seine ID und seinen ZOrder-Wert anzuzeigen.

Die Textausgabe wird nicht immer rechtzeitig ausgelöst, aber das spielt jetzt keine Rolle, da diese Funktionalität nur einmal benötigt wird. Danach werde ich die Codes aus der Methode entfernen.

Jetzt sind wir bereit für den Test.


Test

Nehmen wir den EA aus dem vorherigen Artikel und speichern ihn in \MQL5\Experts\TestDoEasy\Part104\ als TestDoEasyPart104.mq5.

Im vorherigen EA habe ich bereits ein Paneel erstellt, das wiederum einige andere Paneele enthält. Jetzt werde ich ein einzelnes Bedienfeldobjekt innerhalb des Bedienfelds erstellen und die Tasten zuweisen, um es an die Ränder des Hauptbedienfelds zu binden. Durch Drücken der Tasten auf der Tastatur stellen wir alle möglichen Arten der Bindung des abhängigen Paneels an die Seiten des Hauptpaneels ein. Der Padding-Wert gleich 10 wird dem Hauptpaneel zugewiesen, damit der Einzug von den Rändern des Paneels sichtbar ist, sodass wir sehen können, wie Padding funktioniert, wenn ein Objekt in einem anderen positioniert wird.

Legen Sie die folgenden Schlüssel fest:

Weisen wir noch die Tastencodes im globalen Bereich zu:

//+------------------------------------------------------------------+
//|                                            TestDoEasyPart104.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 <DoEasy\Engine.mqh>
//--- defines
#define  FORMS_TOTAL (3)   // Number of created forms
#define  START_X     (4)   // Initial X coordinate of the shape
#define  START_Y     (4)   // Initial Y coordinate of the shape
#define  KEY_LEFT    (65)  // (A) Left
#define  KEY_RIGHT   (68)  // (D) Right
#define  KEY_UP      (87)  // (W) Up
#define  KEY_DOWN    (88)  // (X) Down
#define  KEY_CENTER  (83)  // (S) Center
#define  KEY_ORIGIN  (90)  // (Z) Default
//--- 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
CEngine        engine;
color          array_clr[];
//+------------------------------------------------------------------+

Erstellen Sie in OnInit() des EA alle Objekte (dies wurde zuvor getan) und erstellen wir ein Paneel in einem anderen:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Set EA global variables
   ArrayResize(array_clr,2);        // Array of gradient filling colors
   array_clr[0]=C'26,100,128';      // Original ≈Dark-azure color
   array_clr[1]=C'35,133,169';      // Lightened original color
//--- Create the array with the current symbol and set it to be used in the library
   string array[1]={Symbol()};
   engine.SetUsedSymbols(array);
   //--- Create the timeseries object for the current symbol and period, and show its description in the journal
   engine.SeriesCreate(Symbol(),Period());
   engine.GetTimeSeriesCollection().PrintShort(false); // Short descriptions
//--- Create form objects
   string name="";
   int obj_id=WRONG_VALUE;
   CArrayObj *list=NULL;
   CForm *form=NULL;
   for(int i=0;i<FORMS_TOTAL;i++)
     {
      form=engine.CreateWFForm("Form_0"+string(i+1),30,(form==NULL ? 100 : form.BottomEdge()+20),100,30,array_clr,245,true);
      if(form==NULL)
         continue;
      //--- Set ZOrder to zero, display the text describing the gradient type and update the form
      //--- Text parameters: the text coordinates and the anchor point in the form center
      //--- Create a new text animation frame with the ID of 0 and display the text on the form
      form.SetZorder(0,false);
      form.TextOnBG(0,form.TypeElementDescription()+": ID "+(string)form.ID()+", ZD "+(string)form.Zorder(),form.Width()/2,form.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',255,true,false);
     }
//--- Create four graphical elements
   CGCnvElement *elm=NULL;
   array_clr[0]=C'0x65,0xA4,0xA9';
   array_clr[1]=C'0x48,0x75,0xA2';
//--- Vertical gradient
   elm=engine.CreateWFElement("CElmVG",form.RightEdge()+20,20,200,50,array_clr,127,true);
   if(elm!=NULL)
     {
      elm.SetFontSize(10);
      elm.Text(elm.Width()/2,elm.Height()/2,elm.TypeElementDescription()+": ID "+(string)elm.ID()+", ZD "+(string)elm.Zorder(),C'0xDB,0xEE,0xF2',elm.Opacity(),FRAME_ANCHOR_CENTER);
      elm.Update();
     }
//--- Vertical cyclic gradient
   elm=engine.CreateWFElement("CElmVGC",form.RightEdge()+20,80,200,50,array_clr,127,true,true);
   if(elm!=NULL)
     {
      elm.SetFontSize(10);
      elm.Text(elm.Width()/2,elm.Height()/2,elm.TypeElementDescription()+": ID "+(string)elm.ID()+", ZD "+(string)elm.Zorder(),C'0xDB,0xEE,0xF2',elm.Opacity(),FRAME_ANCHOR_CENTER);
      elm.Update();
     }
//--- Horizontal gradient
   elm=engine.CreateWFElement("CElmHG",form.RightEdge()+20,140,200,50,array_clr,127,false,false);
   if(elm!=NULL)
     {
      elm.SetFontSize(10);
      elm.Text(elm.Width()/2,elm.Height()/2,elm.TypeElementDescription()+": ID "+(string)elm.ID()+", ZD "+(string)elm.Zorder(),C'0xDB,0xEE,0xF2',elm.Opacity(),FRAME_ANCHOR_CENTER);
      elm.Update();
     }
//--- Horizontal cyclic gradient
   elm=engine.CreateWFElement("CElmHGC",form.RightEdge()+20,200,200,50,array_clr,127,false,true);
   if(elm!=NULL)
     {
      elm.SetFontSize(10);
      elm.Text(elm.Width()/2,elm.Height()/2,elm.TypeElementDescription()+": ID "+(string)elm.ID()+", ZD "+(string)elm.Zorder(),C'0xDB,0xEE,0xF2',elm.Opacity(),FRAME_ANCHOR_CENTER);
      elm.Update();
     }
//--- Create WinForms Panel object
   CPanel *pnl=NULL;
   pnl=engine.CreateWFPanel("WFPanel",elm.RightEdge()+20,50,230,150,array_clr,200,true,true,false,-1,FRAME_STYLE_BEVEL,true);
   if(pnl!=NULL)
     {
      //--- Set the Padding value to 10
      pnl.PaddingAll(10);
      pnl.FontDrawStyle(FONT_STYLE_NORMAL);
      pnl.Bold(true);
      pnl.SetFontSize(10);
      pnl.TextOnBG(0,pnl.TypeElementDescription()+": ID "+(string)pnl.ID()+", ZD "+(string)pnl.Zorder(),pnl.Width()/2,pnl.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',pnl.Opacity());
      //--- In the loop, create N bound panel objects (a single panel)
      CPanel *obj=NULL;
      for(int i=0;i<1;i++)
        {
         //--- create the panel object with coordinates along the X axis in the center and 10 along the Y axis, the width of 80 and the height of 50
         pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_PANEL,pnl,pnl.GetUnderlay().Width()/2-40,10,80,50,C'0xCD,0xDA,0xD7',200,true);
         //--- To control the creation of bound objects,
         //--- get the pointer to the bound object by the loop index
         obj=pnl.GetElement(i);
         //--- take the pointer to the base object from the obtained object

         //--- and display the name of a created bound object and the name of its base object in the journal
         Print
           (
            TextByLanguage("Объект ","Object "),obj.TypeElementDescription()," ",obj.Name(),
            TextByLanguage(" привязан к объекту "," is attached to object "),obj.GetBase().TypeElementDescription()," ",obj.GetBase().Name()
           );
         if(obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_PANEL)
           { 
            //--- Display the ID and zorder on the newly created panel
            obj.TextOnBG(0,"ID "+(string)obj.ID()+", ZD "+(string)obj.Zorder(),obj.Width()/2,obj.Height()/2,FRAME_ANCHOR_CENTER,C'0x3A,0x57,0x74',obj.Opacity());
            //--- Set the frame color, active panel area and draw the frame
            obj.SetColorFrame(obj.ColorBackground());
            obj.SetActiveAreaShift(obj.FrameWidthLeft(),obj.FrameWidthBottom(),obj.FrameWidthRight(),obj.FrameWidthTop());
            obj.DrawFormFrame(obj.FrameWidthTop(),obj.FrameWidthBottom(),obj.FrameWidthLeft(),obj.FrameWidthRight(),obj.ColorFrame(),obj.Opacity(),FRAME_STYLE_BEVEL);
            obj.Update();
           }
        }
      pnl.Update(true);
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+


In der Ereignisbehandlung durch OnChartEvent() fügen wir den folgenden Code zur Tastenanschlagbehandlung hinzu:

   //--- If a key is pressed
   if(id==CHARTEVENT_KEYDOWN)
     {
      CPanel *panel=engine.GetWFPanel(7).GetElement(0);
      if(panel!=NULL)
        {
         if(lparam==KEY_UP)
            panel.DockMode(CANV_ELEMENT_DOCK_MODE_TOP);
         else if(lparam==KEY_DOWN)
            panel.DockMode(CANV_ELEMENT_DOCK_MODE_BOTTOM);
         else if(lparam==KEY_LEFT)
            panel.DockMode(CANV_ELEMENT_DOCK_MODE_LEFT);
         else if(lparam==KEY_RIGHT)
            panel.DockMode(CANV_ELEMENT_DOCK_MODE_RIGHT);
         else if(lparam==KEY_CENTER)
            panel.DockMode(CANV_ELEMENT_DOCK_MODE_FILL);
         else if(lparam==KEY_ORIGIN)
            panel.DockMode(CANV_ELEMENT_DOCK_MODE_NONE);
        }

Hier erhalten wir das Paneel-Objekt anhand seiner ID (wir wissen mit Sicherheit, dass seine ID 7 ist), holen das allererste (und einzige) Objekt aus seiner Liste gebundener Objekte und setzen DockMode für das erhaltene Paneel-Objekt abhängig vom Code von a gedrückte Taste.

Kompilieren Sie den EA, starten Sie ihn auf dem Chart und drücken Sie einige Tastaturtasten:


Wie wir sehen können, wird das Paneel beim Drücken verschiedener Tasten und Einstellen der entsprechenden Bindungsmethoden korrekt in seinem Container positioniert. Durch Drücken von Z werden die ursprüngliche Größe und die Koordinaten wiederhergestellt. Dabei haftet das Paneel nicht direkt an den Kanten des Containers, sondern befindet sich im Padding-Abstand von den Kanten des Hauptpaneels. Beim Verschieben des Hauptpaneels wird das daran befestigte Paneel je nach aktuellem Bindungsmodus mit neu eingestellten Koordinaten ebenfalls korrekt verschoben.

Was kommt als Nächstes?

Im nächsten Artikel werde ich meine Arbeit an WinForms-Objekten fortsetzen.

Alle Dateien der aktuellen Bibliotheksversion, des Test-EA und des Chartereignis-Kontrollindikators für MQL5 sind unten angehängt, damit Sie sie testen und herunterladen können. Hinterlassen Sie Ihre Fragen, Kommentare und Vorschläge im Kommentarteil.

Zurück zum Inhalt

*Vorherige Artikel in dieser Reihe:

DoEasy. Steuerung (Teil 1): Erste Schritte
DoEasy. Steuerung (Teil 2): Arbeiten an der Klasse CPanel
DoEasy. Steuerung (Teil 3): Erstellen gebundener Steuerelemente