English Русский 中文 Deutsch 日本語 Português
preview
DoEasy. Elementos de control (Parte 4): Elemento de control "Panel", parámetros Padding y Dock

DoEasy. Elementos de control (Parte 4): Elemento de control "Panel", parámetros Padding y Dock

MetaTrader 5Ejemplos | 30 junio 2022, 13:48
405 0
Artyom Trishkin
Artyom Trishkin

Contenido


Concepto

Continuamos trabajando con la funcionalidad del control "Panel" de WinForms: hoy analizaremos sus propiedades Padding y Dock.

El objeto "Panel" de WinForm es esencialmente un contenedor regular en cuyo interior podemos colocar otros objetos de WinForm. Al colocar dichos objetos, podemos especificar de forma independiente las coordenadas necesarias para colocar el objeto, que se ubicará en las coordenadas especificadas. Pero también podemos especificar cómo vincular el objeto colocado en el contenedor después de que se haya creado dentro del mismo. Y para esto, existen seis formas de vincular un objeto dentro de su contenedor (propiedad Dock del objeto):

  1. Unido al borde superior y estirado a lo ancho del contenedor,
  2. Unido al borde inferior y estirado a lo ancho del contenedor,
  3. Unido al borde izquierdo y estirado a lo alto del contenedor,
  4. Unido al borde derecho y estirado a lo alto del contenedor,
  5. Unido a lo ancho y alto de todo el contenedor (rellenado),
  6. El objeto se vincula a las coordenadas especificadas y sus dimensiones no cambian.

Al mismo tiempo, si seleccionamos uno de los métodos de vinculación en que el objeto se adhiere a uno o todos los límites de su contenedor, sus bordes se atraerán hacia el borde del mismo, teniendo en cuenta el valor de Padding establecido para el contenedor: el borde del objeto posicionado no se adjuntará al borde del contenedor, sino que se separará respecto a su borde en una distancia cuyo valor se especificará en el valor de Padding del contenedor.

La forma en que se coloca un objeto dentro de un contenedor se especifica en su valor Dock. En este caso, si hay más de un objeto en el contenedor, cada uno de los siguientes se "pegará" no al borde del contenedor a la distancia de Padding, sino al objeto anterior, atraído por el mismo lado del contenedor.

La imagen muestra cómo los objetos en MS Visual Studio son atraídos hacia el borde superior de su contenedor, para lo cual el valor de Padding se establece en 20, si seleccionamos la atracción hacia el borde superior del contenedor para ellos:


Hoy implementaremos todas las opciones posibles de colocación de un objeto dentro de su contenedor para un solo objeto.
Para tener en cuenta el valor de Padding del contenedor, no cambiaremos las coordenadas de la ubicación del objeto dentro del panel, sin embargo, sí que añadiremos al objeto "Panel" un objeto más en el lienzo: el objeto de sustrato, en el que ya colocaremos todos los objetos necesarios dentro del panel.

¿Por qué lo hacemos así? Simplemente resultará más cómodo a la hora de organizar los objetos, teniendo en cuenta el valor de Padding del contenedor: el objeto de sustrato en sí se desplazará en el valor de esta propiedad, además, cuando necesitemos dibujar algo en el lienzo, lo dibujaremos solo en el sustrato, ya que se desplazará en el valor de Padding, y no necesitaremos calcular las coordenadas y dimensiones del dibujo en este objeto. Existen otras comodidades adicionales que luego podremos evaluar al desarrollar otros objetos WinForm.


Mejorando las clases de la biblioteca

Tenemos la capacidad de crear diferentes estilos de objetos de formulario, y para su definición existe un archivo aparte en la biblioteca, donde escribimos gradualmente los parámetros adicionales para los estilos. Como el objeto de formulario se puede rellenar no solo con un color, sino también con un rellenado de gradiente, indicaremos en el archivo \MQL5\Include\DoEasy\GraphINI.mqh los nuevos ajustes de estilo para el rellenado de gradiente del fondo del objeto de formulario:

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

Aumentemos el número de parámetros de estilo de 9 a 11.


En el archivo \MQL5\Include\DoEasy\Defines.mqh, en la enumeración de los bordes del control vinculado al contenedor, desplazaremos la constanteque indica que el objeto no está vinculado a los bordes del contenedor al primer lugar:

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

Anteriormente, la constante se encontraba en el último lugar de la lista. Esto no resulta muy cómodo si queremos comprobar que el objeto no está vinculado a los bordes del contenedor. Ahora podemos comprobar simplemente si el valor del método que retorna el tipo de vinculación del objeto al contenedor es falso, ya que el valor 0, que ahora se corresponde con esta constante, es el equivalente booleano a false.

En el archivo E:\MetaQuotes\MetaTrader 5\MQL5\Include\DoEasy\Data.mqh, escribimos los índices de los nuevos mensajes:

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

y los textos de los mensajes que se corresponden con los índices nuevamente añadidos:

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


Al modificar el tamaño del objeto de formulario y sus herederos, necesitaremos redibujar completamente el objeto. Para restaurar posteriormente sus dimensiones originales, necesitaremos conocer sus coordenadas y dimensiones originales. Además, como el formulario se puede rellenar tanto con un color como con un gradiente, resultará más cómodo tener una matriz en el objeto que contenga todos los colores del gradiente. Si solo hay un color, se escribirá en la matriz. Por consiguiente, en las propiedades del objeto tendremos siempre solo el color inicial, mientras que en la matriz tendremos todos los colores utilizados para el gradiente. En consecuencia, necesitaremos mejorar el método para configurar el color y añadir otro método para configurar los colores del gradiente. En este caso, ambos métodos completarán los valores tanto para un solo color como para un gradiente.

Además, podremos vincular diferentes objetos entre sí, por ejemplo, en un objeto de panel puede haber dos objetos WinForm adjuntos. Y este objeto de panel estará adjunto a otro panel. Para los dos objetos adjuntos al primer panel, será el objeto básico y se escribirá en sus propiedades como el objeto básico, que es el contenedor para ellos. Pero cuando colocamos este contenedor con dos objetos dentro de otro contenedor, entonces, para el primer contenedor, el nuevo objeto de panel se convertirá en el contenedor básico. Y para que los dos objetos colocados en el primer contenedor puedan saber qué objeto es el principal en esta jerarquía, deberemos añadir una propiedad más: el objeto principal de toda la jerarquía. Luego, los dos objetos colocados en el primer contenedor conocerán su contenedor básico, así como el objeto principal, el segundo contenedor, al que está unido su básico.

Vamos a mejorar la clase del objeto de elemento gráfico en el archivo \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh.

En la sección protegida del mismo, declararemos las nuevas variables:

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


En la sección privada de la clase, declararemos el método que permite guardar una matriz de colores de rellenado de gradiente:

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


En la sección pública de la clase, escribiremos los métodos que establecerán y retornarán los valores de las variables recién añadidas:

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

En el constructor por defecto, inicializamos con el valor NULL el puntero al objeto principal de la jerarquía de objetos vinculados:

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


Vamos a mejorar el método que establece el color de fondo del elemento gráfico:

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

Ahora, aquí, no solo se escribe en las propiedades del objeto el valor del color de fondo transmitido al método, sino que también se llama a un método que escribe este color único en la nueva matriz de colores del rellenado de gradiente. Para hacer esto, declararemos una matriz, escribiremos el valor de color en ella y transmitiremos esta matriz al método SaveColorsBG (), que analizaremos un poco más adelante.

A continuación, escribiremos un método que establece los colores del rellenado de gradiente del fondo de un elemento gráfico:

   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];
                       }

Al método se le transmite una matriz de colores de rellenado de gradiente, que transmitimos al método SaveColorsBG(), y luego escribimos el primer color de la matriz de colores en el valor del color de fondo. Por lo tanto, estableceremos a la vez varios colores distintos: si solo se usa un color para el fondo del elemento gráfico, este será el primer color de la matriz; si se usa un rellenado de gradiente, todos los colores transmitidos al método en la matriz se escribirán en la matriz de colores de rellenado de gradiente en el método SaveColorsBG().

Un poco más abajo, escribiremos dos métodos más: un método que retorna el número de colores en el rellenado de gradientey un método que retorna el color de la matriz de colores en el índice especificado:

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

El primer método simplemente retorna el tamaño de la matriz de colores del objeto.

En el segundo método, si el tamaño de la matriz de colores del objeto es cero, se retornará el valor de la variable m_color_bg; de lo contrario, si se transmite un índice incorrecto (mayor que el tamaño de la matriz), se retornará el color más reciente, de lo contrario, el color ubicado en la matriz en el índice especificado.

En el constructor paramétrico, inicializamos el puntero al objeto principal de la jerarquía de objetos adjuntos y escribimos en la matriz de colores del rellenado de gradiente del fondo del objeto el color de la variable m_color_bg:

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

Vamos a escribir las mismas líneas en el constructor protegido (no tiene sentido volver a mostrarlo aquí).


A continuación, eliminamos del método que actualiza las coordenadas del objeto la verificación de su inmovilidad:

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

El hecho de que esta verificación se encuentre dentro de este método en particular es muy lógico, pero esto nos crea grandes problemas al desplazar objetos relacionados con una jerarquía común. Si movemos un objeto principal que contiene objetos anidados, y alguno de ellos no se puede mover, se volverá muy difícil rastrear sus banderas de inmovilidad según la presencia del puntero a los objetos padre y principal dentro del objeto.

En general, por el momento, hemos decidido que resulta más fácil monitorear la inmovilidad solo del objeto que queremos mover directamente cuando intentamos desplazar un objeto con el ratón, que realizar numerosas comprobaciones sobre la prohibición del desplazamiento e ignorarlas para todos los objetos anidados en la jerarquía.

Vamos a mejorar los métodos que establecen la nueva anchura y altura:

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

Antes, ambos métodos retornaban directamente el resultado del cambio del tamaño de la altura o la anchura del lienzo:

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

Pero esto no modificaba los valores escritos en las propiedades del objeto.

Por lo tanto, ahora, primero verificaremos que el valor escrito en la propiedad del objeto sea igual al valor transmitido al método y, si los valores resultan iguales, entonces no habrá nada que cambiar, y retornaremos de inmediato true.
Además, si no ha sido posible modificar el tamaño del lienzo, informaremos de ello en el diario de registro y retornaremos false.
Si todo sale bien, escribiremos el nuevo valor en las propiedades del objeto y retornaremos true.

Cuando llamamos al método Erase() de la clase CCanvas, rellenamos el formulario con el color y la opacidad especificados. Por ello, si especificamos un color diferente al establecido en la variable m_color_bg (o en la matriz de colores) al llamar a este método, el formulario se coloreará con este mismo color. Al transmitir la matriz de colores al método, almacenaremos estos colores en la matriz interna en dos métodos Erase() del objeto de elemento gráfico:

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

En este método, encargado de rellenar el formulario con un color, estableceremos el mismo color en la matriz de colores del rellenado de gradiente del objeto. Así, coloreando el formulario con un color distinto al original, podremos cambiar este color inicial para el objeto.

En otro método, guardaremos adicionalmente los valores transmitidos al método en las variables que almacenan el tipo de rellenado de gradiente y luego guardaremos la matriz de colores en la matriz del objeto:

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

Las banderas del tipo de rellenado de gradiente almacenadas en este método nos permitirán volver a crear el objeto con los mismos valores de gradiente que tenía antes de que el formulario fuera redibujado y redimensionado.

Método que guarda los colores en una matriz de colores de fondo:

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

Aquí, si el tamaño de la matriz de colores transmitido al método no coincide con el tamaño de la matriz de colores del objeto, cambiaremos el tamaño de la matriz de colores del rellenado de gradiente del objeto y luego copiaremos la matriz transmitida al método a la matriz de colores del objeto.

Como ahora tenemos los métodos que guardan y devuelven el desplazamiento en píxeles de las coordenadas de un objeto en relación con otro objeto, en el archivo de clase del objeto de sombra \MQL5\Include\DoEasy\Objects\Graph\ShadowObj.mqh, eliminaremos las variables que almacenan los valores de desplazamiento de la sombra en relación con el objeto que la proyecta, y las reemplazaremos con estos métodos.

Eliminamos de la sección privada de la clase las variables:

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

Asimismo, eliminamos de la sección pública de la clase los métodos que retornan los valores de las variables remotas:

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

En el constructor de la clase, eliminamos la inicialización de estas variables:

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

En el método que dibuja la sombra del objeto, reemplazamos las líneas que escriben valores en las variables

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

y la línea donde se utilizan los valores escritos para estas variables

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

por el establecimiento de los valores llamando a los métodosy la lectura de los valores de desplazamientoen la clase principal, en lugar de leer los valores de las variables ahora eliminadas:

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

Ahora, para cada objeto cuyo padre sea la clase de objeto del elemento gráfico, siempre estarán disponibles los valores de sus coordenadas compensadas con respecto al objeto básico. Por esta razón, hemos eliminado aquí estas variables ahora innecesarias.


Vamos a mejorar la clase del objeto de formulario en el archivo \MQL5\Include\DoEasy\Objects\Graph\Form.mqh.

Como los objetos de WinForms se heredan de esta clase, deberemos trasladar la lista para colocar los punteros a los objetos creados dentro del formulario desde la sección privada a la sección protegida. Y en el mismo lugar, declararemos el método que actualiza las coordenadas de los objetos adjuntos:

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:


Al crear un nuevo objeto adjunto a un formulario, deberemos indicar en el nuevo objeto el objeto al que está adjunto, es decir, su objeto principal.
Para ello, transmitiremos al método de creación de un nuevo objeto el puntero al objeto desde el que se crea:

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


En el método de inicialización de variables, inicializamos las banderas del tipo de rellenado de gradiente del fondo del formulario con los valores predeterminados:

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

Por defecto, el rellenado de gradiente utilizará un gradiente vertical no cíclico.

En el método que crea un nuevo objeto gráfico, antes olvidamos especificar la bandera de reubicación. Vamos a indicarla:

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

Y así, en el método que crea un nuevo elemento adjunto, primero, escribimos el puntero a su objeto padre principal de la jerarquía de objetos relacionados y establecemos las propiedades que omitimos anteriormente:

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


Como hoy hemos añadido dos nuevos valores a los estilos de formulario, vamos a añadir en el método que establece el estilo del formulario la configuración de estos valores, y ahí mismo, y también llamaremos a otro método Erase (), el encargado de dibujar el formulario con un rellenado de gradiente. En este caso, si solo hay un color en la matriz de colores del rellenado de gradiente, el formulario se dibujará de este color, sin gradiente alguno:

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


En el método que actualiza las coordenadas del objeto, antes recorrimos en un ciclo todos los objetos adjuntos al formulario y cambiamos sus coordenadas siguiendo las coordenadas del objeto actual. Ahora hemos declarado el nuevo método MoveDependentObj(), que organizará este ciclo.
Por lo tanto, cambiaremos el método en sí, que se encarga de actualizar las coordenadas del elemento:

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

En primer lugar, añadiremos el puntero al objeto principal de toda la jerarquía de objetos relacionados: este será exactamente el objeto cuyo movimiento provocará el desplazamiento de todos los demás elementos asociados a él (y entre sí).
Al mover un objeto de sombra, ahora usaremos los métodos del objeto de elemento gráfico para obtener el desplazamiento de la sombra en relación con el objeto que proyecta la misma.
En lugar de un ciclo, ahora llamaremos al nuevo método MoveDependentObj(), que analizaremos a continuación.
Finalmente, ahora comprobaremos que este es el objeto principal de toda la cadena de la jerarquía de objetos relacionados, y no el objeto básico de una sola de las cadenas de la jerarquía.


Método que actualiza las coordenadas de los objetos vinculados:

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

Aquí, simplemente tenemos que obtener el siguiente objeto en un ciclo a través de todos los objetos vinculados, y llamar al método Move () de este objeto. En consecuencia, en ese objeto, todos los métodos Move() se llamarán de la misma forma para todos los objetos relacionados con él. Esto hará que se desplace toda la jerarquía de relaciones de todos los objetos vinculados al objeto. 

Vamos a mejorar la clase del objeto de panel en el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Panel.mqh.

En la sección privada de la clase, declararemos un puntero al objeto de sustrato, las variables para almacenar las coordenadas y dimensiones iniciales del panel al crearse este, y los métodos para crear un sustrato y vincular un elemento al contenedor:

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


En la sección protegida de la clase, escribiremos los métodos para trabajar con las coordenadas del objeto de sustrato:

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:


En la sección pública de la clase, escribiremos un método que retorne el puntero al objeto de sustrato y declararemos un método virtual que mueva el objeto del panel:

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


Vamos a mejorar algunos métodos públicos.

Método que establece el modo de vinculación de los bordes del elemento al contenedor:

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

Antes, el método simplemente establecía el valor transmitido a la variable de clase adecuada.

Ahora, no solo estableceremos el valor de la variable, sino que también adjuntaremos directamente el panel a los bordes deseados de su contenedor usando el método SetDockingToContainer(), que analizaremos a continuación.

De la misma manera, finalizaremos los métodos que establecen el espacio a la izquierda, arriba, a la derecha y abajo dentro del control:

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

De la misma forma, en estos métodos, además de escribir los valores transmitidos al método en las variables correspondientes, cambiaremos directamente estas propiedades del objeto de sustrato.

Del mismo modo, mejoraremos los métodos que establecen la anchura del marco del formulario a la izquierda, arriba, a la derecha y al final del elemento de control:

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

Ahora, tan pronto como se cambie la anchura del marco en cualquier lado del panel, las propiedades correspondientes del objeto de sustrato se cambiarán inmediatamente para que el sustrato siempre encaje en el marco del panel (si el Padding del lado del panel es menor que la anchura del marco en el mismo lado), o bien se corresponda con el valor de Padding de ese lado del panel.

Vamos a eliminar dos constructores adicionales de la lista: su declaración e implementación se dará fuera del cuerpo de la clase:

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

No necesitábamos estos constructores.

Pero en el constructor paramétrico, añadiremos la inicialización de las propiedades del objeto del panel, que no realizamos antes, así como las nuevas variables:

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

De la misma forma, inicializamos las mismas variables en el constructor, indicando los identificadores del gráfico y la subventana.

En el método que retorna las coordenadas iniciales del objeto vinculado, ahora devolveremos las coordenadas relativas a las coordenadas del objeto de sustrato, y no el panel en sí y la anchura de su borde, como antes:

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


Método que crea el objeto de sustrato:

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

Aquí, crearemos un nuevo objeto de elemento gráfico con las coordenadas y los tamaños calculados en relación con los valores de rellenado de todos los lados del panel, para que el objeto de sustrato encaje exactamente en el área delimitada por los valores de rellenado de todos los lados del panel .
Si no hemos podido crear el objeto, informaremos sobre ello y retornaremos false.
Si el objeto creado no se ha podido añadir a la lista de objetos de panel, informaremos sobre ello, eliminaremos el objeto creado y retornaremos false.
Si todo ha salido bien, llamaremos al método para establecer todos los parámetros del sustrato creado y retornaremos true.


Método que establece todos los parámetros del sustrato:

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

Aquí, todo es simple: escribimos los desplazamientos de las coordenadas del sustrato en relación con las coordenadas del panely las coordenadas y tamaños del objeto de sustrato.


Método virtual que actualiza las coordenadas del elemento:

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

La lógica del método se explica en los comentarios al código. Aquí, comprobaremos la movilidad del objeto, y si no es móvil, saldremos del método. A continuación, desplazaremos la sombra y el objeto en sí. Después, llamaremos al método de desplazamiento de todos los objetos vinculados de su jerarquía. Al final, comprobaremos que este sea el objeto principal de la jerarquía de todos los objetos vinculados y actualizaremos el gráfico.


Método que vincula un elemento a un contenedor:

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

La lógica del método se explica en los comentarios al código. La conclusión es que, según el modo de vinculación del objeto al contenedor, calcularemos las coordenadas y los tamaños correspondientes necesarios y desplazaremos el objeto a nuevas coordenadas. Si el objeto no está vinculado a los lados del contenedor, obtendremos sus coordenadas y dimensiones iniciales, establecidas cuando se creó.

Siempre llamamos al método cuando se establece un nuevo valor para la propiedad DockMode del objeto.

Ahora vamos a corregir los métodos en la clase de colección de elementos gráficos en \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh.

Como ahora tenemos la capacidad de configurar simultáneamente el color único del fondo del formulario y los colores para su rellenado de gradiente, en todos los métodos necesarios para crear los objetos de formulario con un rellenado de gradiente, reemplazaremos la indicación del color con el método

obj.SetColorBackground(clr[0]);

por el método SetColorsBackground():

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

Ahora, el método no transmitirá el primer color de la matriz de colores, sino la matriz en sí.

Estos cambios ya se han realizado en todos los métodos para crear los objetos de formulario.

Vamos a realizar mejoras similares en los métodos para crear los objetos de panel:

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

Aquí también transmitiremos una matriz de colores de rellenado de gradiente a las propiedades del objeto, además, estableceremos el tipo de marco como marco con relieve en las propiedades del objeto y usaremos este tipo al dibujar el marco. Antes, simplemente dibujábamos un marco con el tipo transmitido al método, pero no escribíamos el tipo de marco en el objeto en sí, para que, al redibujarse el objeto, no se dibujara el marco, ya que la propiedad era la predeterminada: sin marco, y aquí no cambiaba de ninguna forma. Hemos solucionado este problema en el presente artículo.

Estos cambios ya se han realizado en todos los métodos para crear paneles con un rellenado de gradiente y se pueden encontrar en los archivos adjuntos al artículo.

Para que podamos controlar la corrección de la asignación a los objetos adjuntos al panel, así como sus identificadores y propiedades de ZOrder, escribiremos temporalmente el código necesario para mostrar los textos en los objetos adjuntos al panel.

En el método que asigna ZOrder al elemento especificado y lo ajusta en todos los demás elementos, añadimos estos bloques de código:

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

Este código nos permitirá encontrar un objeto adjunto al panel y mostrar una inscripción en él con el identificador del objeto encontrado y su valor de ZOrder.

La muestra de las inscripciones no siempre funciona a tiempo, pero eso no importa por ahora, solo necesitamos esta característica una vez y de forma temporal, luego eliminaremos estos códigos del método.

Ya estamos preparados para realizar las pruebas.


Simulación

Para la simulación, tomaremos el asesor del artículo anterior y lo guardaremos en la nueva carpeta \MQL5\Experts\TestDoEasy\Part104\ con el nuevo nombre TestDoEasyPart104.mq5.

¿Cómo realizaremos la prueba? Ya creamos un panel en el asesor experto anterior, y hay varios más en él. Ahora crearemos un objeto de panel dentro del panel y asignaremos las teclas para vincularlo a los bordes del panel principal. Presionando las teclas en el teclado, configuraremos todos los tipos posibles de vinculación del panel dependiente a los lados del principal. Vamos a asignar un valor de Padding de 10 al panel principal, de manera que la separación respecto a los bordes del panel sea visible y quede claro cómo funciona el Padding al colocar un objeto dentro de otro.

Las teclas se asignan de la forma siguiente:

  • W - para ajustar al borde superior,
  • A - para ajustar al lado izquierdo,
  • D - para ajustar al lado derecho,
  • X - para ajustar al borde inferior,
  • S - para el rellenado,
  • Z - para restablecer la vinculación y volver a las dimensiones y coordenadas originales.

En el área global, asignamos las teclas clave:

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

En el OnInit() del asesor experto, crearemos todos los objetos (ya se dio antes) y crearemos otro panel dentro del panel:

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


En el manejador OnChartEvent(), escribiremos el siguiente código de procesamiento de las pulsaciones de las teclas:

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

Aquí, obtenemos el objeto de panel según su ID (sabemos con certeza que su ID es 7); luego, obtenemos de su lista de objetos vinculados el primero (y único) y, según el código del botón presionado, establecemos DockMode para el objeto de panel obtenido.

Compilamos el asesor experto, lo ejecutamos en el gráfico y presionamos las teclas en el teclado:


Como podemos ver, al presionar diferentes teclas y establecer los métodos de vinculación correspondientes, el panel se coloca correctamente dentro de su contenedor, y cuando presionamos la tecla Z, vuelve a sus dimensiones y coordenadas originales. En este caso, el panel no se pega directamente a los bordes del contenedor, sino que se encuentra a la distancia de Padding de los bordes del panel principal. Al desplazar el panel principal, el panel adjunto también se moverá correctamente a las coordenadas recién especificadas, según el modo de ajuste actual.

¿Qué es lo próximo?

En el próximo artículo, continuaremos desarrollando los objetos de WinForms.

Más abajo, adjuntamos todos los archivos de la versión actual de la biblioteca, así como los archivos del asesor de prueba y el indicador de control de eventos de los gráficos para MQL5. Podrá descargarlo todo y ponerlo a prueba por sí mismo. Si tiene preguntas, observaciones o sugerencias, podrá concretarlas en los comentarios al artículo.

Volver al contenido

*Artículos de esta serie:

DoEasy. Elementos de control (Parte 1): Primeros pasos
DoEasy. Elementos de control (Parte 2): Continuamos trabajando con la clase CPanel
DoEasy. Elementos de control (Parte 3): Creando controles vinculados


Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/10756

Archivos adjuntos |
MQL5.zip (4341.62 KB)
Desarrollando un EA comercial desde cero (Parte 14): Volume at Price (II) Desarrollando un EA comercial desde cero (Parte 14): Volume at Price (II)
Hoy añadiremos varios recursos a nuestro EA. Este artículo les resultará bastante interesante y puede orientarlos hacia nuevas ideas y métodos para presentar la información y, al mismo tiempo, corregir pequeños fallos en sus proyectos.
Desarrollando un EA comercial desde cero (Parte 13): Times And Trade (II) Desarrollando un EA comercial desde cero (Parte 13): Times And Trade (II)
Hoy vamos a construir la segunda parte del sistema Times & Trade para analizar el mercado. En el artículo anterior Times & Trade ( I ) presenté un sistema alternativo para organizar un gráfico de manera que tengamos un indicador que nos permita interpretar las operaciones que se han ejecutado en el mercado lo más rápido posible.
DoEasy. Elementos de control (Parte 5): Objeto básico WinForms, control "Panel", parámetro AutoSize DoEasy. Elementos de control (Parte 5): Objeto básico WinForms, control "Panel", parámetro AutoSize
En este artículo, crearemos un objeto básico para todos los objetos de la biblioteca WinForms y comenzaremos a implementar la propiedad AutoSize del objeto WinForms "Panel", es decir, el cambio automático del tamaño para que se ajuste a su contenido interno.
Aprendiendo a diseñar un sistema de trading con ADX Aprendiendo a diseñar un sistema de trading con ADX
En este artículo, continuaremos nuestra serie sobre el diseño de sistemas de trading usando los indicadores más populares, y hablaremos del indicador del índice direccional medio (ADX). Analizaremos este indicador con detalle para entenderlo bien y utilizarlo con una sencilla estrategia. Y es que, profundizando en un indicador, podremos usarlo mejor en el trading.