DoEasy. Elementos de control (Parte 3): Creando controles vinculados

17 junio 2022, 12:17
Artyom Trishkin
0
377

Contenido


Concepto

Hoy trabajaremos en la creación de una funcionalidad que permita construir controles gráficos adjuntos a otro elemento gráfico. Por ejemplo, tenemos el control "Panel". El elemento en sí es tan solo un contenedor en el que se pueden colocar otros controles. Al mover este panel, todos los controles adjuntos a él también se desplazarán. Esto significa que el panel es el objeto básico para los elementos de GUI agrupados en él. Como no disponemos de un constructor de GUI visual en el terminal, el diseño de estos elementos será responsabilidad del programador que escriba el programa. Además, la biblioteca facilitará la creación de estos elementos de la interfaz gráfica de usuario: solo tendremos que escribir la secuencia de creación de los elementos gráficos necesarios para colocar estos en el panel. Además, podremos crear elementos y añadirlos al panel de forma programática.

Hoy implementaremos los métodos, o más bien seguiremos escribiendo estos, ya que tenemos desde hace mucho tiempo un método para crear elementos dentro de otro. Los métodos nos permitirán crear un nuevo elemento gráfico directamente desde un panel ya unido a él, y posteriormente podremos trabajar con este como con una parte independiente de la GUI del programa. A su vez, cada uno de estos elementos creados y adjuntos a un panel puede también crear otros elementos dentro de sí mismo. La unidad mínima que tendrá esta funcionalidad será un objeto de la clase de formulario.

Además de la tarea mencionada, tocaremos también el objeto de sombra de un elemento gráfico, ya que todavía existen algunos errores lógicos no resueltos a la hora de utilizarlo con cualquiera de los objetos que permiten tener una sombra. Por ejemplo, la sombra solo se dibuja encima de un gráfico, pero debe superponerse al objeto sobre el que se encuentra el que la proyecta. En general, se realizarán algunos trabajos para corregir pequeñas deficiencias.


Mejorando las clases de la biblioteca

En el archivo \MQL5\Include\DoEasy\Defines.mqh ya tenemos escritas las macrosustituciones para indicar los valores por defecto para algunas propiedades de los objetos de la biblioteca.

En el bloque de parámetros del lienzo, cambiamos los nombres de las macros de CLR_FORE_COLOR a CLR_DEF_FORE_COLOR, añadimos el valor por defecto para la opacidad de los elementos gráficos y añadimos algunos valores más por defecto para las propiedades de sombreado de los objetos:

//--- Canvas parameters
#define PAUSE_FOR_CANV_UPDATE          (16)                       // Canvas update frequency
#define CLR_CANV_NULL                  (0x00FFFFFF)               // Zero for the canvas with the alpha channel
#define CLR_DEF_FORE_COLOR             (C'0x2D,0x43,0x48')        // Default color for texts of objects on canvas
#define CLR_DEF_OPACITY                (200)                      // Default color non-transparency for canvas objects
#define CLR_DEF_SHADOW_COLOR           (C'0x6B,0x6B,0x6B')        // Default color for canvas object shadows
#define CLR_DEF_SHADOW_OPACITY         (127)                      // Default color non-transparency for canvas objects
#define DEF_SHADOW_BLUR                (4)                        // Default blur for canvas object shadows
#define DEF_FONT                       ("Calibri")                // Default font
#define DEF_FONT_SIZE                  (8)                        // Default font size
#define OUTER_AREA_SIZE                (16)                       // Size of one side of the outer area around the form workspace
#define DEF_FRAME_WIDTH_SIZE           (3)                        // Default form/panel/window frame width
//--- Graphical object parameters

Estos valores serán utilizados por la biblioteca en sus métodos de creación de elementos gráficos. Una vez creados, los valores por defecto siempre se podrán cambiar.

En la lista de tipos de elementos gráficos, no tenemos una disposición muy lógica de las constantes:

//+------------------------------------------------------------------+
//| The list of graphical element types                              |
//+------------------------------------------------------------------+
enum ENUM_GRAPH_ELEMENT_TYPE
  {
   GRAPH_ELEMENT_TYPE_STANDARD,                       // Standard graphical object
   GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED,              // Extended standard graphical object
   GRAPH_ELEMENT_TYPE_ELEMENT,                        // Element
   GRAPH_ELEMENT_TYPE_SHADOW_OBJ,                     // Shadow object
   GRAPH_ELEMENT_TYPE_FORM,                           // Form
   GRAPH_ELEMENT_TYPE_WINDOW,                         // Window
   //--- WinForms
   GRAPH_ELEMENT_TYPE_PANEL,                          // Windows Forms Panel
  };
//+------------------------------------------------------------------+

La constante del objeto de sombra se encuentra después de la constante del objeto de elemento gráfico. Esto no resulta muy práctico en cuanto a la limitación de los tipos de objetos en algunos métodos. Por ejemplo, si queremos manejar todos los objetos que representan elementos de la interfaz gráfica de usuario, podemos indicar que se manejen los objetos del tipo que abarca desde el propio elemento y superior. Sin embargo, en este caso, el objeto de sombra también será manejado por el método. Para evitar esta situación y permitir la selección de un grupo de objetos según el valor de una constante de enumeración, intercambiaremos las constantes especificadas de forma que los objetos de elemento de la interfaz gráfica de usuario estén dispuestos en el orden de su jerarquía de herencia:

//+------------------------------------------------------------------+
//| The list of graphical element types                              |
//+------------------------------------------------------------------+
enum ENUM_GRAPH_ELEMENT_TYPE
  {
   GRAPH_ELEMENT_TYPE_STANDARD,                       // Standard graphical object
   GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED,              // Extended standard graphical object
   GRAPH_ELEMENT_TYPE_SHADOW_OBJ,                     // Shadow object
   GRAPH_ELEMENT_TYPE_ELEMENT,                        // Element
   GRAPH_ELEMENT_TYPE_FORM,                           // Form
   GRAPH_ELEMENT_TYPE_WINDOW,                         // Window
   //--- WinForms
   GRAPH_ELEMENT_TYPE_PANEL,                          // Windows Forms Panel
  };
//+------------------------------------------------------------------+

Ahora, podremos seleccionar rápidamente los objetos necesarios para su procesamiento en los métodos de la biblioteca.


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

//--- CForm
   MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT,// No shadow object. Create it using the CreateShadowObj() method
   MSG_FORM_OBJECT_ERR_FAILED_CREATE_SHADOW_OBJ,      // Failed to create new shadow object
   MSG_FORM_OBJECT_ERR_FAILED_CREATE_PC_OBJ,          // Failed to create new pixel copier object
   MSG_FORM_OBJECT_PC_OBJ_ALREADY_IN_LIST,            // Pixel copier object with ID already present in the list 
   MSG_FORM_OBJECT_PC_OBJ_NOT_EXIST_LIST,             // No pixel copier object with ID in the list 
   MSG_FORM_OBJECT_ERR_NOT_INTENDED,                  // The method is not meant for creating such an object: 

y el texto del mensaje correspondiente al nuevo índice añadido:

//--- CForm
   {"Отсутствует объект тени. Необходимо сначала его создать при помощи метода CreateShadowObj()","There is no shadow object. You must first create it using the CreateShadowObj () method"},
   {"Не удалось создать новый объект для тени","Failed to create new object for shadow"},
   {"Не удалось создать новый объект-копировщик пикселей","Failed to create new pixel copier object"},
   {"В списке уже есть объект-копировщик пикселей с идентификатором ","There is already a pixel copier object in the list with ID "},
   {"В списке нет объекта-копировщика пикселей с идентификатором ","No pixel copier object with ID "},
   {"Метод не предназначен для создания такого объекта: ","The method is not intended to create such an object: "},


Todos los elementos de la interfaz gráfica de usuario se crearán a partir de los diferentes objetos de la biblioteca, el más pequeño de los cuales es el objeto gráfico. No obstante, a cada objeto subsiguiente en su jerarquía de herencia, se le añadirá una nueva funcionalidad que faltará en los objetos padre. Por lo tanto, algunos métodos del objeto padre no tendrán funcionalidad suficiente para ser implementados al completo en los objetos herederos. Tendremos que hacer virtuales estos métodos y añadir la funcionalidad necesaria a sus objetos herederos.

Cada objeto que compone los elementos de la interfaz gráfica de usuario deberá "conocer" el elemento al que está unido, de forma que pueda usar las propiedades del objeto básico para su posicionamiento, por ejemplo. Para ello, insertaremos un puntero al objeto básico en la clase de objeto de elemento (este objeto será el objeto padre de todos los elementos de GUI, por lo que tendrá sentido colocar el puntero allí).

En esta fase, todos los objetos a partir de los cuales se construye la interfaz gráfica estarán vinculados a las coordenadas de pantalla del gráfico: sus coordenadas partirán de la esquina superior izquierda del gráfico. Pero si necesitamos posicionar un objeto dentro de otro, lo lógico será desplazar su sistema de coordenadas a su objeto básico, donde el inicio será la esquina superior izquierda del elemento gráfico básico, no el gráfico. Para ello, introduciremos el concepto de coordenadas relativas y añadiremos nuevos métodos que retornen las coordenadas del objeto respecto al objeto básico al que se adjunta. Entonces podremos simplemente indicar el desplazamiento relativo a la esquina superior izquierda del objeto básico para posicionar el objeto, en lugar de calcular continuamente una nueva coordenada de posicionamiento cuando el elemento gráfico básico se mueva en el gráfico.

En el archivo \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh, en su sección protegida, declaramos el puntero al objeto básico al que se adjunta el elemento gráfico, y en la sección privada, declaramos las variables para almacenar sus coordenadas relativas al objeto básico:

//+------------------------------------------------------------------+
//| Class of the graphical element object                            |
//+------------------------------------------------------------------+
class CGCnvElement : public CGBaseObj
  {
protected:
   CGCnvElement     *m_element_base;                           // Pointer to the parent element
   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

//--- Create (1) the object structure and (2) the object from the structure
   virtual bool      ObjectToStruct(void);
   virtual void      StructToObject(void);
   
private:
   int               m_shift_coord_x;                          // Offset of the X coordinate relative to the base object
   int               m_shift_coord_y;                          // Offset of the Y coordinate relative to the base object
   struct SData

En la sección pública, escribimos los métodos para establecer y retornar el puntero al objeto básico:

//--- 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 pointer to the parent control
   void              SetBase(CGCnvElement *element)                                    { this.m_element_base=element;               }
   CGCnvElement     *GetBase(void)                                                     { return this.m_element_base;                }
//--- Return the pointer to a canvas object
   CCanvas          *GetCanvasObj(void)                                                { return &this.m_canvas;                     }

El método Move() lo haremos virtual:

//--- Return the size of the graphical resource copy array
   uint              DuplicateResArraySize(void)                                       { return ::ArraySize(this.m_duplicate_res);  }
   
//--- Update the coordinates (shift the canvas)
   virtual bool      Move(const int x,const int y,const bool redraw=false);

//--- Save an image to the array
   bool              ImageCopy(const string source,uint &array[]);

Como el objeto gráfico puede estar unido a otro objeto, pero no puede tener objetos adjuntos a él, desplazarlo usando el método Move() no requerirá ninguna modificación. Al mismo tiempo, sus objetos heredados podrán tener ya otros elementos gráficos adjuntos cuyos punteros se colocarán en la lista, lo cual significa que su método Move() deberá manejar también la lista completa de objetos adjuntos, donde su método Move() será llamado para cada uno. Es por ello que este método será virtual y se implementará aparte para cada uno de los objetos heredados.

En la sección pública, escribiremos más métodos para establecer y retornar las coordenadas relativas del objeto:

protected:
//--- Protected constructor
                     CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                  const long    chart_id,
                                  const int     wnd_num,
                                  const string  name,
                                  const int     x,
                                  const int     y,
                                  const int     w,
                                  const int     h);
public:
//--- (1) Set and (2) return the X coordinate shift relative to the base object
   void              SetCoordXRelative(const int value)                                { this.m_shift_coord_x=value;                }
   int               CoordXRelative(void)                                        const { return this.m_shift_coord_x;               }
//--- (1) Set and (2) return the Y coordinate shift relative to the base object
   void              SetCoordYRelative(const int value)                                { this.m_shift_coord_y=value;                }
   int               CoordYRelative(void)                                        const { return this.m_shift_coord_y;               }
   
//--- Event handler


En el constructor por defecto, escribimos la inicialización del puntero al objeto básico y los desplazamientos de las coordenadas relativas del objeto:

//--- 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_base=NULL; this.m_shift_coord_x=0; this.m_shift_coord_y=0; }
                    ~CGCnvElement()
                        { this.m_canvas.Destroy();             }

Si el puntero al objeto básico es NULL, significará que el objeto no está adjunto a ningún otro elemento gráfico.

En el bloque de métodos simplificados de acceso a las propiedades del objeto, escribimos los métodos que retornan las coordenadas relativas de los bordes derecho e inferior del objeto:

//--- 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;                                               }
   uchar             Opacity(void)                       const { return this.m_opacity;                                                }
   int               RightEdge(void)                     const { return this.CoordX()+this.m_canvas.Width();                           }
   int               BottomEdge(void)                    const { return this.CoordY()+this.m_canvas.Height();                          }
//--- Return the relative coordinate of the (1) right and (2) bottom element edge
   int               RightEdgeRelative(void)             const { return this.CoordXRelative()+this.m_canvas.Width();                   }
   int               BottomEdgeRelative(void)            const { return this.CoordYRelative()+this.m_canvas.Height();                  }
//--- Return the (1) X, (2) Y coordinates, (3) element width and (4) height,

Y hacemos que el método BringToTop() sea virtual:

//--- Set the object above all
   virtual void      BringToTop(void)                          { CGBaseObj::SetVisible(false,false); CGBaseObj::SetVisible(true,false);}
//--- (1) Show and (2) hide the element
   virtual void      Show(void)                                { CGBaseObj::SetVisible(true,false);                                    }
   virtual void      Hide(void)                                { CGBaseObj::SetVisible(false,false);                                   }

La razón por la que este método también será virtual es la misma de antes: todos los elementos gráficos adjuntos al objeto, y su lista, también deberán ser manejados al establecer el objeto básico por encima de todos. De lo contrario, cualquier elemento adjunto al objeto en primer plano simplemente desaparecerá de él: también deberá ser desplazado por encima del objeto básico en el orden en el que ha sido vinculado al mismo. Y esto será gestionado en los métodos virtuales BringToTop() de los objetos herederos, que tienen las listas de objetos adjuntos.

En los constructores paramétricos y protegidos, inicializamos el puntero al objeto básico y el desplazamiento de coordenadas relativo:

//+------------------------------------------------------------------+
//| 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_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(this.Create(chart_id,wnd_num,this.m_name,x,y,w,h,colour,opacity,redraw))
     {

El puntero y la inicialización de la variable también se escribirán en el constructor protegido de la misma forma.


Un objeto de sombra puede ser mínimamente un objeto de formulario y más hacia abajo en la jerarquía de herencia. Ya lo hemos usado antes, pero para objetos fijos. Si, por el contrario, asignamos una sombra a un objeto de interfaz móvil, notaremos inmediatamente un fallo en el movimiento del objeto gráfico: la sombra permanecerá en su sitio. Así que primero tendremos que añadir variables para almacenar el desplazamiento de la sombra en relación con el objeto que la proyecta, y en segundo lugar, para mover la sombra siguiendo al objeto que se mueve. Concretamente: como el objeto de sombra estará por debajo del elemento gráfico, primero deberemos mover el objeto de sombra, después el elemento gráfico, y luego deberemos actualizar el gráfico. En este caso, visualmente, parecerá que el objeto se mueve y que la sombra que proyecta también se mueve tras el objeto.

Y no solo eso: al clicar en un objeto, este pasará a primer plano, por lo que su sombra deberá estar también por encima de los demás objetos del gráfico, de forma que la sombra del objeto que se mueve se dibuje sobre los otros elementos gráficos situados en el gráfico por debajo del objeto que se mueve.

En el archivo \MQL5\Include\DoEasy\Objects\Graph\ShadowObj.mqh, en la sección privada, declaramos las variables para almacenar el desplazamiento de la sombra respecto al objeto gráfico que arroja esta sombra:

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

En la sección pública, escribimos los métodos que retornan los valores de desplazamiento de la sombra respecto al objeto:

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

Inicializamos los valores de estas variables en el constructor de la clase:

//+------------------------------------------------------------------+
//| 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 el objeto de sombra, escribimos los valores de desplazamiento transmitidos en los argumentos del método en estas nuevas variables:

//+------------------------------------------------------------------+
//| 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.m_offset_x=shift_x;
   this.m_offset_y=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.m_offset_x,this.CoordY()+this.m_offset_y);
   CGCnvElement::Update();
  }
//+------------------------------------------------------------------+

Una vez dibujada la sombra, la desplazaremos a los valores especificados. En principio, el método es prácticamente el mismo, salvo el almacenamiento de los desplazamientos de los ejes X e Y en las nuevas variables. Antes, el objeto sombra se desplazaba usando los valores transmitidos en los argumentos del método, pero ahora se desplaza a los valores escritos en las nuevas variables, que es lo mismo. Sin embargo, ahora tenemos los desplazamientos especificados al crear y dibujar el objeto sombra, y los necesitaremos al mover el objeto sombra.


En el archivo de objetos de formulario \MQL5\Include\DoEasy\Objects\Graph\Form.mqh, trasladamos los dos métodos CreateNameDependentObject() y CreateShadowObj()
desde lasección privada de la clase a la sección protegida:

protected:
   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;   }
   
public:

Estos métodos deberán estar disponibles en los objetos de las clases herederas, por lo que su lugar estará en la sección protegida de la clase.

Ahora, hacemos virtual el método privado que crea un nuevo objeto gráfico, ya que requerirá algunos ajustes en los objetos herederos. Asimismo, declaramos un método virtual que retorna las coordenadas del objeto vinculado al crearse:

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

El método que crea el nuevo objeto vinculado se encuentra dentro de la clase de objeto de formulario. El objeto de formulario se hereda de la clase de objeto de elemento gráfico. En consecuencia, estas dos clases (de elemento y de formulario) son conocidas y visibles en el objeto clase de formulario. Y también se pueden crear aquí. No obstante, necesitaremos ser capaces de crear otros controles: ventanas, paneles y otros objetos de clase, lo cual haremos en el proceso de desarrollo de los objetos WinForms. Sin embargo, no serán visibles en esta clase. En consecuencia, cada una de ellas tendrá un método virtual CreateNewGObject() que contendrá el código para crear aquellos controles que sean visibles dentro de esas clases.

En la sección pública de la clase, declaramos los métodos virtuales que hemos hecho virtuales en la clase de objeto de elemento gráfico:

public:
//--- Return (1) the mouse status relative to the form, as well as (2) X and (3) Y coordinate of the cursor
   ENUM_MOUSE_FORM_STATE MouseFormState(const int id,const long lparam,const double dparam,const string sparam);
   int               MouseCursorX(void)            const { return this.m_mouse.CoordX();     }
   int               MouseCursorY(void)            const { return this.m_mouse.CoordY();     }
//--- Set the flags of mouse scrolling, context menu and the crosshairs tool for the chart
   void              SetChartTools(const bool flag);
//--- (1) Set and (2) return the shift of X and Y coordinates relative to the cursor
   void              SetOffsetX(const int value)         { this.m_offset_x=value;            }
   void              SetOffsetY(const int value)         { this.m_offset_y=value;            }
   int               OffsetX(void)                 const { return this.m_offset_x;           }
   int               OffsetY(void)                 const { return this.m_offset_y;           }

//--- Update the coordinates (shift the canvas)
   virtual bool      Move(const int x,const int y,const bool redraw=false);
//--- Set the priority of a graphical object for receiving the event of clicking on a chart
   virtual bool      SetZorder(const long value,const bool only_prop);
//--- Set the object above all
   virtual void      BringToTop(void);
   
//--- Event handler

A continuación, escribiremos la implementación de estos métodos que contienen los manejadores necesarios para la clase del objeto de formulario donde se debe manejar el desplazamiento de los otros controles adjuntos al objeto de formulario.

El método que retorna el objeto de sombra, antes devolvía el tipo de objeto CGCnvElement por error. No existe ningún error crítico en esto, salvo que los métodos escritos en la clase del objeto de sombra no estarán disponibles para nosotros.
Así que vamos a arreglar este error para que el método retorne el tipo de objeto de sombra, y también a añadir dos métodos para devolver el número de controles adjuntos y el control según su índice en la lista de objetos adjuntos:

//--- Return (1) the list of attached objects and (2) the shadow object
   CForm            *GetObject(void)                                          { return &this;                  }
   CArrayObj        *GetListElements(void)                                    { return &this.m_list_elements;  }
   CShadowObj       *GetShadowObj(void)                                       { return this.m_shadow_obj;      }
//--- Return the pointer to (1) the animation object, the list of (2) text and (3) rectangular animation frames
   CAnimations      *GetAnimationsObj(void)                                   { return this.m_animations;      }
   CArrayObj        *GetListFramesText(void)
                       { return(this.m_animations!=NULL ? this.m_animations.GetListFramesText() : NULL);       }
   CArrayObj        *GetListFramesQuad(void)
                       { return(this.m_animations!=NULL ? this.m_animations.GetListFramesQuad() : NULL);       }

//--- Return the (1) number of bound elements and (2) the bound element by the index in the list
   int               ElementsTotal(void)                       const { return this.m_list_elements.Total();    }
   CGCnvElement     *GetElement(const int index)                     { return this.m_list_elements.At(index);  }
   
//--- Set the form (1) color scheme and (2) style


En el método que crea un nuevo archivo adjunto, cambiamos los argumentos del método. Antes, transmitíamos al método, entre otras propiedades, el número del elemento a crear y el indicador de desplazamiento. Como el número se elegirá automáticamente, y la bandera de desplazamiento no será necesaria para los elementos vinculados, porque los objetos heredan todos los movimientos del objeto básico al que están unidos, en lugar del número, transmitiremos el tipo de objeto que se está creando, mientras que la bandera de desplazamiento simplemente se eliminará de los argumentos del método. La declaración del método tiene ahora el aspecto que sigue:

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


En el método que dibuja la sombra del objeto, introducimos un valor por defecto para el valor de desenfoque de la sombra, en lugar del valor 4 anteriormente transmitido:

//--- Add a new attached element
   bool              AddNewElement(CGCnvElement *obj,const int x,const int y);

//--- Draw an object shadow
   void              DrawShadow(const int shift_x,const int shift_y,const color colour,const uchar opacity=127,const uchar blur=DEF_SHADOW_BLUR);

//--- Draw the form frame


Anteriormente, el método que creaba un nuevo objeto gráfico solo creaba un objeto de tipo CGCnvElement.
Ahora vamos a mejorar el método para crear un objeto del tipo transmitidoen los argumentos del método:

//+------------------------------------------------------------------+
//| 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;
   //--- Depending on the created object type,
   switch(type)
     {
      //--- create a graphical element object
      case GRAPH_ELEMENT_TYPE_ELEMENT :
         element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity);
        break;
      //--- create a form object
      case GRAPH_ELEMENT_TYPE_FORM :
         element=new CForm(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      default:
        break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name);
   return element;
  }
//+------------------------------------------------------------------+


El método que crea un nuevo elemento adjunto ha sufrido cambios significativos:

//+------------------------------------------------------------------+
//| Create a new attached element                                    |
//+------------------------------------------------------------------+
bool CForm::CreateNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                             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 initial 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.SetBase(this.GetObject());
   obj.SetCoordXRelative(x);
   obj.SetCoordYRelative(y);
   obj.SetZorder(this.Zorder(),false);
//--- Draw an added object and return 'true'
   obj.Erase(colour,opacity,true);
   return true;
  }
//+------------------------------------------------------------------+

La lógica completa se explica en los comentarios al código. Al crear el nombre del objeto gráfico, usaremos el nombre del objeto básico + la línea "_Elm" + el número de objeto de la lista. Para el número de objeto usaremos la siguiente lógica: si el número de objeto es del 1 al 9 inclusive, añadiremos un cero a la izquierda al número, de manera que sea 01, 02, 03,..., ..., 08, 09. A partir del 10, no añadiremos nada. En este método solo se crea el nombre del objeto: se le asigna el nombre del objeto básico en el método CreateNewGObject(), en su primera línea, al llamar al método CreateNameDependentObject().

El método que añade un nuevo archivo adjunto a la lista también lo hemos modificado ligeramente. Por el momento, el método utilizará un ciclo para encontrar en la lista un objeto con el mismo nombre que el añadido:

//+------------------------------------------------------------------+
//| Add a new attached element                                       |
//+------------------------------------------------------------------+
bool CForm::AddNewElement(CGCnvElement *obj,const int x,const int y)
  {
   if(obj==NULL)
      return false;
   for(int i=0;i<this.m_list_elements.Total();i++)
     {
      CGCnvElement *elm=this.m_list_elements.At(i);
      if(elm==NULL)
         continue;
      if(elm.Name()==obj.Name())
        {
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_OBJ_ALREADY_IN_LIST),": ",obj.NameObj());
         return false;
        }
     }
   if(!this.m_list_elements.Add(obj))
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST),": ",obj.NameObj());
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

Esa búsqueda anterior

   this.m_list_elements.Sort(SORT_BY_CANV_ELEMENT_NAME_OBJ);
   int index=this.m_list_elements.Search(obj);
   if(index>WRONG_VALUE)
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_OBJ_ALREADY_IN_LIST),": ",obj.NameObj());
      return false;
     }

Por razones incomprensibles (por ahora), no funciona; siempre retorna el objeto con el nombre transmitido al método en la lista de objetos vinculados, lo cual no debería ser así. Nos ocuparemos de ello más adelante. Precisamente esta búsqueda ha sido sustituida por un ciclo.

Método que retorna las coordenadas iniciales del objeto vinculado:

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

El método retorna los valores de sus coordenadas relativas transmitidas por enlace a los valores ajustados para la anchura del marco a la izquierda para la coordenada X, y en la parte superior para la coordenada Y. Simplemente porque si un objeto tiene un marco, los objetos añadidos no deberían ubicarse en él. Por ello, si transmitimos una coordenada, por ejemplo 0, se compensará con el valor de la anchura del marco. Es decir, si el marco tiene una anchura de 3, todas las coordenadas se desplazarán en este valor.

Método virtual que actualiza 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();
   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.OffsetX(),y-OUTER_AREA_SIZE+this.m_shadow_obj.OffsetY(),false))
         return false;
     }
//--- If failed to set new values into graphical object properties, return 'false'
   if(!this.SetCoordX(x) || !this.SetCoordY(y))
      return 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+this.m_frame_width_left+obj.CoordXRelative(),y+this.m_frame_width_top+obj.CoordYRelative(),false))
         return false;
     }
   //--- If the update flag is set and this is a base object, redraw the chart.
   if(redraw && base==NULL)
      ::ChartRedraw(this.ChartID());
   //--- Return 'true'
   return true;
  }
//+------------------------------------------------------------------+

El método se toma de la clase de objeto padre CGCnvElement, y se le añade un ciclo sobre la lista de objetos vinculados, para cada uno de los cuales también se llama al mismo método. De este modo, todos los objetos de la cadena se desplazarán siguiendo a su objeto básico.
Para asegurarnos de que cada objeto en el que se llama a este método no actualiza el gráfico, se añadirá la comprobación de que se trata precisamente del objeto básico, y no de uno de la lista de adjuntos (ya que el objeto básico no estará adjunto a ningún otro objeto básico, su puntero al objeto básico será NULL, lo cual se comprueba aquí). Es decir, el gráfico solo se actualizará una vez, cuando el ciclo esté completo.

Método que establece la prioridad de un objeto gráfico para obtener el evento de clic sobre el gráfico (CHARTEVENT_CLICK):

//+------------------------------------------------------------------+
//| Set the priority of a graphical object                           |
//| for receiving the event of clicking on a chart                   |
//+------------------------------------------------------------------+
bool CForm::SetZorder(const long value,const bool only_prop)
  {
   if(!CGCnvElement::SetZorder(value,only_prop))
      return false;
   if(this.m_shadow)
     {
      if(this.m_shadow_obj==NULL || !this.m_shadow_obj.SetZorder(value,only_prop))
         return false;
     }
   int total=this.m_list_elements.Total();
   for(int i=0;i<total;i++)
     {
      CGCnvElement *obj=this.m_list_elements.At(i);
      if(obj==NULL)
         continue;
      if(!obj.SetZorder(value,only_prop))
         return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

Al igual que el método anterior, en este método se añade un ciclo por todos los objetos adjuntos para los que se llama al mismo método.

Método que especifica un objeto por encima de todos los demás:

//+------------------------------------------------------------------+
//| Set the object above all the rest                                |
//+------------------------------------------------------------------+
void CForm::BringToTop(void)
  {
//--- If the shadow usage flag is set
   if(this.m_shadow)
     {
      //--- If the shadow object is created, move it to the foreground
      if(this.m_shadow_obj!=NULL)
         this.m_shadow_obj.BringToTop();
     }
//--- Move the object to the foreground (the object is located above the shadow)
   CGCnvElement::BringToTop();
//--- In the loop by all bound objects,
   int total=this.m_list_elements.Total();
   for(int i=0;i<total;i++)
     {
      //--- get the next object from the list
      CGCnvElement *obj=this.m_list_elements.At(i);
      if(obj==NULL)
         continue;
      //--- and move it to the foreground
      obj.BringToTop();
     }
  }
//+------------------------------------------------------------------+

La lógica del método se explica en los comentarios al código. Debemos dejar una cosa clara: si un objeto tiene una sombra, esta deberá ser desplazada al primer plano en primer lugar, quedando más alta que todos los objetos en el gráfico. A continuación, el propio objeto se desplaza al primer plano, de forma que quede por encima de la sombra, y luego, en un ciclo por todos los objetos adjuntos, desplazamos también estos sobre el objeto al que están unidos.


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

En la sección privada de la clase, declaramos los dos métodos virtuales que ya hemos implementado hoy en la clase padre:

//+------------------------------------------------------------------+
//| Panel object class of WForms controls                            |
//+------------------------------------------------------------------+
class CPanel : public CForm
  {
private:
   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
//--- 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);

public:

En el método de creación de un nuevo objeto gráfico en esta clase, podremos añadir la creación de un objeto CPanel, ya que resulta naturalmente visible en esta clase, que tiene el mismo tipo. El método GetCoords() tendrá por ahora la misma implementación que la clase padre. Puede que modifiquemos este método más adelante, al finalizar esta clase.

Además, mejoraremos los métodos que establecen los valores de Padding para cada lado del panel:

//--- 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);
                       }
   void              PaddingTop(const uint value)
                       {
                        this.m_padding[1]=((int)value<this.m_frame_width_top ? this.m_frame_width_top : (int)value);
                       }
   void              PaddingRight(const uint value)
                       {
                        this.m_padding[2]=((int)value<this.m_frame_width_right ? this.m_frame_width_right : (int)value);
                       }
   void              PaddingBottom(const uint value)
                       {
                        this.m_padding[3]=((int)value<this.m_frame_width_bottom ? this.m_frame_width_bottom : (int)value);
                       }
   void              PaddingAll(const uint value)
                       {
                        this.PaddingLeft(value); this.PaddingTop(value); this.PaddingRight(value); this.PaddingBottom(value);
                       }
//--- Return the gap (1) to the left, (2) at the top, (3) to the right and (4) at the bottom between the fields inside the control

Ahora, si el valor de Padding a establecer es inferior a la anchura del marco del panel, el valor resultará igual a la anchura del lado correspondiente del marco.


En el constructor por defecto, añadimos la configuración del tipo de objeto gráfico como «panel» a las propiedades del objeto:

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

Y escribimos exactamente la misma línea en todos los demás constructores de la clase. Sin esta línea, el tipo de objeto registrado por el constructor de la clase padre se establecerá como "Formulario". Por ello, en estos constructores hemos añadido la configuración forzosa de esta propiedad como "Panel".
Sí, es un fallo en la lógica de estas clases. A ver qué podemos hacer en el futuro...

Los nombres de las macrosustituciones CLR_FORE_COLOR, anteriormente renombradas en todos los constructores de las clases, han sido sustituidos por los nuevos: CLR_DEF_FORE_COLOR.

Método que crea un nuevo objeto gráfico:

//+------------------------------------------------------------------+
//| Create a new graphical object                                    |
//+------------------------------------------------------------------+
CGCnvElement *CPanel::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;
   switch(type)
     {
      case GRAPH_ELEMENT_TYPE_ELEMENT :
         element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity);
        break;
      case GRAPH_ELEMENT_TYPE_FORM :
         element=new CForm(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_PANEL :
         element=new CPanel(this.ChartID(),this.SubWindow(),name,x,y,w,h);
        break;
      default:
        break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name);
   return element;
  }
//+------------------------------------------------------------------+

Este método sigue la lógica del método del objeto padre del mismo nombre que hemos comentado anteriormente, pero aquí hemos añadido la creación de otro objeto gráfico de control "Panel", porque ese tipo de objeto resulta conocido en esta clase, al fin y al cabo es una clase de ese tipo.
En otras clases que crearemos más adelante, añadiremos líneas a este método virtual para crear los elementos gráficos correspondientes a su tipo.

Método que retorna las coordenadas iniciales del objeto vinculado:

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

El método tiene la misma lógica que el método del objeto padre homónimo. La diferencia es que aquí usamos los métodos públicos de la clase padre para acceder al valor de la anchura del marco, en lugar de los valores de las variables que se ocultan en la sección privada y no están disponibles en las clases herederas.


En la clase de colección de elementos gráficos \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh, añadiremos los métodos para acceder rápidamente a los controles gráficos:

//--- Return the list of graphical elements by element type
   CArrayObj        *GetListCanvElementByType(const ENUM_GRAPH_ELEMENT_TYPE type)
                       {
                        return CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),CANV_ELEMENT_PROP_TYPE,type,EQUAL);
                       }

//--- ...
                
//--- Return the graphical element by chart ID and name
   CGCnvElement     *GetCanvElement(const long chart_id,const string name)
                       {
                        CArrayObj *list=this.GetListCanvElementByName(chart_id,name);
                        return(list!=NULL ? list.At(0) : NULL);
                       }
//--- Return the graphical element by chart and object IDs
   CGCnvElement     *GetCanvElement(const long chart_id,const int element_id)
                       {
                        CArrayObj *list=this.GetListCanvElementByID(chart_id,element_id);
                        return(list!=NULL ? list.At(0) : NULL);
                       }

//--- Constructor
                     CGraphElementsCollection();

La lógica de estos métodos se ha analizado muchas veces antes, y supone la selección habitual del filtrado según una propiedad especificada.

En el método que crea un objeto gráfico WinForms Panel en el lienzo sobre el gráfico y la subventana especificados, implementamos la creación y el dibujado del objeto de sombra
siespecificamos la presencia de una sombra en los argumentos del método:

//--- Create graphical object WinForms Panel object on canvas on a specified chart and subwindow
   int               CreatePanel(const long chart_id,
                                 const int subwindow,
                                 const string name,
                                 const int x,
                                 const int y,
                                 const int w,
                                 const int h,
                                 const 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.SetColorBackground(clr);
                        obj.SetColorFrame(clr);
                        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,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(),frame_style);
                        obj.Done();
                        return obj.ID();
                       }

Asimismo, añadiremos métodos que creen objetos de panel con diferentes tipos de rellenado (gradiente vertical y horizontal, y gradiente vertical y horizontal cíclico). Aquí solo veremos uno de estos métodos, ya que todos los demás se distinguen solo en la combinación de banderas transmitida al método Erase() de la clase CGCnvElement:

//--- 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.SetColorBackground(clr[0]);
                        obj.SetColorFrame(clr[0]);
                        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(),frame_style);
                        obj.Done();
                        return obj.ID();
                       }
//--- ...

  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+

Los métodos resultan casi idénticos, salvo por las banderas transmitidas a los métodos Erase(). Y, a diferencia del primer método, estos métodos no transmiten una variable que indique el color de rellenado del panel, sino la matriz de colores utilizados para rellenar el gradiente.

Los métodos Erase() de la clase padre CGCnvElement usados para colorear el área rectangular de un objeto, permiten colorear con un solo color o diferentes métodos de rellenado de gradiente. Ya hemos hablado de ellos al crear la clase de elemento gráfico.

Todos estos métodos se pueden encontrar en los archivos adjuntos al artículo.

Para poder acceder fácilmente a la creación y manipulación de los controles desde nuestros programas, en la clase principal de la biblioteca CEngine, en el archivo \MQL5\Include\DoEasy\Engine.mqh, en la sección pública, escribiremos los métodos de acceso rápido a estos objetos y añadiremos una variable para guardar el prefijo de los nombres de los objetos en la sección privada de la clase:

   ENUM_PROGRAM_TYPE    m_program_type;                  // Program type
   string               m_name_program;                  // Program name
   string               m_name_prefix;                   // Object name prefix
//--- Return the counter index by id
   int                  CounterIndex(const int id) const;

...

//--- Return the list of graphical elements by chart ID and object name
   CArrayObj           *GetListCanvElementByName(const long chart_id,const string name)
                          {
                           return this.m_graph_objects.GetListCanvElementByName(chart_id,name);
                          }
//--- Return the list of graphical elements by object type
   CArrayObj           *GetListCanvElementByType(const ENUM_GRAPH_ELEMENT_TYPE type)
                          {
                           return this.m_graph_objects.GetListCanvElementByType(type);
                          }
   
//--- Return the graphical element by chart ID and object name
   CGCnvElement        *GetCanvElementByName(const long chart_id,const string name)
                          {
                           return this.m_graph_objects.GetCanvElement(chart_id,name);
                          }
//--- Return the graphical element by chart and object IDs
   CGCnvElement        *GetCanvElementByID(const long chart_id,const int element_id)
                          {
                           return this.m_graph_objects.GetCanvElement(chart_id,element_id);
                          }
   
//--- Return the WForm Element object by object name on the current chart
   CGCnvElement        *GetWFElement(const string name)
                          {
                           string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_ELEMENT);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,::ChartID(),EQUAL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
//--- Return the WForm Element object by chart ID and object name
   CGCnvElement        *GetWFElement(const long chart_id,const string name)
                          {
                           string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_ELEMENT);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,chart_id,EQUAL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
//--- Return the WForm Element object by object ID
   CGCnvElement        *GetWFElement(const int element_id)
                          {
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_ELEMENT);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_ID,element_id,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
   
//--- Return the WForm Form object by object name on the current chart
   CForm               *GetWFForm(const string name)
                          {
                           string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_FORM);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,::ChartID(),EQUAL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
//--- Return the WForm Form object by chart ID and object name
   CForm               *GetWFForm(const long chart_id,const string name)
                          {
                           string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_FORM);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,chart_id,EQUAL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
//--- Return the WForm Element object by object ID
   CForm               *GetWFForm(const int element_id)
                          {
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_FORM);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_ID,element_id,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
   
//--- Return the WForm Panel object by object name on the current chart
   CPanel              *GetWFPanel(const string name)
                          {
                           string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_PANEL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,::ChartID(),EQUAL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
//--- Return the WForm Panel object by chart ID and object name
   CPanel              *GetWFPanel(const long chart_id,const string name)
                          {
                           string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_PANEL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,chart_id,EQUAL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
//--- Return the WForm Panel object by object ID
   CPanel              *GetWFPanel(const int element_id)
                          {
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_PANEL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_ID,element_id,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }

La lógica detrás de todos los métodos consiste simplemente en filtrar las listas según las propiedades del objeto indicado, lo cual hemos discutido muchas veces antes, y no debería suponer ningún problema en absoluto. En cualquier caso, siempre podrá plantear sus dudas en los comentarios al artículo.

Ahora, escribiremos el método que creará un elemento WinForm:

//--- Create the WinForm Element object
   CGCnvElement        *CreateWFElement(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 v_gradient=true,
                                        const bool c_gradient=false,
                                        const bool redraw=false)
                          {
                           //--- Get the created object ID
                           int obj_id=
                             (
                              //--- In case of a vertical gradient:
                              v_gradient ?
                                (
                                 //--- if not a cyclic gradient, create an object with the vertical gradient filling
                                 !c_gradient ? this.m_graph_objects.CreateElementVGradient(chart_id,subwindow,name,x,y,w,h,clr,opacity,false,true,redraw) :
                                 //--- otherwise, create an object with the cyclic vertical gradient filling
                                 this.m_graph_objects.CreateElementVGradientCicle(chart_id,subwindow,name,x,y,w,h,clr,opacity,false,true,redraw)
                                ) :
                              //--- If this is not a vertical gradient:
                              !v_gradient ?
                                (
                                 //--- if not a cyclic gradient, create an object with the horizontal gradient filling
                                 !c_gradient ? this.m_graph_objects.CreateElementHGradient(chart_id,subwindow,name,x,y,w,h,clr,opacity,false,true,redraw) :
                                 //--- otherwise, create an object with the cyclic horizontal gradient filling
                                 this.m_graph_objects.CreateElementHGradientCicle(chart_id,subwindow,name,x,y,w,h,clr,opacity,false,true,redraw)
                                ) :
                              WRONG_VALUE
                             );
                           //--- return the pointer to an object by its ID
                           return this.GetWFElement(obj_id);
                          }

Dependiendo del tipo de rellenado (gradiente vertical u horizontal, cíclico o no), llamamos a los métodos correspondientes para crear el elemento gráfico que hemos escrito anteriormente en la clase de colección de elementos gráficos.

Basándonos en este método, vamos a escribir los métodos para crear objetos gráficos en el gráfico actual y en la subventana indicada, y en la ventana principal del gráfico actual:

//--- Create the WinForm Element object in the specified subwindow on the current chart
   CGCnvElement        *CreateWFElement(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 v_gradient=true,
                                        const bool c_gradient=false,
                                        const bool redraw=false)
                          {
                           return this.CreateWFElement(::ChartID(),subwindow,name,x,y,w,h,clr,opacity,v_gradient,c_gradient,redraw);
                          }
//--- Create the WinForm Element object in the main window of the current chart
   CGCnvElement        *CreateWFElement(const string name,
                                        const int x,
                                        const int y,
                                        const int w,
                                        const int h,
                                        color &clr[],
                                        const uchar opacity,
                                        const bool v_gradient=true,
                                        const bool c_gradient=false,
                                        const bool redraw=false)
                          {
                           return this.CreateWFElement(::ChartID(),0,name,x,y,w,h,clr,opacity,v_gradient,c_gradient,redraw);
                          }

Los métodos retornan el resultado de la llamada al método para crear un elemento gráfico, especificando el ID del gráfico y la subventana necesarios.

Del mismo modo, escribimos los métodos para crear los objetos WinForm Form:

//--- Create the WinForm Form object
   CForm               *CreateWFForm(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 v_gradient=true,
                                     const bool c_gradient=false,
                                     const bool shadow=false,
                                     const bool redraw=false)
                          {
                           int obj_id=
                             (
                              v_gradient ?
                                (
                                 !c_gradient ? this.m_graph_objects.CreateFormVGradient(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,shadow,redraw) :
                                 this.m_graph_objects.CreateFormVGradientCicle(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,shadow,redraw)
                                ) :
                              !v_gradient ?
                                (
                                 !c_gradient ? this.m_graph_objects.CreateFormVGradient(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,shadow,redraw) :
                                 this.m_graph_objects.CreateFormVGradientCicle(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,shadow,redraw)
                                ) :
                              WRONG_VALUE
                             );
                           return this.GetWFForm(obj_id);
                          }
//--- Create the WinForm Form object in the specified subwindow on the current chart
   CForm               *CreateWFForm(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 v_gradient=true,
                                     const bool c_gradient=false,
                                     const bool shadow=false,
                                     const bool redraw=false)
                          {
                           return this.CreateWFForm(::ChartID(),subwindow,name,x,y,w,h,clr,opacity,movable,v_gradient,c_gradient,shadow,redraw);
                          }
//--- Create the WinForm Form object in the main window of the current chart
   CForm               *CreateWFForm(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 v_gradient=true,
                                     const bool c_gradient=false,
                                     const bool shadow=false,
                                     const bool redraw=false)
                          {
                           return this.CreateWFForm(::ChartID(),0,name,x,y,w,h,clr,opacity,movable,v_gradient,c_gradient,shadow,redraw);
                          }

Aquí ocurre exactamente lo mismo: un método construye un objeto de formulario con el rellenado especificado, mientras que los otros dos métodos retornan el resultado de la llamada al primero, pero con el ID del gráfico y la subventana necesarios.

De la misma forma, escribimos los métodos para crear un objeto WinForm Panel:

//--- Create the WinForm Panel object
   CForm               *CreateWFPanel(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 v_gradient=true,
                                      const bool c_gradient=false,
                                      const int frame_width=-1,
                                      const ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL,
                                      const bool shadow=false,
                                      const bool redraw=false)
                          {
                           int obj_id=
                             (
                              v_gradient ?
                                (
                                 !c_gradient ? this.m_graph_objects.CreatePanelVGradient(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,frame_width,frame_style,shadow,redraw) :
                                 this.m_graph_objects.CreatePanelVGradientCicle(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,frame_width,frame_style,shadow,redraw)
                                ) :
                              !v_gradient ?
                                (
                                 !c_gradient ? this.m_graph_objects.CreatePanelHGradient(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,frame_width,frame_style,shadow,redraw) :
                                 this.m_graph_objects.CreatePanelHGradientCicle(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,frame_width,frame_style,shadow,redraw)
                                ) :
                              WRONG_VALUE
                             );
                           return this.GetWFPanel(obj_id);
                          }
//--- Create the WinForm Panel object in the specified subwindow on the current chart
   CForm               *CreateWFPanel(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 v_gradient=true,
                                      const bool c_gradient=false,
                                      const int frame_width=-1,
                                      const ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL,
                                      const bool shadow=false,
                                      const bool redraw=false)
                          {
                           return this.CreateWFPanel(::ChartID(),subwindow,name,x,y,w,h,clr,opacity,movable,v_gradient,c_gradient,frame_width,frame_style,shadow,redraw);
                          }
//--- Create the WinForm Panel object in the main window of the current chart
   CForm               *CreateWFPanel(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 v_gradient=true,
                                      const bool c_gradient=false,
                                      const int frame_width=-1,
                                      const ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL,
                                      const bool shadow=false,
                                      const bool redraw=false)
                          {
                           return this.CreateWFPanel(::ChartID(),0,name,x,y,w,h,clr,opacity,movable,v_gradient,c_gradient,frame_width,frame_style,shadow,redraw);
                          }

Dejaremos que el lector estudie estos métodos por sí mismo: todos son idénticos a los comentados anteriormente.

En el constructor de la clase, creamos el prefijo para el nombre de los objetos gráficos:

//+------------------------------------------------------------------+
//| CEngine constructor                                              |
//+------------------------------------------------------------------+
CEngine::CEngine() : m_first_start(true),
                     m_last_trade_event(TRADE_EVENT_NO_EVENT),
                     m_last_account_event(WRONG_VALUE),
                     m_last_symbol_event(WRONG_VALUE),
                     m_global_error(ERR_SUCCESS)
  {
   this.m_is_hedge=#ifdef __MQL4__ true #else bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) #endif;
   this.m_is_tester=::MQLInfoInteger(MQL_TESTER);
   this.m_program_type=(ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE);
   this.m_name_program=::MQLInfoString(MQL_PROGRAM_NAME);
   this.m_name_prefix=this.m_name_program+"_";
   
//--- ...

//--- ...

  }

Como habrá notado, la variable m_name ha sido renombrada a m_name_program, para precisar su finalidad.


Simulación

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

Ahora, la creación de controles será más fácil que en el último asesor: en la clase CEngine, tenemos los métodos necesarios para crearlos y simultáneamente obtener los punteros a los objetos creados. Después de crear un objeto de panel, creamos cinco objetos de formulario vinculados a él:

//+------------------------------------------------------------------+
//| 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)
     {
      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 five bound form objects located horizontally on top of the panel indented by 4 pixels from each other
      CGCnvElement *obj=NULL;
      for(int i=0;i<5;i++)
        {
         //--- create a form object with calculated coordinates along the X axis 
         pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_FORM,(obj==NULL ? 3 : obj.RightEdgeRelative()+4),3,40,30,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
         CPanel *p=obj.GetBase();
         //--- 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 "),p.TypeElementDescription()," ",p.Name()
           );
        }
      pnl.Update(true);
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Vamos a compilar el asesor y a ejecutarlo en el gráfico:


Bien..., podemos ver que todos los objetos adjuntos al panel han sido creados con éxito; la sombra del objeto de panel está encima de los otros objetos presentes en el gráfico, y se mueve con el objeto para el que ha sido construida. Al construir una línea vertical, al igual que sucede con cualquier otro objeto gráfico estándar, todos los controles, salvo los elementos gráficos fijos, siguen estando por encima del objeto gráfico recién creado.

Cuando creamos los controles vinculados, obtenemos entradas en el diario de registro, donde podemos comprobar que cada objeto vinculado tiene un puntero al objeto básico:

Object Form TestDoEasyPart103_WFPanel_Elm01 is attached to object Control element "Panel" TestDoEasyPart103_WFPanel
Object Form TestDoEasyPart103_WFPanel_Elm02 is attached to object Control element "Panel" TestDoEasyPart103_WFPanel
Object Form TestDoEasyPart103_WFPanel_Elm03 is attached to object Control element "Panel" TestDoEasyPart103_WFPanel
Object Form TestDoEasyPart103_WFPanel_Elm04 is attached to object Control element "Panel" TestDoEasyPart103_WFPanel
Object Form TestDoEasyPart103_WFPanel_Elm05 is attached to object Control element "Panel" TestDoEasyPart103_WFPanel


¿Qué es lo próximo?

En el próximo artículo, seguiremos trabajando en la funcionalidad del control «Panel».

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


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

Archivos adjuntos |
MQL5.zip (4333.23 KB)
Aprendiendo a diseñar un sistema comercial basado en CCI Aprendiendo a diseñar un sistema comercial basado en CCI
En este nuevo artículo de nuestra serie sobre el diseño de sistemas comerciales, hablaremos del Índice del Canal de Mercaderías (CCI), estudiaremos sus entresijos y crearemos juntos un sistema comercial basado en este indicador.
DoEasy. Elementos de control (Parte 2): Continuamos trabajando con la clase CPanel DoEasy. Elementos de control (Parte 2): Continuamos trabajando con la clase CPanel
En este artículo, eliminaremos algunos errores que surgen al trabajar con los elementos gráficos y continuaremos desarrollando el control CPanel. Estos métodos servirán para establecer por defecto los parámetros de fuente usados para todos los objetos de texto en el panel, que a su vez podrán ser colocados en él en el futuro.
Aprendizaje automático y Data Science (Parte 02): Regresión logística Aprendizaje automático y Data Science (Parte 02): Regresión logística
La clasificación de los datos es un punto crucial para los tráders algorítmicos y los programadores. En este artículo, nos centraremos en uno de los algoritmos logísticos de clasificación que podría ayudarnos a identificar los síes o los noes, las subidas y bajadas, las compras y las ventas.
DoEasy. Elementos de control (Parte 1): Primeros pasos DoEasy. Elementos de control (Parte 1): Primeros pasos
Con este artículo, iniciamos un extenso tutorial sobre la creación de controles al estilo de Windows Forms en MQL5. Vamos a empezar el tema creando una clase de panel. Ya se está haciendo difícil manejar las cosas sin controles. Por consiguiente, crearemos todos los controles posibles al estilo de Windows Forms.