English Русский 中文 Deutsch 日本語 Português
preview
DoEasy. Elementos de control (Parte 19): Scrolling de pestañas en el elemento TabControl, eventos de objetos WinForms

DoEasy. Elementos de control (Parte 19): Scrolling de pestañas en el elemento TabControl, eventos de objetos WinForms

MetaTrader 5Ejemplos | 23 diciembre 2022, 16:20
527 0
Artyom Trishkin
Artyom Trishkin

Contenido


Concepto

En el último artículo pusimos a prueba el scrolling de la línea de encabezados de las pestañas al seleccionar un encabezado parcialmente oculto: este se desplazó hacia la izquierda, haciéndose totalmente visible. Hoy, basándonos en la funcionalidad creada, vamos a crear métodos para desplazar la línea de encabezados de izquierda a derecha y de arriba a abajo. En esta caso, además, el scrolling de los encabezados hacia arriba y hacia abajo dependerá de dónde se ubica la línea de encabezados, a la izquierda o a la derecha. Todos estos métodos se llamarán tanto al seleccionar un encabezado parcialmente oculto para su visualización completa como al pulsar los botones de scrolling (arriba, abajo, izquierda y derecha). Para entender qué botón y en qué control pulsamos, utilizaremos el modelo de eventos: al pulsar un botón, se enviará un evento que la biblioteca captará y procesará, enviando el evento al manejador de eventos del elemento al que está vinculado el botón pulsado, para su posterior procesamiento dentro del control. Utilizaremos este modelo para trabajar en otros controles que tendrán elementos de control.

Por el momento, los nombres de los elementos gráficos -principal y básico- se enviarán al manejador de eventos en el parámetro sparam. El elemento principal será aquel al que está vinculado el objeto en el que se ha producido el evento, y aquí se considerará el elemento básico. Pero si este control básico es compuesto -algunos otros controles están vinculados a él (botones, por ejemplo)-, y un evento ya ha ocurrido en ellos, entonces no podremos encontrar el control básico porque no está directamente adjunto al objeto principal. Para evitar esto por ahora, haremos lo siguiente: transmitiremos en el parámetro sparam los nombres del objeto principal, el objeto básico (como estaba antes), más el nombre del objeto en el que se ha producido el evento.

De esta forma, tendremos datos de entrada sobre el objeto principal, sobre el objeto básico dentro del cual se ha producido un evento en alguno de los elementos vinculados a él, y el nombre de ese elemento en el que se ha producido el evento. Para saber exactamente de qué control se trataba al pulsar el botón (un caso especial), transmitiremos el tipo de este objeto subyacente en el parámetro dparam. Así, conociendo el tipo del objeto básico, obtendremos una lista con todos los controles con el tipo escrito en dparam en el objeto principal, y luego haremos un ciclo a través de todos esos objetos para encontrar el objeto vinculado con el nombre transmitido en último lugar a sparam, en el que se produjo el evento (pulsación en el control).

Por el momento, este esquema no parece muy sólido en cuanto a su versatilidad para trabajar con objetos más complejos, pero en este momento, un plan de este tipo bastará para continuar el desarrollo de la biblioteca. Y cuando creemos controles más complejos con un esquema más desarrollado de anidamiento de objetos entre sí, entonces tendremos un ejemplo práctico claro de cómo identificar correcta y universalmente los eventos en tales objetos en la biblioteca (recuerde el principio universal "de lo simple a lo complejo").

La biblioteca ahora permite ocultar y mostrar controles. Para mostrar u ocultar dichos elementos, solo tendremos que ocultar el elemento principal o básico, y todos los elementos adjuntos se ocultarán o mostrarán en consecuencia. Pero si lo pensamos un poco e imaginamos que debe haber objetos dentro del control que se muestren o visualicen independientemente del objeto principal (es decir, su visibilidad se establece dentro del propio control), y si tal objeto está oculto pero su elemento padre está visualizado, entonces tal objeto no deberá mostrarse. En tales situaciones, necesitaremos controlar la visibilidad de estos objetos desde su control subyacente y, para ello, deberemos introducir otra propiedad del elemento gráfico: su bandera de visualización. Entonces, cuando el objeto principal se haya ocultado y luego mostrado, el control para el que se ha reiniciado su bandera de visualización permanecerá oculto hasta que se muestre explícitamente desde su control básico.

Bien, basta de teoría, vayamos al grano...


Mejorando las clases de la biblioteca

Como hacemos esto pensando en el futuro, no vamos a crear nuestros propios identificadores de eventos para los controles de scrolling: los crearemos con vistas a futuros nuevos controles que también usarán botones (barra de desplazamiento, listas desplegables, etc.). Es decir, crearemos algunos eventos de clic genéricos en el control que gestiona el scrolling o la aparición de ventanas emergentes.

En el archivo \MQL5\Include\DoEasy\Defines.mqh, añadiremos a la lista de posibles eventos de los controles WinForms las nuevas constantes de enumeración:

//+------------------------------------------------------------------+
//| List of possible WinForms control events                         |
//+------------------------------------------------------------------+
enum ENUM_WF_CONTROL_EVENT
  {
   WF_CONTROL_EVENT_NO_EVENT = GRAPH_OBJ_EVENTS_NEXT_CODE,// No event
   WF_CONTROL_EVENT_CLICK,                            // "Click on the control" event
   WF_CONTROL_EVENT_CLICK_CANCEL,                     // "Canceling the click on the control" event
   WF_CONTROL_EVENT_TAB_SELECT,                       // "TabControl tab selection" event
   WF_CONTROL_EVENT_CLICK_SCROLL_LEFT,                // "Clicking the control left button" event
   WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT,               // "Clicking the control right button" event
   WF_CONTROL_EVENT_CLICK_SCROLL_UP,                  // "Clicking the control up button" event
   WF_CONTROL_EVENT_CLICK_SCROLL_DOWN,                // "Clicking the control down button" event
  };
#define WF_CONTROL_EVENTS_NEXT_CODE (WF_CONTROL_EVENT_TAB_SELECT+1)  // The code of the next event after the last graphical element event code
//+------------------------------------------------------------------+

Aquí hemos creado un identificador de evento para el evento de cancelación de clic (en algunos casos es posible procesar este evento) por si acaso, y hemos añadido eventos comunes para los controles que tienen botones que gestionan la apariencia del control o interactúan con él.

En la enumeración de propiedades enteras del elemento gráfico, en el lienzo, añadimos la nueva propiedad y aumentamos el número total de propiedades de 96 a 97:

//+------------------------------------------------------------------+
//| Integer properties of the graphical element on the canvas        |
//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_PROP_INTEGER
  {
   CANV_ELEMENT_PROP_ID = 0,                          // Element ID
   CANV_ELEMENT_PROP_TYPE,                            // Graphical element type

   //---...
   //---...

   CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH,              // Visibility scope width
   CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,             // Visibility scope height
   CANV_ELEMENT_PROP_DISPLAYED,                       // Non-hidden control display flag
   CANV_ELEMENT_PROP_GROUP,                           // Group the graphical element belongs to
   CANV_ELEMENT_PROP_ZORDER,                          // Priority of a graphical object for receiving the event of clicking on a chart

   //---...
   //---...

   CANV_ELEMENT_PROP_TAB_PAGE_COLUMN,                 // Tab column index
   CANV_ELEMENT_PROP_ALIGNMENT,                       // Location of an object inside the control
   
  };
#define CANV_ELEMENT_PROP_INTEGER_TOTAL (97)          // Total number of integer properties
#define CANV_ELEMENT_PROP_INTEGER_SKIP  (0)           // Number of integer properties not used in sorting
//+------------------------------------------------------------------+

La bandera para mostrar un control que no está oculto indica que si el control no está oculto, pero esta bandera está desactivada(false), entonces este control no se mostrará. Es decir, si mostramos el control principal, sus descendientes, para los que esta bandera está desactivada, seguirán ocultos hasta que se muestren forzosamente estableciendo esta bandera en true y llamando al método Show().


Asimismo, añadiremos las nuevas propiedades a la lista de posibles criterios para clasificar los elementos gráficos en el lienzo:

//+------------------------------------------------------------------+
//| Possible sorting criteria of graphical elements on the canvas    |
//+------------------------------------------------------------------+
#define FIRST_CANV_ELEMENT_DBL_PROP  (CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_INTEGER_SKIP)
#define FIRST_CANV_ELEMENT_STR_PROP  (CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_INTEGER_SKIP+CANV_ELEMENT_PROP_DOUBLE_TOTAL-CANV_ELEMENT_PROP_DOUBLE_SKIP)
enum ENUM_SORT_CANV_ELEMENT_MODE
  {
//--- Sort by integer properties
   SORT_BY_CANV_ELEMENT_ID = 0,                       // Sort by element ID
   SORT_BY_CANV_ELEMENT_TYPE,                         // Sort by graphical element type

   //---...
   //---...

   SORT_BY_CANV_ELEMENT_VISIBLE_AREA_WIDTH,           // Sort by visibility scope width
   SORT_BY_CANV_ELEMENT_VISIBLE_AREA_HEIGHT,          // Sort by visibility scope height
   SORT_BY_CANV_ELEMENT_DISPLAYED,                    // Sort by non-hidden control display flag
   SORT_BY_CANV_ELEMENT_GROUP,                        // Sort by a group the graphical element belongs to
   SORT_BY_CANV_ELEMENT_ZORDER,                       // Sort by the priority of a graphical object for receiving the event of clicking on a chart

   //---...
   //---...

   SORT_BY_CANV_ELEMENT_TAB_PAGE_COLUMN,              // Sort by tab column index
   SORT_BY_CANV_ELEMENT_ALIGNMENT,                    // Sort by the location of the object inside the control
//--- Sort by real properties

//--- Sort by string properties
   SORT_BY_CANV_ELEMENT_NAME_OBJ = FIRST_CANV_ELEMENT_STR_PROP,// Sort by an element object name
   SORT_BY_CANV_ELEMENT_NAME_RES,                     // Sort by the graphical resource name
   SORT_BY_CANV_ELEMENT_TEXT,                         // Sort by graphical element text
   SORT_BY_CANV_ELEMENT_DESCRIPTION,                  // Sort by graphical element description
  };
//+------------------------------------------------------------------+

Ahora podremos seleccionar, clasificar y filtrar listas de elementos gráficos utilizando esta nueva propiedad.


En el archivo \MQL5\Include\DoEasy\Data.mqh, añadiremos los índices de los nuevos mensajes a la biblioteca:

   MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY,                 // Request outside the array
   MSG_LIB_SYS_FAILED_CONV_GRAPH_OBJ_COORDS_TO_XY,    // Failed to convert graphical object coordinates to screen ones
   MSG_LIB_SYS_FAILED_CONV_TIMEPRICE_COORDS_TO_XY,    // Failed to convert time/price coordinates to screen ones
   MSG_LIB_SYS_FAILED_ENQUEUE_EVENT,                  // Failed to put the event in the chart event queue

...

   MSG_CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH,          // Visibility scope width
   MSG_CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,         // Visibility scope height
   MSG_CANV_ELEMENT_PROP_DISPLAYED,                   // Non-hidden control display flag
   MSG_CANV_ELEMENT_PROP_ENABLED,                     // Element availability flag
   MSG_CANV_ELEMENT_PROP_FORE_COLOR,                  // Default text color for all control objects

y los textos de los mensajes correspondientes a los nuevos índices añadidos:

   {"Запрос за пределами массива","Data requested outside the array"},
   {"Не удалось преобразовать координаты графического объекта в экранные","Failed to convert graphics object coordinates to screen coordinates"},
   {"Не удалось преобразовать координаты время/цена в экранные","Failed to convert time/price coordinates to screen coordinates"},
   {"Не удалось поставить событие в очередь событий графика","Failed to put event in chart event queue"},

...

   {"Ширина области видимости","Width of object visibility area"},
   {"Высота области видимости","Height of object visibility area"},
   {"Флаг отображения не скрытого элемента управления","Flag that sets the display of a non-hidden control"},
   {"Флаг доступности элемента","Element Availability Flag"},
   {"Цвет текста по умолчанию для всех объектов элемента управления","Default text color for all objects in the control"},


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

Para ello, añadiremos a la nueva estructura del objeto la nueva propiedad nuevamente añadida:

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
     {         
      //--- Object integer properties
      int            id;                                       // Element ID
      int            type;                                     // Graphical element type
               
      //---... 
      //---... 
               
      int            visible_area_w;                           // Visibility scope width
      int            visible_area_h;                           // Visibility scope height
      bool           displayed;                                // Non-hidden control display flag
      //--- Object real properties
               
      //--- Object string properties
      uchar          name_obj[64];                             // Graphical element object name
      uchar          name_res[64];                             // Graphical resource name
      uchar          text[256];                                // Graphical element text
      uchar          descript[256];                            // Graphical element description
     };        
   SData             m_struct_obj;                             // Object structure
   uchar             m_uchar_array[];                          // uchar array of the object structure

Todas las propiedades del objeto se escriben en la estructura del objeto para guardar este en un archivo y luego recuperar el objeto del mismo.

En el bloque con los métodos simplificados de acceso a las propiedades de los objetos, escribiremos dos nuevos métodos para establecer y recuperar la propiedad de representación de objetos:

//+------------------------------------------------------------------+
//| Methods of simplified access to object properties                |
//+------------------------------------------------------------------+
//--- Set the (1) X, (2) Y coordinates, (3) element width, (4) height, (5) right (6) and bottom edge,
   virtual bool      SetCoordX(const int coord_x);
   virtual bool      SetCoordY(const int coord_y);
   virtual bool      SetWidth(const int width);
   virtual bool      SetHeight(const int height);
   void              SetRightEdge(void)                        { this.SetProperty(CANV_ELEMENT_PROP_RIGHT,this.RightEdge());           }
   void              SetBottomEdge(void)                       { this.SetProperty(CANV_ELEMENT_PROP_BOTTOM,this.BottomEdge());         }
//--- Set the shift of the (1) left, (2) top, (3) right, (4) bottom edge of the active area relative to the element,
//--- (5) all shifts of the active area edges relative to the element, (6) opacity
   void              SetActiveAreaLeftShift(const int value)   { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_LEFT,fabs(value));       }
   void              SetActiveAreaRightShift(const int value)  { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT,fabs(value));      }
   void              SetActiveAreaTopShift(const int value)    { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_TOP,fabs(value));        }
   void              SetActiveAreaBottomShift(const int value) { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM,fabs(value));     }
   void              SetActiveAreaShift(const int left_shift,const int bottom_shift,const int right_shift,const int top_shift);
   void              SetOpacity(const uchar value,const bool redraw=false);
   
//--- (1) Set and (2) return the flag for displaying a non-hidden control
   void              SetDisplayed(const bool flag)             { this.SetProperty(CANV_ELEMENT_PROP_DISPLAYED,flag);                   }
   bool              Displayed(void)                           { return (bool)this.GetProperty(CANV_ELEMENT_PROP_DISPLAYED);           }

//--- (1) Set and (2) return the graphical element type
   void              SetTypeElement(const ENUM_GRAPH_ELEMENT_TYPE type)
                       {
                        CGBaseObj::SetTypeElement(type);
                        this.SetProperty(CANV_ELEMENT_PROP_TYPE,type);
                       }
   ENUM_GRAPH_ELEMENT_TYPE TypeGraphElement(void)  const { return (ENUM_GRAPH_ELEMENT_TYPE)this.GetProperty(CANV_ELEMENT_PROP_TYPE);   }

Los métodos simplemente escriben la bandera transmitida en la propiedad del objeto y retornan el valor escrito en la propiedad del objeto.


En ambos constructores de la clase, escribiremos un valor por defecto en la nueva propiedad:

//+------------------------------------------------------------------+
//| 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   descript,
                           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.SetTypeElement(element_type);
   this.m_type=OBJECT_DE_TYPE_GELEMENT; 
   this.m_element_main=NULL;
   this.m_element_base=NULL;
   this.m_chart_color_bg=(color)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_COLOR_BACKGROUND);
   this.m_name=this.CreateNameGraphElement(element_type);
   this.m_chart_id=(chart_id==NULL || chart_id==0 ? ::ChartID() : chart_id);
   this.m_subwindow=wnd_num;
   this.SetFont(DEF_FONT,DEF_FONT_SIZE);
   this.m_text_anchor=0;
   this.m_text_x=0;
   this.m_text_y=0;
   this.SetBackgroundColor(colour,true);
   this.SetOpacity(opacity);
   this.m_shift_coord_x=0;
   this.m_shift_coord_y=0;
   if(::ArrayResize(this.m_array_colors_bg,1)==1)
      this.m_array_colors_bg[0]=this.BackgroundColor();
   if(::ArrayResize(this.m_array_colors_bg_dwn,1)==1)
      this.m_array_colors_bg_dwn[0]=this.BackgroundColor();
   if(::ArrayResize(this.m_array_colors_bg_ovr,1)==1)
      this.m_array_colors_bg_ovr[0]=this.BackgroundColor();
   if(this.Create(chart_id,wnd_num,x,y,w,h,redraw))
     {
      this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,this.m_canvas.ResourceName()); // Graphical resource name
      this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,CGBaseObj::ChartID());         // Chart ID

      //---...
      //---...

      this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH,w);                  // Visibility scope width
      this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,h);                 // Visibility scope height
      this.SetProperty(CANV_ELEMENT_PROP_DISPLAYED,true);                        // Non-hidden control display flag
      //---
      this.SetProperty(CANV_ELEMENT_PROP_BELONG,ENUM_GRAPH_OBJ_BELONG::GRAPH_OBJ_BELONG_PROGRAM);  // Graphical element affiliation
      this.SetProperty(CANV_ELEMENT_PROP_ZORDER,0);                              // Priority of a graphical object for receiving the event of clicking on a chart
      this.SetProperty(CANV_ELEMENT_PROP_BOLD_TYPE,FW_NORMAL);                   // Font width type

      //---...
      //---...

      this.SetProperty(CANV_ELEMENT_PROP_TEXT,"");                                                    // Graphical element text
      this.SetProperty(CANV_ELEMENT_PROP_DESCRIPTION,descript);                                       // Graphical element description
      this.SetVisibleFlag(false,false);
     }
   else
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),"\"",this.TypeElementDescription(element_type),"\" ",this.NameObj());
     }
  }
//+------------------------------------------------------------------+
//| Protected constructor                                            |
//+------------------------------------------------------------------+
CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                           const long    chart_id,
                           const int     wnd_num,
                           const string  descript,
                           const int     x,
                           const int     y,
                           const int     w,
                           const int     h) : m_shadow(false)
  {
   this.m_type=OBJECT_DE_TYPE_GELEMENT; 
   this.m_element_main=NULL;
   this.m_element_base=NULL;
   this.m_chart_color_bg=(color)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_COLOR_BACKGROUND);
   this.m_name=this.CreateNameGraphElement(element_type);
   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.SetBackgroundColor(CLR_CANV_NULL,true);
   this.SetOpacity(0);
   this.m_shift_coord_x=0;
   this.m_shift_coord_y=0;
   if(::ArrayResize(this.m_array_colors_bg,1)==1)
      this.m_array_colors_bg[0]=this.BackgroundColor();
   if(::ArrayResize(this.m_array_colors_bg_dwn,1)==1)
      this.m_array_colors_bg_dwn[0]=this.BackgroundColor();
   if(::ArrayResize(this.m_array_colors_bg_ovr,1)==1)
      this.m_array_colors_bg_ovr[0]=this.BackgroundColor();
   if(this.Create(chart_id,wnd_num,x,y,w,h,false))
     {
      this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,this.m_canvas.ResourceName()); // Graphical resource name
      this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,CGBaseObj::ChartID());         // Chart ID

      //---...
      //---...

      this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH,w);                  // Visibility scope width
      this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,h);                 // Visibility scope height
      this.SetProperty(CANV_ELEMENT_PROP_DISPLAYED,true);                        // Non-hidden control display flag
      //---
      this.SetProperty(CANV_ELEMENT_PROP_BELONG,ENUM_GRAPH_OBJ_BELONG::GRAPH_OBJ_BELONG_PROGRAM);  // Graphical element affiliation
      this.SetProperty(CANV_ELEMENT_PROP_ZORDER,0);                              // Priority of a graphical object for receiving the event of clicking on a chart
      this.SetProperty(CANV_ELEMENT_PROP_BOLD_TYPE,FW_NORMAL);                   // Font width type

      //---...
      //---...

      this.SetProperty(CANV_ELEMENT_PROP_TEXT,"");                                                    // Graphical element text
      this.SetProperty(CANV_ELEMENT_PROP_DESCRIPTION,descript);                                       // Graphical element description
      this.SetVisibleFlag(false,false);
     }
   else
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),"\"",this.TypeElementDescription(element_type),"\" ",this.NameObj());
     }
  }
//+------------------------------------------------------------------+

De forma predeterminada, el objeto está configurado para ser visible y se muestra al activarse la visibilidad del objeto básico o principal. Para establecer para el objeto el modo de control manual de su visibilidad, el valor de la bandera deberá ser false. En este caso, si el objeto básico o maestro se ha ocultado y luego mostrado, este objeto actual seguirá oculto, y para mostrarlo, necesitaremos establecer la propiedad Displayed en true y llamar nosotros mismos al método Show() de ese objeto.


En el método que crea la estructura del objeto, escribiremos el valor de la propiedad del objeto en el campo correspondiente de la estructura:

//+------------------------------------------------------------------+
//| Create the object structure                                      |
//+------------------------------------------------------------------+
bool CGCnvElement::ObjectToStruct(void)
  {
//--- Save integer properties
   this.m_struct_obj.id=(int)this.GetProperty(CANV_ELEMENT_PROP_ID);                               // Element ID
   this.m_struct_obj.type=(int)this.GetProperty(CANV_ELEMENT_PROP_TYPE);                           // Graphical element type
   //---...
   //---...
   this.m_struct_obj.belong=(int)this.GetProperty(CANV_ELEMENT_PROP_BELONG);                       // Graphical element affiliation
   this.m_struct_obj.number=(int)this.GetProperty(CANV_ELEMENT_PROP_NUM);                          // Element ID in the list
   //---...
   //---...
   this.m_struct_obj.visible_area_x=(int)this.GetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_X);       // Visibility scope X coordinate
   this.m_struct_obj.visible_area_y=(int)this.GetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_Y);       // Visibility scope Y coordinate
   //---...
   //---...
   this.m_struct_obj.visible_area_w=(int)this.GetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH);   // Visibility scope width
   this.m_struct_obj.visible_area_h=(int)this.GetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT);  // Visibility scope height
   this.m_struct_obj.displayed=(bool)this.GetProperty(CANV_ELEMENT_PROP_DISPLAYED);                // Flag for displaying a non-hidden control
   this.m_struct_obj.zorder=this.GetProperty(CANV_ELEMENT_PROP_ZORDER);                            // Priority of a graphical object for receiving the on-chart mouse click event
   this.m_struct_obj.enabled=(bool)this.GetProperty(CANV_ELEMENT_PROP_ENABLED);                    // Element availability flag
   this.m_struct_obj.fore_color=(color)this.GetProperty(CANV_ELEMENT_PROP_FORE_COLOR);             // Default text color for all control objects
   //---...
   //---...
   this.m_struct_obj.fore_color_opacity=(uchar)this.GetProperty(CANV_ELEMENT_PROP_FORE_COLOR_OPACITY); // Opacity of the default text color for all control objects
   this.m_struct_obj.background_color=(color)this.GetProperty(CANV_ELEMENT_PROP_BACKGROUND_COLOR); // Element background color

   this.m_struct_obj.tab_alignment=(int)this.GetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT);                              // Location of tabs inside the control
   this.m_struct_obj.alignment=(int)this.GetProperty(CANV_ELEMENT_PROP_ALIGNMENT);                                      // Location of an object inside the control
//--- Save real properties

//--- Save string properties
   ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_NAME_OBJ),this.m_struct_obj.name_obj);   // Graphical element object name
   ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_NAME_RES),this.m_struct_obj.name_res);   // Graphical resource name
   ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_TEXT),this.m_struct_obj.text);           // Graphical element text
   ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_DESCRIPTION),this.m_struct_obj.descript);// Graphical element description
   //--- Save the structure to the uchar array
   ::ResetLastError();
   if(!::StructToCharArray(this.m_struct_obj,this.m_uchar_array))
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_SAVE_OBJ_STRUCT_TO_UARRAY,true);
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+


En el método que crea un objeto a partir de una estructura, estableceremos el valor de la nueva propiedad del objeto a partir del campo correspondiente de la estructura:

//+------------------------------------------------------------------+
//| Create the object from the structure                             |
//+------------------------------------------------------------------+
void CGCnvElement::StructToObject(void)
  {
//--- Save integer properties
   this.SetProperty(CANV_ELEMENT_PROP_ID,this.m_struct_obj.id);                                    // Element ID
   this.SetProperty(CANV_ELEMENT_PROP_TYPE,this.m_struct_obj.type);                                // Graphical element type
   //---...
   //---...
   this.SetProperty(CANV_ELEMENT_PROP_BELONG,this.m_struct_obj.belong);                            // Graphical element affiliation
   this.SetProperty(CANV_ELEMENT_PROP_NUM,this.m_struct_obj.number);                               // Element index in the list
   //---...
   //---...
   this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,this.m_struct_obj.visible_area_h);       // Visibility scope height
   this.SetProperty(CANV_ELEMENT_PROP_DISPLAYED,this.m_struct_obj.displayed);                      // Non-hidden control display flag
   this.SetProperty(CANV_ELEMENT_PROP_ZORDER,this.m_struct_obj.zorder);                            // Priority of a graphical object for receiving the event of clicking on a chart
   this.SetProperty(CANV_ELEMENT_PROP_ENABLED,this.m_struct_obj.enabled);                          // Element availability flag
   //---...
   //---...
   this.SetProperty(CANV_ELEMENT_PROP_FORE_COLOR,this.m_struct_obj.fore_color);                    // Default text color for all control objects
   this.SetProperty(CANV_ELEMENT_PROP_FORE_COLOR_OPACITY,this.m_struct_obj.fore_color_opacity);    // Opacity of the default text color for all control objects

   this.SetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT,this.m_struct_obj.tab_alignment);                             // Location of tabs inside the control
   this.SetProperty(CANV_ELEMENT_PROP_ALIGNMENT,this.m_struct_obj.alignment);                                     // Location of an object inside the control
//--- Save real properties

//--- Save string properties
   this.SetProperty(CANV_ELEMENT_PROP_NAME_OBJ,::CharArrayToString(this.m_struct_obj.name_obj));   // Graphical element object name
   this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,::CharArrayToString(this.m_struct_obj.name_res));   // Graphical resource name
   this.SetProperty(CANV_ELEMENT_PROP_TEXT,::CharArrayToString(this.m_struct_obj.text));           // Graphical element text
   this.SetProperty(CANV_ELEMENT_PROP_DESCRIPTION,::CharArrayToString(this.m_struct_obj.descript));// Graphical element description
  }
//+------------------------------------------------------------------+


En la clase objeto básico WinForms, en el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh, en el método que retorna la descripción de una propiedad entera del elemento, añadiremos el bloque de código para retornar la descripción de una nueva propiedad del objeto:

//+------------------------------------------------------------------+
//| Return the description of the control integer property           |
//+------------------------------------------------------------------+
string CWinFormBase::GetPropertyDescription(ENUM_CANV_ELEMENT_PROP_INTEGER property,bool only_prop=false)
  {
   return
     (
      property==CANV_ELEMENT_PROP_ID                           ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_ID)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :

      //---...
      //---...

      property==CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT          ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==CANV_ELEMENT_PROP_DISPLAYED                    ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_DISPLAYED)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==CANV_ELEMENT_PROP_GROUP                        ?  CMessage::Text(MSG_GRAPH_OBJ_PROP_GROUP)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :

      //---...
      //---...

      property==CANV_ELEMENT_PROP_ALIGNMENT                    ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_ALIGNMENT)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+AlignmentDescription((ENUM_CANV_ELEMENT_ALIGNMENT)this.GetProperty(property))
         )  :
      ""
     );
  }
//+------------------------------------------------------------------+

Si esta propiedad es transmitida al método, entonces dependiendo de la bandera only_prop, se generará un mensaje de texto, ya sea solo el nombre de la propiedad (only_prop = true), o bien junto con el valor establecido a la propiedad (only_prop = false).


Todos los controles utilizarán la funcionalidad de los eventos de una forma u otra, tanto para su propio uso interno como para notificar al programa de eventos en su GUI. La clase principal para la interacción con el usuario es la clase objeto de formulario, que implementa la funcionalidad de interacción con el ratón, y también hereda la clase básica de todos los objetos de la biblioteca WinForms. Precisamente en ella crearemos un método para enviar mensajes de elementos gráficos.

En el archivo de clase objeto \MQL5\Include\DoEasy\Objects\Graph\Form.mqh, en su sección protegida, declararemos el método para enviar mensajes.
El método será virtual, en caso de que necesite ser sobrescrito para cualquier objeto heredero:

//--- 'The cursor is inside the window scrolling area, a mouse button is clicked (any)' event handler
   virtual void      MouseScrollAreaPressedHandler(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- 'The cursor is inside the window scrolling area, the mouse wheel is being scrolled' event handler
   virtual void      MouseScrollAreaWhellHandler(const int id,const long& lparam,const double& dparam,const string& sparam);

//--- Send a message about the event
   virtual bool      SendEvent(const long chart_id,const ushort event_id);

public:


En el método que muestra el formulario, escribiremos una comprobación para la bandera de visualización del objeto (comprobación de una nueva propiedad del elemento gráfico):

//+------------------------------------------------------------------+
//| Show the form                                                    |
//+------------------------------------------------------------------+
void CForm::Show(void)
  {
//--- If the element should not be displayed (hidden inside another control), leave
   if(!this.Displayed())
      return;
//--- If the object has a shadow, display it
   if(this.m_shadow_obj!=NULL)
      this.m_shadow_obj.Show();
//--- Display the main form
   CGCnvElement::Show();
//--- In the loop by all bound graphical objects,
   for(int i=0;i<this.m_list_elements.Total();i++)
     {
      //--- get the next graphical element
      CGCnvElement *element=this.m_list_elements.At(i);
      if(element==NULL)
         continue;
      //--- and display it
      element.Show();
     }
//--- Update the form
   CGCnvElement::Update();
  }
//+------------------------------------------------------------------+

Al llamar a este método para cualquier objeto de tipo CForm o superior, primero comprobaremos la bandera de visualización del objeto y, si la bandera no está activada (el control de visibilidad manual está activado), saldremos inmediatamente del método.


Fuera del cuerpo de la clase, escribiremos una implementación del método que envía el mensaje de evento:

//+------------------------------------------------------------------+
//| Send a message about the event                                   |
//+------------------------------------------------------------------+
bool CForm::SendEvent(const long chart_id,const ushort event_id)
  {
   //--- Create the event:
   //--- Get the base and main objects
   CGCnvElement *base=this.GetBase();
   CGCnvElement *main=this.GetMain();
   //--- find the names of the main and base objects
   string name_main=(main!=NULL ? main.Name() : this.IsMain() ? this.Name() : "Lost name of object");
   string name_base=(base!=NULL ? base.Name() : "Lost name of object");
   ENUM_GRAPH_ELEMENT_TYPE base_base_type=(base!=NULL ? base.GetBase().TypeGraphElement() : this.TypeGraphElement());
   //--- pass the object ID in the event 'long' parameter
   //--- pass the object type in the event 'double' parameter
   //--- in the event 'string' parameter, pass the names of the main, base and current objects separated by ";"
   long lp=this.ID();
   double dp=base_base_type;
   string sp=::StringSubstr(name_main,::StringLen(this.NamePrefix()))+";"+
             ::StringSubstr(name_base,::StringLen(this.NamePrefix()))+";"+
             ::StringSubstr(this.Name(),::StringLen(this.NamePrefix()));
   //--- Send the event of clicking on the control to the control program chart
   bool res=true;
   ::ResetLastError();
   res=::EventChartCustom(chart_id,event_id,lp,dp,sp);
   if(res)
      return true;
   ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_ENQUEUE_EVENT),". ",CMessage::Text(MSG_LIB_SYS_ERROR),": ",CMessage::Text(::GetLastError()));
   return false;
  }
//+------------------------------------------------------------------+

Aquí obtendremos los punteros a los objetos principal y básico y también obtendremos sus nombres. Si el puntero al objeto principal es NULL, probablemente sea el objeto principal. Para ello, comprobaremos si es así y, en caso afirmativo, usaremos el nombre del objeto actual. Si el puntero no se obtiene por alguna razón, utilizaremos la cadena "Lost name of object" para el nombre, de forma que pueda ser monitoreado.

Luego deberemos averiguar el tipo de objeto básico al que está vinculado el objeto básico de este objeto (es decir, obtendremos del objeto básico su objeto básico, y de este obtendremos su tipo), y escribiremos todos los datos obtenidos en las variables enviadas en el mensaje de evento. En lparam enviaremos el identificador del objeto actual, en dparam el tipo de objeto básico al que está vinculado el objeto básico del objeto actual, y en sparam enviaremos una línea con los nombres de los tres objetos separados por ";" - el objeto principal, el objeto básico y el objeto actual. Al recibir un evento, podremos usar estos datos para determinar exactamente de qué objeto procede el mensaje de evento.

Por ahora, esta lógica bastará para identificar el objeto que ha generado el evento, pero la modificaremos más adelante, ya que no nos permite monitorear el anidamiento completo de los objetos entre sí al crear controles más complejos con una jerarquía más profunda de anidamiento mutuo.

Ahora vamos a añadir a los manejadores de eventos de los objetos WinForms el envío de mensajes sobre eventos.

En el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Button.mqh, en el manejador del evento "Cursor dentro del área activa, botón (izquierdo) del ratón pulsado", escribiremos un mensaje sobre el evento:

//+------------------------------------------------------------------+
//| 'The cursor is inside the active area,                           |
//| left mouse button released                                       |
//+------------------------------------------------------------------+
void CButton::MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- The mouse button released outside the element means refusal to interact with the element
   if(lparam<this.CoordX() || lparam>this.RightEdge() || dparam<this.CoordY() || dparam>this.BottomEdge())
     {
      //--- If this is a simple button, set the initial background and text color
      if(!this.Toggle())
        {
         this.SetBackgroundColor(this.BackgroundColorInit(),false);
         this.SetForeColor(this.ForeColorInit(),false);
        }
      //--- If this is the toggle button, set the initial background and text color depending on whether the button is pressed or not
      else
        {
         this.SetBackgroundColor(!this.State() ? this.BackgroundColorInit() : this.BackgroundStateOnColorInit(),false);
         this.SetForeColor(!this.State() ? this.ForeColorInit() : this.ForeStateOnColorInit(),false);
        }
      //--- Set the initial frame color
      this.SetBorderColor(this.BorderColorInit(),false);
      //--- Send the event:
      this.SendEvent(::ChartID(),WF_CONTROL_EVENT_CLICK_CANCEL);
      //--- Send the test message to the journal
      Print(DFUN_ERR_LINE,TextByLanguage("Отмена","Cancel"));
     }
//--- The mouse button released within the element means a  click on the control
   else
     {
      //--- If this is a simple button, set the background and text color for "The cursor is over the active area" status
      if(!this.Toggle())
        {
         this.SetBackgroundColor(this.BackgroundColorMouseOver(),false);
         this.SetForeColor(this.ForeColorMouseOver(),false);
        }
      //--- If this is the toggle button,
      else
        {
         //--- if the button does not work in the group, set its state to the opposite,
         if(!this.GroupButtonFlag())
            this.SetState(!this.State());
         //--- if the button is not pressed yet, set it to the pressed state
         else if(!this.State())
            this.SetState(true);
         //--- set the background and text color for "The cursor is over the active area" status depending on whether the button is clicked or not
         this.SetBackgroundColor(this.State() ? this.BackgroundStateOnColorMouseOver() : this.BackgroundColorMouseOver(),false);
         this.SetForeColor(this.State() ? this.ForeStateOnColorMouseOver() : this.ForeColorMouseOver(),false);
        }
      
      //--- Send the event:
      this.SendEvent(::ChartID(),WF_CONTROL_EVENT_CLICK);
      //--- Send the test message to the journal
      Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.State()=",this.State(),", ID=",this.ID(),", Group=",this.Group());
      //--- Set the frame color for "The cursor is over the active area" status
      this.SetBorderColor(this.BorderColorMouseOver(),false);
     }
//--- Redraw the object
   this.Redraw(false);
  }
//+------------------------------------------------------------------+


En el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\RadioButton.mqh:

//+------------------------------------------------------------------+
//| 'The cursor is inside the active area,                           |
//| left mouse button released                                       |
//+------------------------------------------------------------------+
void CRadioButton::MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- The mouse button released outside the element means refusal to interact with the element
   if(lparam<this.CoordX() || lparam>this.RightEdge() || dparam<this.CoordY() || dparam>this.BottomEdge())
     {
      this.SetCheckBackgroundColor(this.BackgroundColorInit(),false);
      this.SetCheckBorderColor(this.CheckBorderColorInit(),false);
      this.SetCheckFlagColor(this.CheckFlagColorInit(),false);
      //--- Send the event:
      this.SendEvent(::ChartID(),WF_CONTROL_EVENT_CLICK_CANCEL);
      //--- Send a test entry to the journal
      Print(DFUN_ERR_LINE,TextByLanguage("Отмена","Cancel"));
     }
//--- The mouse button released within the element means a  click on the control
   else
     {
      this.SetCheckBackgroundColor(this.CheckBackgroundColorMouseOver(),false);
      this.SetCheckBorderColor(this.CheckBorderColorMouseOver(),false);
      this.SetCheckFlagColor(this.CheckFlagColorInit(),false);
      if(!this.Checked())
         this.SetChecked(true);
      //--- Send the event:
      this.SendEvent(::ChartID(),WF_CONTROL_EVENT_CLICK);
      //--- Send a test entry to the journal
      Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.Checked()=",this.Checked(),", ID=",this.ID(),", Group=",this.Group());
     }
   this.Redraw(false);
  }
//+------------------------------------------------------------------+


En el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\CheckBox.mqh:

//+------------------------------------------------------------------+
//| 'The cursor is inside the active area,                           |
//| left mouse button released                                       |
//+------------------------------------------------------------------+
void CCheckBox::MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- The mouse button released outside the element means refusal to interact with the element
   if(lparam<this.CoordX() || lparam>this.RightEdge() || dparam<this.CoordY() || dparam>this.BottomEdge())
     {
      this.SetCheckBackgroundColor(this.CheckBackgroundColorInit(),false);
      this.SetCheckBorderColor(this.CheckBorderColorInit(),false);
      this.SetCheckFlagColor(this.CheckFlagColorInit(),false);
      this.SetBackgroundColor(this.BackgroundColorInit(),false);
      //--- Send the event:
      this.SendEvent(::ChartID(),WF_CONTROL_EVENT_CLICK_CANCEL);
      //--- Send a test entry to the journal
      Print(DFUN_ERR_LINE,TextByLanguage("Отмена","Cancel"));
     }
//--- The mouse button released within the element means a  click on the control
   else
     {
      this.SetCheckBackgroundColor(this.CheckBackgroundColorMouseOver(),false);
      this.SetCheckBorderColor(this.CheckBorderColorMouseOver(),false);
      this.SetCheckFlagColor(this.CheckFlagColorInit(),false);
      this.SetBackgroundColor(this.BackgroundColorMouseOver(),false);
      this.SetChecked(!this.Checked());
      //--- Send the event:
      this.SendEvent(::ChartID(),WF_CONTROL_EVENT_CLICK);
      //--- Send a test entry to the journal
      Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.Checked()=",this.Checked(),", ID=",this.ID());
     }
   this.Redraw(false);
  }
//+------------------------------------------------------------------+


Ya después de publicar el último artículo, reparamos en que los dos archivos de clase para las flechas izquierda-derecha y arriba-abajo dejaron de compilarse solos. Solo se pueden compilar como parte de una biblioteca, al compilar el archivo principal de la biblioteca Engine.mqh, pero no por sí solos, y esto no está bien. Para rectificar dicho defecto, deberemos cambiar la lista de archivos de inclusión en los archivos de estos objetos.
Antes, teníamos un archivo de objeto de panel conectado:

//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "..\Containers\Panel.mqh"
//+------------------------------------------------------------------+

Ahora añadiremos solo aquellos archivos que deben ser utilizados por estas clases.

Para el archivo del objeto de botón con flechas arriba-abajo \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ArrowUpDownBox.mqh:

//+------------------------------------------------------------------+
//|                                               ArrowUpDownBox.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "..\Containers\Container.mqh"
#include "..\Helpers\ArrowUpButton.mqh"
#include "..\Helpers\ArrowDownButton.mqh"
//+------------------------------------------------------------------+


Para el archivo del objeto de botón con flechas izquierda-derecha \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ArrowLeftRightBox.mqh:

//+------------------------------------------------------------------+
//|                                            ArrowLeftRightBox.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "..\Containers\Container.mqh"
#include "..\Helpers\ArrowLeftButton.mqh"
#include "..\Helpers\ArrowRightButton.mqh"
//+------------------------------------------------------------------+

Ambos archivos se compilarán ahora con normalidad, ya sea por sí mismos o como parte de una biblioteca.


Vamos a mejorar la clase del objeto de encabezado de la pestaña del control TabControl en el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\TabHeader.mqh.

Del manejador del evento "Cursor dentro del área activa, botón (izquierdo) del ratón pulsado", eliminaremos el bloque de código para crear el evento:

      //--- Send the test message to the journal
      Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.State()=",this.State(),", ID=",this.ID(),", Group=",this.Group());
      //--- Create the event:
      //--- Get the base and main objects
      CWinFormBase *base=this.GetBase();
      CWinFormBase *main=this.GetMain();
      //--- in the 'long' event parameter, pass a string, while in the 'double' parameter, the tab header location column
      long lp=this.Row();
      double dp=this.Column();
      //--- in the 'string' parameter of the event, pass the names of the main and base objects separated by ";"
      string name_main=(main!=NULL ? main.Name() : "");
      string name_base=(base!=NULL ? base.Name() : "");
      string sp=name_main+";"+name_base;
      //--- Send the tab selection event to the chart of the control program
      ::EventChartCustom(::ChartID(),WF_CONTROL_EVENT_TAB_SELECT,lp,dp,sp);
      //--- Set the frame color for "The cursor is over the active area" status
      this.SetBorderColor(this.BorderColorMouseOver(),false);
     }
  }
//+------------------------------------------------------------------+

Ahora tenemos un método para crear y enviar el evento, que es lo que vamos a utilizar:

//+------------------------------------------------------------------+
//| 'The cursor is inside the active area,                           |
//| left mouse button released                                       |
//+------------------------------------------------------------------+
void CTabHeader::MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- The mouse button released outside the element means refusal to interact with the element
   if(lparam<this.CoordX() || lparam>this.RightEdge() || dparam<this.CoordY() || dparam>this.BottomEdge())
     {
      //--- If this is a simple button, set the initial background and text color
      if(!this.Toggle())
        {
         this.SetBackgroundColor(this.BackgroundColorInit(),false);
         this.SetForeColor(this.ForeColorInit(),false);
        }
      //--- If this is the toggle button, set the initial background and text color depending on whether the button is pressed or not
      else
        {
         this.SetBackgroundColor(!this.State() ? this.BackgroundColorInit() : this.BackgroundStateOnColorInit(),false);
         this.SetForeColor(!this.State() ? this.ForeColorInit() : this.ForeStateOnColorInit(),false);
        }
      //--- Set the initial frame color
      this.SetBorderColor(this.BorderColorInit(),false);
      //--- Send the event:
      this.SendEvent(::ChartID(),WF_CONTROL_EVENT_CLICK_CANCEL);
      //--- Send the test message to the journal
      Print(DFUN_ERR_LINE,TextByLanguage("Отмена","Cancel"));
     }
//--- The mouse button released within the element means a  click on the control
   else
     {
      //--- If this is a simple button, set the background and text color for "The cursor is over the active area" status
      if(!this.Toggle())
        {
         this.SetBackgroundColor(this.BackgroundColorMouseOver(),false);
         this.SetForeColor(this.ForeColorMouseOver(),false);
        }
      //--- If this is the toggle button,
      else
        {
         //--- if the button does not work in the group, set its state to the opposite,
         if(!this.GroupButtonFlag())
            this.SetState(!this.State());
         //--- if the button is not pressed yet, set it to the pressed state
         else if(!this.State())
            this.SetState(true);
         //--- set the background and text color for "The cursor is over the active area" status depending on whether the button is clicked or not
         this.SetBackgroundColor(this.State() ? this.BackgroundStateOnColorMouseOver() : this.BackgroundColorMouseOver(),false);
         this.SetForeColor(this.State() ? this.ForeStateOnColorMouseOver() : this.ForeColorMouseOver(),false);
         
         //--- Get the field object corresponding to the header
         CWinFormBase *field=this.GetFieldObj();
         if(field!=NULL)
           {
            //--- Display the field, bring it to the front, draw a frame and crop the excess
            field.Show();
            field.BringToTop();
            field.DrawFrame();
            field.Crop();
           }
        }
      //--- Send the event:
      this.SendEvent(::ChartID(),WF_CONTROL_EVENT_TAB_SELECT);
      //--- Send the test message to the journal
      Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.State()=",this.State(),", ID=",this.ID(),", Group=",this.Group());
      //--- Set the frame color for "The cursor is over the active area" status
      this.SetBorderColor(this.BorderColorMouseOver(),false);
     }
//--- Redraw an object and a chart
   this.Redraw(true);
  }
//+------------------------------------------------------------------+


Al desplazar la línea de encabezados hacia la izquierda, por ejemplo, la línea de encabezados de la izquierda saldrá del control y será sustituida por la de su derecha. Como la coordenada inicial para la ubicación del encabezado está dos píxeles a la derecha del borde izquierdo del contenedor, el encabezado que se encuentre más allá del borde izquierdo permanecerá visible en la misma zona de dos píxeles, pues se recortará por los bordes del área del contenedor dentro de la cual los elementos deberían ser visibles.

Para ocultar esta parte delgada y visible del encabezado, deberemos ajustar ligeramente el tamaño de la zona contenedora donde se muestran los objetos adjuntos. También habrá que tener en cuenta si el encabezado está seleccionado o no, ya que un encabezado seleccionado aumenta de tamaño dos píxeles a cada lado, y para que no se recorte dos píxeles al seleccionarlo, deberemos tenerlo en cuenta. Es decir, necesitaremos cambiar dinámicamente el tamaño de la zona contenedora en la que los objetos resultan visibles, dependiendo de qué objeto esté en el borde. Si está seleccionado, no lo redimensionaremos, y si no está seleccionado, reduciremos en dos píxeles.

En el método que recorta la imagen contorneada por la zona de visibilidad rectangular calculada, añadiremos un ajuste al tamaño de la zona de visibilidad del contenedor y añadiremos el valor resultante a los bordes de la zona visible:

//+------------------------------------------------------------------+
//| Crop the image outlined by the calculated                        |
//| rectangular visibility scope                                     |
//+------------------------------------------------------------------+
void CTabHeader::Crop(void)
  {
//--- Get the pointer to the base object
   CGCnvElement *base=this.GetBase();
//--- If the object does not have a base object it is attached to, then there is no need to crop the hidden areas - leave
   if(base==NULL)
      return;
//--- Set the initial coordinates and size of the visibility scope to the entire object
   int vis_x=0;
   int vis_y=0;
   int vis_w=this.Width();
   int vis_h=this.Height();
//--- Set the size of the top, bottom, left and right areas that go beyond the container
   int crop_top=0;
   int crop_bottom=0;
   int crop_left=0;
   int crop_right=0;
//--- Get the additional size, by which to crop the titles when the arrow buttons are visible
   int add_size_lr=(this.IsVisibleLeftRightBox() ? this.m_arr_butt_lr_size : 0);
   int add_size_ud=(this.IsVisibleUpDownBox()    ? this.m_arr_butt_ud_size : 0);
   int dec_size_vis=(this.State() ? 0 : 2);
//--- Calculate the boundaries of the container area, inside which the object is fully visible
   int top=fmax(base.CoordY()+(int)base.GetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_TOP),base.CoordYVisibleArea())+dec_size_vis+(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT ? add_size_ud : 0);
   int bottom=fmin(base.BottomEdge()-(int)base.GetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_BOTTOM),base.BottomEdgeVisibleArea()+1)-dec_size_vis-(this.Alignment()==CANV_ELEMENT_ALIGNMENT_RIGHT ? add_size_ud : 0);
   int left=fmax(base.CoordX()+(int)base.GetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_LEFT),base.CoordXVisibleArea())+dec_size_vis;
   int right=fmin(base.RightEdge()-(int)base.GetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_RIGHT),base.RightEdgeVisibleArea()+1)-add_size_lr;
//--- Calculate the values of the top, bottom, left and right areas, at which the object goes beyond
//--- the boundaries of the container area, inside which the object is fully visible
   crop_top=this.CoordY()-top;
   if(crop_top<0)
      vis_y=-crop_top;
   crop_bottom=bottom-this.BottomEdge()-1;
   if(crop_bottom<0)
      vis_h=this.Height()+crop_bottom-vis_y;
   crop_left=this.CoordX()-left;
   if(crop_left<0)
      vis_x=-crop_left;
   crop_right=right-this.RightEdge()-1;
   if(crop_right<0)
      vis_w=this.Width()+crop_right-vis_x;
//--- If there are areas that need to be hidden, call the cropping method with the calculated size of the object visibility scope
   if(crop_top<0 || crop_bottom<0 || crop_left<0 || crop_right<0)
      this.Crop(vis_x,vis_y,vis_w,vis_h);
  }
//+------------------------------------------------------------------+

Ahora, si los encabezados se salen del contenedor, no se mostrará su sección estrecha de dos píxeles.


En el archivo de la clase del objeto WinForms TabControl \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\TabControl.mqh, en su sección privada, declararemos los nuevos métodos:

//--- Return the list of (1) headers, (2) tab fields, the pointer to the (3) up-down and (4) left-right button objects
   CArrayObj        *GetListHeaders(void)          { return this.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);        }
   CArrayObj        *GetListFields(void)           { return this.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD);         }
   CArrowUpDownBox  *GetArrUpDownBox(void)         { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX,0); }
   CArrowLeftRightBox *GetArrLeftRightBox(void)    { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX,0); }
   
//--- Return the pointer to the (1) last and (2) first visible tab header
   CTabHeader       *GetLastHeader(void)           { return this.GetTabHeader(this.TabPages()-1);                                }
   CTabHeader       *GetFirstVisibleHeader(void);
//--- Set the tab as selected
   void              SetSelected(const int index);
//--- Set the tab as released
   void              SetUnselected(const int index);
//--- Set the number of a selected tab
   void              SetSelectedTabPageNum(const int value) { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,value);         }
//--- Arrange the tab headers according to the set modes
   void              ArrangeTabHeaders(void);
//--- Arrange the tab headers at the (1) top, (2) bottom, (3) left and (4) right
   void              ArrangeTabHeadersTop(void);
   void              ArrangeTabHeadersBottom(void);
   void              ArrangeTabHeadersLeft(void);
   void              ArrangeTabHeadersRight(void);
//--- Stretch tab headers by control size
   void              StretchHeaders(void);
//--- Stretch tab headers by (1) control width and height when positioned on the (2) left and (3) right
   void              StretchHeadersByWidth(void);
   void              StretchHeadersByHeightLeft(void);
   void              StretchHeadersByHeightRight(void);
//--- Scroll the header row (1) to the left, (2) to the right, (3) up when headers are on the left, (4) down, (3) up, (4) down
   void              ScrollHeadersRowToLeft(void);
   void              ScrollHeadersRowToRight(void);
//--- Scroll the row of headers when they are located on the left (1) up, (2) down
   void              ScrollHeadersRowLeftToUp(void);
   void              ScrollHeadersRowLeftToDown(void);
//--- Scroll the row of headers when they are located on the right (1) up, (2) down
   void              ScrollHeadersRowRightToUp(void);
   void              ScrollHeadersRowRightToDown(void);
public:

y eliminaremos el método público

//--- Show the control
   virtual void      Show(void);
//--- Shift the header row
   void              ShiftHeadersRow(const int selected);
//--- Event handler
   virtual void      OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam);

//--- Constructor

Ya hicimos este método público en un artículo anterior, y se encargaba de desplazar la línea de encabezados a la izquierda al pulsar sobre un encabezado parcialmente oculto. A partir de ahora, esto se gestionará usando los métodos declarados anteriormente, además, estos procesarán tanto el clic sobre un encabezado parcialmente oculto como el clic sobre el botón de control de scrolling de la línea de encabezados.

Cada uno de los métodos declarados ha sido diseñado para el scrolling de la línea de encabezados en una dirección diferente:

  • Cuando los encabezados se encuentran arriba o abajo, hay dos métodos para el scrolling a la izquierda y a la derecha.
  • Cuando los encabezados se encuentran a la izquierda, hay dos métodos para el scrolling a la izquierda y el scrolling a la derecha. 
  • Cuando los encabezados se encuentran a la derecha, hay dos métodos para el scrolling a la izquierda y el scrolling a la derecha.

En el método que crea el número indicado de pestañas, al crear los objetos botón con flechas izquierda-derecha y arriba-abajo, necesitaremos indicar a estos objetos y a cada objeto de botón con flechas dentro de estos objetos sus objetos principal y básico, de lo contrario, no podremos encontrar estos objetos al crear el mensaje de evento al pulsar sobre el botón:

//+------------------------------------------------------------------+
//| Create the specified number of tabs                              |
//+------------------------------------------------------------------+
bool CTabControl::CreateTabPages(const int total,const int selected_page,const int tab_w=0,const int tab_h=0,const string header_text="")
  {
//--- Calculate the size and initial coordinates of the tab title
   int w=(tab_w==0 ? this.ItemWidth()  : tab_w);
   int h=(tab_h==0 ? this.ItemHeight() : tab_h);

//--- In the loop by the number of tabs

//---...
//---...

//--- Create left-right and up-down button objects
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX,this.Width()-32,0,15,15,clrNONE,255,this.Active(),false);
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX,0,this.Height()-32,15,15,clrNONE,255,this.Active(),false);
//--- 
   CArrowLeftRightBox *box_lr=this.GetArrLeftRightBox();
   if(box_lr!=NULL)
     {
      this.SetVisibleLeftRightBox(false);
      this.SetSizeLeftRightBox(box_lr.Width());
      box_lr.SetMain(this.GetMain());
      box_lr.SetBase(this.GetObject());
      box_lr.SetBorderStyle(FRAME_STYLE_NONE);
      box_lr.SetBackgroundColor(CLR_CANV_NULL,true);
      box_lr.SetOpacity(0);
      box_lr.Hide();
      CArrowLeftButton *lb=box_lr.GetArrowLeftButton();
      if(lb!=NULL)
        {
         lb.SetMain(this.GetMain());
         lb.SetBase(box_lr);
        }
      CArrowRightButton *rb=box_lr.GetArrowRightButton();
      if(rb!=NULL)
        {
         rb.SetMain(this.GetMain());
         rb.SetBase(box_lr);
        }
     }
//---
   CArrowUpDownBox *box_ud=this.GetArrUpDownBox();
   if(box_ud!=NULL)
     {
      this.SetVisibleUpDownBox(false);
      this.SetSizeUpDownBox(box_ud.Height());
      box_ud.SetMain(this.GetMain());
      box_ud.SetBase(this.GetObject());
      box_ud.SetBorderStyle(FRAME_STYLE_NONE);
      box_ud.SetBackgroundColor(CLR_CANV_NULL,true);
      box_ud.SetOpacity(0);
      box_ud.Hide();
      CArrowDownButton *db=box_ud.GetArrowDownButton();
      if(db!=NULL)
        {
         db.SetMain(this.GetMain());
         db.SetBase(box_ud);
        }
      CArrowUpButton *ub=box_ud.GetArrowUpButton();
      if(ub!=NULL)
        {
         ub.SetMain(this.GetMain());
         ub.SetBase(box_ud);
        }
     }

//--- Arrange all titles in accordance with the specified display modes and select the specified tab
   this.ArrangeTabHeaders();
   this.Select(selected_page,true);
   return true;
  }
//+------------------------------------------------------------------+

Después de crear los objetos de botón con flechas izquierda-derecha y arriba-abajo, obtendremos el puntero al objeto creado y estableceremos los objetos principal y básico para él. A continuación, obtendremos sus objetos de botón con flechas del objeto resultante y especificaremos el objeto principal y el objeto básico para cada uno de ellos.

En el método que muestra un control, añadiremos la comprobación de la bandera de visualización del objeto:

//+------------------------------------------------------------------+
//| Show the control                                                 |
//+------------------------------------------------------------------+
void CTabControl::Show(void)
  {
//--- If the element should not be displayed (hidden inside another control), leave
   if(!this.Displayed())
      return;
//--- Get the list of all tab headers
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- If the object has a shadow, display it
   if(this.m_shadow_obj!=NULL)
      this.m_shadow_obj.Show();
//--- Display the container
   CGCnvElement::Show();
//--- Move all elements of the object to the foreground
   this.BringToTop();
  }
//+------------------------------------------------------------------+

Si el modo de visualización manual está activado para el objeto, saldremos del método.


Método que retorna un puntero al primer encabezado visible:

//+------------------------------------------------------------------+
//| Return the pointer to the first visible header                   |
//+------------------------------------------------------------------+
CTabHeader *CTabControl::GetFirstVisibleHeader(void)
  {
   for(int i=0;i<this.TabPages();i++)
     {
      CTabHeader *obj=this.GetTabHeader(i);
      if(obj==NULL)
         continue;
      switch(this.Alignment())
        {
         case CANV_ELEMENT_ALIGNMENT_TOP     :
         case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
           if(obj.CoordX()==this.CoordXWorkspace()+(obj.State() ? 0 : 2))
              return obj;
           break;
         case CANV_ELEMENT_ALIGNMENT_LEFT  :
           if(obj.BottomEdge()==this.BottomEdgeWorkspace()+(obj.State() ? 0 : -2))
              return obj;
           break;
         case CANV_ELEMENT_ALIGNMENT_RIGHT  :
           if(obj.CoordY()==this.CoordYWorkspace()+(obj.State() ? 0 : 2))
              return obj;
           break;
         default:
           break;
        }
     }
   .return NULL;
  }
//+------------------------------------------------------------------+

El primer encabezado visible es el de la izquierda cuando los encabezados están en la parte superior/inferior, o en la parte inferior cuando los encabezados están a la izquierda, o en la parte superior cuando los encabezados están a la derecha del control. Para encontrar el encabezado más externo, deberemos iterar por todos los encabezados de los objetos y comprobar sus coordenadas con la ubicación de la fila de encabezados. Para la ubicación superior, el encabezado deberá encontrarse en la coordenada inicial X del contenedor. Si el encabezado no está seleccionado, su coordenada inicial se desplazará dos píxeles a la derecha. Lo mismo ocurrirá con una disposición diferente de la fila de encabezados.
El método buscará en un ciclo una coincidencia entre las coordenadas del objeto y la coordenada del contenedor en función de la ubicación de la fila de encabezados, y retornará el puntero al objeto encontrado. Si no se encuentra ninguno de los encabezados, el método retornará NULL.


Método que desplaza la línea de encabezados hacia la izquierda:

//+------------------------------------------------------------------+
//| Scroll the header bar to the left                                |
//+------------------------------------------------------------------+
void CTabControl::ScrollHeadersRowToLeft(void)
  {
//--- If there are multiline headers, leave
   if(this.Multiline())
      return;
//--- Declare the variables and get the index of the selected tab
   int shift=0;
   int correct_size=0;
   int selected=this.SelectedTabPageNum();
//--- Get the first visible header
   CTabHeader *first=this.GetFirstVisibleHeader();
   if(first==NULL)
      return;
//--- If the first visible header is selected, set the size adjustment value
   if(first.PageNumber()==selected)
      correct_size=4;
//--- Get the pointer to the very last header
   CTabHeader *last=this.GetLastHeader();
   if(last==NULL)
      return;
//--- If the last heading is fully visible, leave since the shift of all headers to the left is completed
   if(last.RightEdge()<=this.RightEdgeWorkspace())
      return;
//--- Get the shift size
   shift=first.Width()-correct_size;
//--- In the loop by all headers
   for(int i=0;i<this.TabPages();i++)
     {
      //--- get the next header
      CTabHeader *header=this.GetTabHeader(i);
      if(header==NULL)
         continue;
      //--- and, if the header is successfully shifted to the left by 'shift' value,
      if(header.Move(header.CoordX()-shift,header.CoordY()))
        {
         //--- save its new relative coordinates
         header.SetCoordXRelative(header.CoordX()-this.CoordX());
         header.SetCoordYRelative(header.CoordY()-this.CoordY());
         //--- If the title has gone beyond the left edge,
         int x=(i==selected ? 0 : 2);
         if(header.CoordX()-x<this.CoordXWorkspace())
           {
            //--- crop and hide it
            header.Crop();
            header.Hide();
            //--- Get the selected header
            CTabHeader *header_selected=this.GetTabHeader(selected);
            if(header_selected==NULL)
               continue;
            //--- Get the tab field corresponding to the selected header
            CTabField *field_selected=header_selected.GetFieldObj();
            if(field_selected==NULL)
               continue;
            //--- Draw the field frame
            field_selected.DrawFrame();
            field_selected.Update();
           }
         //--- If the header fits the visible area of the control,
         else
           {
            //--- display and redraw it
            header.Show();
            header.Redraw(false);
            //--- Get the tab field corresponding to the header
            CTabField *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- If this is a selected header,
            if(i==selected)
              {
               //--- Draw the field frame
               field.DrawFrame();
               field.Update();
              }
           }
        }
     }
//--- Get the selected header
   CTabHeader *obj=this.GetTabHeader(selected);
//--- If the header is placed in the visible part of the control, bring it to the foreground
   if(obj!=NULL && obj.CoordX()>=this.CoordXWorkspace() && obj.RightEdge()<=this.RightEdgeWorkspace())
      obj.BringToTop();
//--- Redraw the chart to display changes immediately
   ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+


Método que desplaza la línea de encabezados hacia la derecha:

//+------------------------------------------------------------------+
//| Scroll the header bar to the right                               |
//+------------------------------------------------------------------+
void CTabControl::ScrollHeadersRowToRight(void)
  {
//--- If there are multiline headers, leave
   if(this.Multiline())
      return;
//--- Declare the variables and get the index of the selected tab
   int shift=0;
   int correct_size=0;
   int selected=this.SelectedTabPageNum();
//--- Get the first visible header
   CTabHeader *first=this.GetFirstVisibleHeader();
   if(first==NULL)
      return;
//--- Get the header located before the first visible one
   CTabHeader *prev=this.GetTabHeader(first.PageNumber()-1);
//--- If there is no such header, leave since the shift of all headers to the right is completed
   if(prev==NULL)
      return;
//--- If the header is selected, specify the size adjustment value
   if(prev.PageNumber()==selected)
      correct_size=4;
//--- Get the shift size
   shift=prev.Width()-correct_size;
//--- In the loop by all headers
   for(int i=0;i<this.TabPages();i++)
     {
      //--- get the next header
      CTabHeader *header=this.GetTabHeader(i);
      if(header==NULL)
         continue;
      //--- and, if the header is successfully shifted to the right by 'shift' value,
      if(header.Move(header.CoordX()+shift,header.CoordY()))
        {
         //--- save its new relative coordinates
         header.SetCoordXRelative(header.CoordX()-this.CoordX());
         header.SetCoordYRelative(header.CoordY()-this.CoordY());
         //--- If the title goes beyond the left edge,
         int x=(i==selected ? 0 : 2);
         if(header.CoordX()-x<this.CoordXWorkspace())
           {
            //--- crop and hide it
            header.Crop();
            header.Hide();
           }
         //--- If the header fits the visible area of the control,
         else
           {
            //--- display and redraw it
            header.Show();
            header.Redraw(false);
            //--- Get the tab field corresponding to the header
            CTabField *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- If this is a selected header,
            if(i==selected)
              {
               //--- Draw the field frame
               field.DrawFrame();
               field.Update();
              }
           }
        }
     }
//--- Get the selected header
   CTabHeader *obj=this.GetTabHeader(selected);
//--- If the header is placed in the visible part of the control, bring it to the foreground
   if(obj!=NULL && obj.CoordX()>=this.CoordXWorkspace() && obj.RightEdge()<=this.RightEdgeWorkspace())
      obj.BringToTop();
//--- Redraw the chart to display changes immediately
   ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+


Método que desplaza hacia arriba la línea de encabezados cuando estos se ubican a la izquierda:

//+------------------------------------------------------------------+
//| Scroll the header row up when the headers are on the left        |
//+------------------------------------------------------------------+
void CTabControl::ScrollHeadersRowLeftToUp(void)
  {
//--- If there are multiline headers, leave
   if(this.Multiline())
      return;
//--- Declare the variables and get the index of the selected tab
   int shift=0;
   int correct_size=0;
   int selected=this.SelectedTabPageNum();
//--- Get the first visible header
   CTabHeader *first=this.GetFirstVisibleHeader();
   if(first==NULL)
      return;
//--- Get the header located before the first visible one
   CTabHeader *prev=this.GetTabHeader(first.PageNumber()-1);
//--- If there is no such header, leave since the shift of all headers upwards is completed
   if(prev==NULL)
      return;
//--- If the header is selected, specify the size adjustment value
   if(prev.PageNumber()==selected)
      correct_size=4;
//--- Get the shift size
   shift=prev.Height()-correct_size;
//--- In the loop by all headers
   for(int i=0;i<this.TabPages();i++)
     {
      //--- get the next header
      CTabHeader *header=this.GetTabHeader(i);
      if(header==NULL)
         continue;
      //--- and, if the header is successfully shifted upwards by 'shift' value,
      if(header.Move(header.CoordX(),header.CoordY()-shift))
        {
         //--- save its new relative coordinates
         header.SetCoordXRelative(header.CoordX()-this.CoordX());
         header.SetCoordYRelative(header.CoordY()-this.CoordY());
         //--- If the header goes beyond the lower edge,
         int x=(i==selected ? 0 : 2);
         if(header.BottomEdge()+x>this.BottomEdgeWorkspace())
           {
            //--- crop and hide it
            header.Crop();
            header.Hide();
           }
         //--- If the header fits the visible area of the control,
         else
           {
            //--- display and redraw it
            header.Show();
            header.Redraw(false);
            //--- Get the tab field corresponding to the header
            CTabField *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- If this is a selected header,
            if(i==selected)
              {
               //--- Draw the field frame
               field.DrawFrame();
               field.Update();
              }
           }
        }
     }
//--- Get the selected header
   CTabHeader *obj=this.GetTabHeader(selected);
//--- If the header is placed in the visible part of the control, bring it to the foreground
   if(obj!=NULL && obj.CoordY()>=this.CoordYWorkspace() && obj.BottomEdge()<=this.BottomEdgeWorkspace())
      obj.BringToTop();
//--- Redraw the chart to display changes immediately
   ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+


Método que desplaza hacia abajo la línea de encabezados cuando estos se ubican a la izquierda:

//+------------------------------------------------------------------+
//| Scroll the header row down when the headers are on the left      |
//+------------------------------------------------------------------+
void CTabControl::ScrollHeadersRowLeftToDown(void)
  {
//--- If there are multiline headers, leave
   if(this.Multiline())
      return;
//--- Declare the variables and get the index of the selected tab
   int shift=0;
   int correct_size=0;
   int selected=this.SelectedTabPageNum();
//--- Get the first visible header
   CTabHeader *first=this.GetFirstVisibleHeader();
   if(first==NULL)
      return;
//--- If the first visible header is selected, set the size adjustment value
   if(first.PageNumber()==selected)
      correct_size=4;
//--- Get the pointer to the very last header
   CTabHeader *last=this.GetLastHeader();
   if(last==NULL)
      return;
//--- If the last heading is fully visible, leave since the shift of all headers downwards is completed
   if(last.CoordY()>=this.CoordYWorkspace())
      return;
//--- Get the shift size
   shift=first.Height()-correct_size;
//--- In the loop by all headers
   for(int i=0;i<this.TabPages();i++)
     {
      //--- get the next header
      CTabHeader *header=this.GetTabHeader(i);
      if(header==NULL)
         continue;
      //--- and, if the header is successfully shifted downwards by 'shift' value,
      if(header.Move(header.CoordX(),header.CoordY()+shift))
        {
         //--- save its new relative coordinates
         header.SetCoordXRelative(header.CoordX()-this.CoordX());
         header.SetCoordYRelative(header.CoordY()-this.CoordY());
         //--- If the header has gone beyond the lower edge,
         int x=(i==selected ? 0 : 2);
         if(header.BottomEdge()-x>this.BottomEdgeWorkspace())
           {
            //--- crop and hide it
            header.Crop();
            header.Hide();
            //--- Get the selected header
            CTabHeader *header_selected=this.GetTabHeader(selected);
            if(header_selected==NULL)
               continue;
            //--- Get the tab field corresponding to the selected header
            CTabField *field_selected=header_selected.GetFieldObj();
            if(field_selected==NULL)
               continue;
            //--- Draw the field frame
            field_selected.DrawFrame();
            field_selected.Update();
           }
         //--- If the header fits the visible area of the control,
         else
           {
            //--- display and redraw it
            header.Show();
            header.Redraw(false);
            //--- Get the tab field corresponding to the header
            CTabField *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- If this is a selected header,
            if(i==selected)
              {
               //--- Draw the field frame
               field.DrawFrame();
               field.Update();
              }
           }
        }
     }
//--- Get the selected header
   CTabHeader *obj=this.GetTabHeader(selected);
//--- If the header is placed in the visible part of the control, bring it to the foreground
   if(obj!=NULL && obj.CoordY()>=this.CoordYWorkspace() && obj.BottomEdge()<=this.BottomEdgeWorkspace())
      obj.BringToTop();
//--- Redraw the chart to display changes immediately
   ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+


Método que desplaza hacia arriba la línea de encabezados cuando estos se ubican a la derecha:

//+------------------------------------------------------------------+
//| Scroll the header row up when the headers are on the right       |
//+------------------------------------------------------------------+
void CTabControl::ScrollHeadersRowRightToUp(void)
  {
//--- If there are multiline headers, leave
   if(this.Multiline())
      return;
//--- Declare the variables and get the index of the selected tab
   int shift=0;
   int correct_size=0;
   int selected=this.SelectedTabPageNum();
//--- Get the first visible header
   CTabHeader *first=this.GetFirstVisibleHeader();
   if(first==NULL)
      return;
//--- If the first visible header is selected, set the size adjustment value
   if(first.PageNumber()==selected)
      correct_size=4;
//--- Get the pointer to the very last header
   CTabHeader *last=this.GetLastHeader();
   if(last==NULL)
      return;
//--- If the last heading is fully visible, leave since the shift of all headers upwards is completed
   if(last.BottomEdge()<=this.BottomEdgeWorkspace())
      return;
//--- Get the shift size
   shift=first.Height()-correct_size;
//--- In the loop by all headers
   for(int i=0;i<this.TabPages();i++)
     {
      //--- get the next header
      CTabHeader *header=this.GetTabHeader(i);
      if(header==NULL)
         continue;
      //--- and, if the header is successfully shifted upwards by 'shift' value,
      if(header.Move(header.CoordX(),header.CoordY()-shift))
        {
         //--- save its new relative coordinates
         header.SetCoordXRelative(header.CoordX()-this.CoordX());
         header.SetCoordYRelative(header.CoordY()-this.CoordY());
         //--- If the header has gone beyond the upper edge,
         int x=(i==selected ? 0 : 2);
         if(header.CoordY()-x<this.CoordYWorkspace())
           {
            //--- crop and hide it
            header.Crop();
            header.Hide();
            //--- Get the selected header
            CTabHeader *header_selected=this.GetTabHeader(selected);
            if(header_selected==NULL)
               continue;
            //--- Get the tab field corresponding to the selected header
            CTabField *field_selected=header_selected.GetFieldObj();
            if(field_selected==NULL)
               continue;
            //--- Draw the field frame
            field_selected.DrawFrame();
            field_selected.Update();
           }
         //--- If the header fits the visible area of the control,
         else
           {
            //--- display and redraw it
            header.Show();
            header.Redraw(false);
            //--- Get the tab field corresponding to the header
            CTabField *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- If this is a selected header,
            if(i==selected)
              {
               //--- Draw the field frame
               field.DrawFrame();
               field.Update();
              }
           }
        }
     }
//--- Get the selected header
   CTabHeader *obj=this.GetTabHeader(selected);
//--- If the header is placed in the visible part of the control, bring it to the foreground
   if(obj!=NULL && obj.CoordY()>=this.CoordYWorkspace() && obj.BottomEdge()<=this.BottomEdgeWorkspace())
      obj.BringToTop();
//--- Redraw the chart to display changes immediately
   ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+


Método que desplaza hacia abajo la línea de encabezados cuando estos se ubican a la derecha:

//+------------------------------------------------------------------+
//| Scroll the header row down when the headers are on the right     |
//+------------------------------------------------------------------+
void CTabControl::ScrollHeadersRowRightToDown(void)
  {
//--- If there are multiline headers, leave
   if(this.Multiline())
      return;
//--- Declare the variables and get the index of the selected tab
   int shift=0;
   int correct_size=0;
   int selected=this.SelectedTabPageNum();
//--- Get the first visible header
   CTabHeader *first=this.GetFirstVisibleHeader();
   if(first==NULL)
      return;
//--- Get the header located before the first visible one
   CTabHeader *prev=this.GetTabHeader(first.PageNumber()-1);
//--- If there is no such header, leave since the shift of all headers downwards is completed
   if(prev==NULL)
      return;
//--- If the header is selected, specify the size adjustment value
   if(prev.PageNumber()==selected)
      correct_size=4;
//--- Get the shift size
   shift=prev.Height()-correct_size;
//--- In the loop by all headers
   for(int i=0;i<this.TabPages();i++)
     {
      //--- get the next header
      CTabHeader *header=this.GetTabHeader(i);
      if(header==NULL)
         continue;
      //--- and, if the header is successfully shifted downwards by 'shift' value,
      if(header.Move(header.CoordX(),header.CoordY()+shift))
        {
         //--- save its new relative coordinates
         header.SetCoordXRelative(header.CoordX()-this.CoordX());
         header.SetCoordYRelative(header.CoordY()-this.CoordY());
         //--- If the title goes beyond the upper edge
         int x=(i==selected ? 0 : 2);
         if(header.CoordY()-x<this.CoordYWorkspace())
           {
            //--- crop and hide it
            header.Crop();
            header.Hide();
           }
         //--- If the header fits the visible area of the control,
         else
           {
            //--- display and redraw it
            header.Show();
            header.Redraw(false);
            //--- Get the tab field corresponding to the header
            CTabField *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- If this is a selected header,
            if(i==selected)
              {
               //--- Draw the field frame
               field.DrawFrame();
               field.Update();
              }
           }
        }
     }
//--- Get the selected header
   CTabHeader *obj=this.GetTabHeader(selected);
//--- If the header is placed in the visible part of the control, bring it to the foreground
   if(obj!=NULL && obj.CoordY()>=this.CoordYWorkspace() && obj.BottomEdge()<=this.BottomEdgeWorkspace())
      obj.BringToTop();
//--- Redraw the chart to display changes immediately
   ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+

La lógica de todos los métodos para el scrolling de las filas de encabezados se explica completamente en el código del método. Todas son idénticas entre sí y solo se distinguen ligeramente en los cálculos de desplazamiento y las zonas de visibilidad. Espero que los métodos no necesiten más explicaciones. En cualquier caso, podrá plantear cualquier duda en los comentarios al artículo.

Manejador de eventos:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CTabControl::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Adjust subwindow Y shift
   CGCnvElement::OnChartEvent(id,lparam,dparam,sparam);

//--- If the tab is selected
   if(id==WF_CONTROL_EVENT_TAB_SELECT)
     {
      //--- Get the header of the selected tab
      CTabHeader *header=this.GetTabHeader(this.SelectedTabPageNum());
      if(header==NULL)
         return;
      //--- Depending on the location of the header row
      switch(this.Alignment())
        {
         //--- Headers at the top/bottom
         case CANV_ELEMENT_ALIGNMENT_TOP     :
         case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
            //--- If the header is cropped, shift the header row to the left
            if(header.RightEdge()>this.RightEdgeWorkspace())
               this.ScrollHeadersRowToLeft();
            break;
         //--- Headers on the left
         case CANV_ELEMENT_ALIGNMENT_LEFT    :
            //--- If the header is cropped, shift the header row downwards
            if(header.CoordY()<this.CoordYWorkspace())
               this.ScrollHeadersRowLeftToDown();
            break;
         //--- Headers on the right
         case CANV_ELEMENT_ALIGNMENT_RIGHT   :
            //--- If the header is cropped, shift the header row upwards
            Print(DFUN,"header.BottomEdge=",header.BottomEdge(),", this.BottomEdgeWorkspace=",this.BottomEdgeWorkspace());
            if(header.BottomEdge()>this.BottomEdgeWorkspace())
               this.ScrollHeadersRowRightToUp();
            break;
         default:
           break;
        }
      
     }

//--- When clicking on any header row scroll button
   if(id>=WF_CONTROL_EVENT_CLICK_SCROLL_LEFT && id<=WF_CONTROL_EVENT_CLICK_SCROLL_DOWN)
     {
      //--- Get the header of the last tab
      CTabHeader *header=this.GetTabHeader(this.GetListHeaders().Total()-1);
      if(header==NULL)
         return;
      int hidden=0;
      
      //--- When clicking on the left arrow header row scroll button
      if(id==WF_CONTROL_EVENT_CLICK_SCROLL_LEFT)
         this.ScrollHeadersRowToRight();
      
      //--- When clicking on the right arrow header row scroll button
      if(id==WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT)
         this.ScrollHeadersRowToLeft();
      
      //--- When clicking on the down arrow header row scroll button
      if(id==WF_CONTROL_EVENT_CLICK_SCROLL_DOWN)
        {
         //--- Depending on the location of the header row
         switch(this.Alignment())
           {
            //--- scroll the headings upwards using the appropriate method
            case CANV_ELEMENT_ALIGNMENT_LEFT    :  this.ScrollHeadersRowLeftToUp();    break;
            case CANV_ELEMENT_ALIGNMENT_RIGHT   :  this.ScrollHeadersRowRightToUp();   break;
            default: break;
           }
        }
      
      //--- When clicking on the up arrow header row scroll button
      if(id==WF_CONTROL_EVENT_CLICK_SCROLL_UP)
        {
         //--- Depending on the location of the header row
         switch(this.Alignment())
           {
            //--- scroll the headings downwards using the appropriate method
            case CANV_ELEMENT_ALIGNMENT_LEFT    :  this.ScrollHeadersRowLeftToDown();  break;
            case CANV_ELEMENT_ALIGNMENT_RIGHT   :  this.ScrollHeadersRowRightToDown(); break;
            default: break;
           }
        }
     }
  }
//+------------------------------------------------------------------+

Ahora, utilizando los métodos descritos anteriormente, procesaremos cada evento, ya sea la selección de un encabezado de pestaña parcialmente oculto o el clic sobre el botón de scrolling de la fila de encabezados. En general, dependiendo de la ubicación de la fila de encabezados y del evento pulsación de un botón o encabezado, llamaremos al método correspondiente para el scrolling por la fila de encabezados.

En la clase de colección de elementos gráficos, en el archivo \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh, en su manejador de eventos, ahora deberemos procesar correctamente los eventos obtenidos de los objetos WinForms. A saber, los tres nombres del parámetro de cadena sparam, así como encontrar el objeto básico, y a partir de él, el que ha generado el evento. Una vez encontrado, y si este objeto pertenece al control TabControl, llamaremos al manejador de eventos del control TabControl, enviándole el identificador del evento.

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CGraphElementsCollection::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   CGStdGraphObj *obj_std=NULL;  // Pointer to the standard graphical object
   CGCnvElement  *obj_cnv=NULL;  // Pointer to the graphical element object on canvas
   ushort idx=ushort(id-CHARTEVENT_CUSTOM);

//--- Processing WinForms control events
   if(idx>WF_CONTROL_EVENT_NO_EVENT && idx<WF_CONTROL_EVENTS_NEXT_CODE)
     {
      //--- Declare the array of names and enter the names of three objects, set in 'sparam' and separated by ";", into it
      string array[];
      if(::StringSplit(sparam,::StringGetCharacter(";",0),array)!=3)
        {
         CMessage::ToLog(MSG_GRAPH_OBJ_FAILED_GET_OBJECT_NAMES);
         return;
        }
      //--- Get the main object by name
      CWinFormBase *main=this.GetCanvElement(array[0]);
      if(main==NULL)
         return;
      //--- Get the base object, inside which the event has occurred, from the main object by name
      CWinFormBase *base=main.GetElementByName(array[1]);
      CWinFormBase *base_elm=NULL;
      //--- If there is no element with the same name, then this is the base object of the event element bound to the base one - look for it in the list
      if(base==NULL)
        {
         //--- Get the list of all elements bound to the main object with the type set in the 'dparam' parameter
         CArrayObj *list_obj=CSelect::ByGraphCanvElementProperty(main.GetListElements(),CANV_ELEMENT_PROP_TYPE,(long)dparam,EQUAL);
         if(list_obj==NULL || list_obj.Total()==0)
            return;
         //--- In the loop by the obtained list
         for(int i=0;i<list_obj.Total();i++)
           {
            //--- get the next object
            base_elm=list_obj.At(i);
            if(base_elm==NULL)
               continue;
            //--- If the base object is found, get the bound object from it by name from array[1]
            base=base_elm.GetElementByName(array[1]);
            if(base!=NULL)
               break;
           }
        }
      //--- If failed to find the object here, exit
      if(base==NULL)
         return;
      //--- From the found base object, get the object the event occurred from by name
      CWinFormBase *object=base.GetElementByName(array[2]);
      if(object==NULL)
         return;

      //+------------------------------------------------------------------+
      //|  Clicking the control                                            |
      //+------------------------------------------------------------------+
      if(idx==WF_CONTROL_EVENT_CLICK)
        {
         //--- If TabControl type is set in dparam
         if((ENUM_GRAPH_ELEMENT_TYPE)dparam==GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL)
           {
            //--- Set the event type depending on the element type that generated the event
            int event_id=
              (object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT   ?  WF_CONTROL_EVENT_CLICK_SCROLL_LEFT  :
               object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT  ?  WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT :
               object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP     ?  WF_CONTROL_EVENT_CLICK_SCROLL_UP    :
               object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN   ?  WF_CONTROL_EVENT_CLICK_SCROLL_DOWN  :
               WF_CONTROL_EVENT_NO_EVENT);
            //--- If the base control is received, call its event handler
            if(base_elm!=NULL)
               base_elm.OnChartEvent(event_id,lparam,dparam,sparam);
           }
        }

      //+------------------------------------------------------------------+
      //|  Selecting the TabControl tab                                    |
      //+------------------------------------------------------------------+
      if(idx==WF_CONTROL_EVENT_TAB_SELECT)
        {
         if(base!=NULL)
            base.OnChartEvent(idx,lparam,dparam,sparam);
        }
     }
//--- Handle the events of renaming and clicking a standard graphical object
   if(id==CHARTEVENT_OBJECT_CHANGE  || id==CHARTEVENT_OBJECT_DRAG    || id==CHARTEVENT_OBJECT_CLICK   ||
      idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG   || idx==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Calculate the chart ID

      //---...
      //---...

Aquí también podemos ver que toda la lógica está completamente explicada en el código, por lo que esperamos que no necesite más explicaciones.

Ahora ya está todo listo para poner a prueba lo que hemos hecho hoy.


Simulación

Para la prueba, tomaremos el asesor del artículo anterior y lo guardaremos en la nueva carpeta \MQL5\Experts\TestDoEasy\Part119\ con el nuevo nombre TestDoEasy119.mq5.

La única diferencia entre el asesor y la versión anterior es que crearemos 15 pestañas en el control TabControl:

         //--- Create TabControl
         pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,InpTabControlX,InpTabControlY,pnl.Width()-30,pnl.Height()-40,clrNONE,255,true,false);
         CTabControl *tc=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,0);
         if(tc!=NULL)
           {
            tc.SetTabSizeMode((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)InpTabPageSizeMode);
            tc.SetAlignment((ENUM_CANV_ELEMENT_ALIGNMENT)InpHeaderAlignment);
            tc.SetMultiline(InpTabCtrlMultiline);
            tc.SetHeaderPadding(6,0);
            tc.CreateTabPages(15,0,56,20,TextByLanguage("Вкладка","TabPage"));
            //--- Create a text label with a tab description on each tab
            for(int j=0;j<tc.TabPages();j++)
              {
               tc.CreateNewElement(j,GRAPH_ELEMENT_TYPE_WF_LABEL,60,20,80,20,clrDodgerBlue,255,true,false);
               CLabel *label=tc.GetTabElement(j,0);
               if(label==NULL)
                  continue;
               label.SetText("TabPage"+string(j+1));
              }
           }

Podríamos haberlo dejado como estaba, con 11 pestañas, pero hemos aumentado el número de pestañas para probar el rendimiento y encontrar algunos 'bugs'. Así que este número es simplemente el resultado de la depuración y la solución de problemas cuando el encabezado resaltado se desplaza fuera del contenedor en ambos lados.

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


Como podemos ver, todo lo que queríamos hacer hoy está funcionando según lo previsto.

Eso sí, hay dos pequeñas faltas: si pasamos el cursor por encima de una zona del encabezado de una pestaña que esté oculta, el encabezado reaccionará cambiando de color como si estuviera visible en ese lugar. Esta es la razón por la que la zona activa del elemento no cambia de tamaño cuando se redimensiona la zona visible. Para corregirlo, deberemos calcular la zona activa y redimensionarla para que coincida con la zona visible.

La segunda falta es que si desplazamos el encabezado seleccionado fuera del contenedor y desplazamos el panel, se mostrarán los dos píxeles del encabezado oculto. Esto tiene que ver con la especificación del tamaño de la pestaña para calcular la zona de visibilidad, ya que el encabezado seleccionado aumentará de tamaño en dos píxeles a cada lado. Para solucionarlo, dentro del objeto de encabezado de la pestaña, deberemos averiguar cómo obtener el tamaño del encabezado adyacente que se utiliza para calcular el tamaño de la zona de visibilidad.

Llegaremos a esto artículos posteriores, junto con el desarrollo del nuevo objeto WinForms.


¿Qué es lo próximo?

En el próximo artículo, comenzaremos a desarrollar el objeto WinForms SplitContainer.

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 13): Optimizando la interacción de los objetos WinForms con el ratón. Comenzamos el desarrollo del objeto WinForms TabControl
DoEasy. Elementos de control (Parte 14): Nuevo algoritmo de denominación de los elementos gráficos. Continuamos trabajando con el objeto WinForms TabControl
DoEasy. Elementos de control (Parte 15): Objeto WinForms TabControl - múltiples filas de encabezados de pestañas, métodos de trabajo con pestañas 
DoEasy. Elementos de control (Parte 16): Objeto WinForms TabControl - múltiples filas de encabezados de pestañas, modo de expansión de encabezados para ajustarse al tamaño del contenedor
DoEasy. Elementos de control (Parte 17): Recortando partes invisibles de objetos, objetos WinForms auxiliares de botón con flechas
DoEasy. Elementos de control (Parte 18): Preparamos la funcionalidad para el scrolling de las pestañas en TabControl

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

Archivos adjuntos |
MQL5.zip (4458.6 KB)
Tablero de cotizaciones: Versión básica Tablero de cotizaciones: Versión básica
Aquí te mostraré cómo crear esas pantallas en forma de banda, normalmente usadas para mostrar cotizaciones como en la bolsa, pero pura y simplemente usando MQL5, sin recurrir a programación externa complicada e innecesariamente sofisticada.
Aprendiendo a diseñar un sistema de trading con Awesome Oscillator Aprendiendo a diseñar un sistema de trading con Awesome Oscillator
En este nuevo artículo de la serie, nos familiarizaremos con otra herramienta técnica útil para el trading: el indicador Awesome Oscillator (AO). Asimismo, aprenderemos a desarrollar sistemas comerciales basados en las lecturas de este indicador.
Tablero de cotizaciones: Versión mejorada Tablero de cotizaciones: Versión mejorada
¿Qué tal si animamos la versión básica del tablero? Lo primero que vamos a hacer es modificar el tablero para añadir una imagen, ya sea el logotipo del activo o cualquier otra imagen, para facilitar una rápida identificación del activo que estamos viendo.
Redes neuronales: así de sencillo (Parte 29): Algoritmo actor-crítico con ventaja (Advantage actor-critic) Redes neuronales: así de sencillo (Parte 29): Algoritmo actor-crítico con ventaja (Advantage actor-critic)
En los artículos anteriores de esta serie, nos familiarizamos con dos algoritmos de aprendizaje por refuerzo. Obviamente, cada uno de ellos tiene sus propias ventajas y desventajas. Como suele suceder en estos casos, se nos ocurre combinar ambos métodos en un algoritmo que incorporaría lo mejor de los dos, y así compensar las carencias de cada uno de ellos. En este artículo, hablaremos de dicho método.