English Русский 中文 Deutsch 日本語 Português
preview
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 15): Objeto WinForms TabControl - múltiples filas de encabezados de pestañas, métodos de trabajo con pestañas

MetaTrader 5Ejemplos | 17 noviembre 2022, 09:41
322 0
Artyom Trishkin
Artyom Trishkin

Contenido


Concepto

Continuamos el tema del objeto WinForms TabControl. Si un control tiene más pestañas de las que pueden caber a todo lo ancho del objeto (nos referimos a posicionar estas en la parte superior), los encabezados que no quepan dentro del control podrán o bien ser recortados en el borde, con la presencia de los botones para su scrolling, o bien, si el objeto tiene la bandera Multiline, los encabezados se colocarán en múltiples filas (tantas como permita el tamaño del control). Existen tres maneras de establecer el tamaño de las pestañas(SizeMode) para el modo de múltiples filas:

  • Normal — la anchura de las pestañas se ajusta a la anchura del texto del encabezado, el espacio especificado en los valores PaddingWidth y PaddingHeight del encabezado se añade a lo largo de los bordes de la misma;
  • Fixed — tamaño fijo indicado en la configuración del control. El texto del encabezado se recorta si no entra en sus dimensiones;
  • FillToRight — las pestañas que caben en la anchura del control se estiran a su anchura completa.

Al seleccionar una pestaña cuando Multiline está activo, su encabezado que no bordea el campo de la pestaña, junto con toda la fila en la que se encuentra, se desplazará a ras del campo de la pestaña, y los encabezados que se han asignado al campo se insertarán en lugar de la fila de la pestaña seleccionada.

Hoy implementaremos un modo así, pero lo haremos solo para la ubicación de las pestañas en la parte superior del control, y para los modos de tamaño de pestañas Normal y Fixed. El modo FillToRight y la disposición de las pestañas en la parte inferior, izquierda y derecha en los tres modos de tamaño de las pestañas se implementarán en artículos posteriores, además del modo de desplazamiento para las pestañas dispuestas en una fila con el modo multilínea desactivado.

Para interactuar con un campo de pestaña previamente implementado como objeto contenedor de la clase CContainer, vamos a crear un nuevo objeto TabField, que es heredero del objeto contenedor con sus propias propiedades y métodos para trabajar completamente con el campo de la pestaña.


Mejorando las clases de la biblioteca

En el archivo de la biblioteca \MQL5\Include\DoEasy\Defines.mqh, añadiremos un nuevo tipo de objeto auxiliar WinForms a la lista de tipos de elementos gráficos:

//+------------------------------------------------------------------+
//| The list of graphical element types                              |
//+------------------------------------------------------------------+
enum ENUM_GRAPH_ELEMENT_TYPE
  {
   GRAPH_ELEMENT_TYPE_STANDARD,                       // Standard graphical object
   GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED,              // Extended standard graphical object
   GRAPH_ELEMENT_TYPE_SHADOW_OBJ,                     // Shadow object
   GRAPH_ELEMENT_TYPE_ELEMENT,                        // Element
   GRAPH_ELEMENT_TYPE_FORM,                           // Form
   GRAPH_ELEMENT_TYPE_WINDOW,                         // Window
   //--- WinForms
   GRAPH_ELEMENT_TYPE_WF_UNDERLAY,                    // Panel object underlay
   GRAPH_ELEMENT_TYPE_WF_BASE,                        // Windows Forms Base
   //--- 'Container' object types are to be set below
   GRAPH_ELEMENT_TYPE_WF_CONTAINER,                   // Windows Forms container base object
   GRAPH_ELEMENT_TYPE_WF_PANEL,                       // Windows Forms Panel
   GRAPH_ELEMENT_TYPE_WF_GROUPBOX,                    // Windows Forms GroupBox
   GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,                 // Windows Forms TabControl
   //--- 'Standard control' object types are to be set below
   GRAPH_ELEMENT_TYPE_WF_COMMON_BASE,                 // Windows Forms base standard control
   GRAPH_ELEMENT_TYPE_WF_LABEL,                       // Windows Forms Label
   GRAPH_ELEMENT_TYPE_WF_BUTTON,                      // Windows Forms Button
   GRAPH_ELEMENT_TYPE_WF_CHECKBOX,                    // Windows Forms CheckBox
   GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,                 // Windows Forms RadioButton
   GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX,           // Base list object of Windows Forms elements
   GRAPH_ELEMENT_TYPE_WF_LIST_BOX,                    // Windows Forms ListBox
   GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,            // Windows Forms CheckedListBox
   GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX,             // Windows Forms ButtonListBox
   //--- Auxiliary elements of WinForms objects
   GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM,               // Windows Forms ListBoxItem
   GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,                  // Windows Forms TabHeader
   GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,                   // Windows Forms TabField
  };
//+------------------------------------------------------------------+

Como este objeto no funcionará como una unidad independiente, entonces será un objeto auxiliar, y funcionará como parte del control TabControl, junto con el propio objeto auxiliar TabHeader que creamos en el último artículo.

Vamos a añadir una nueva enumeración con los modos de configuración del tamaño de las pestañas:

//+------------------------------------------------------------------+
//| Location of an object inside a control                           |
//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_ALIGNMENT
  {
   CANV_ELEMENT_ALIGNMENT_TOP,                        // Top
   CANV_ELEMENT_ALIGNMENT_BOTTOM,                     // Bottom
   CANV_ELEMENT_ALIGNMENT_LEFT,                       // Left
   CANV_ELEMENT_ALIGNMENT_RIGHT,                      // Right
  };
//+------------------------------------------------------------------+
//| Tab size setting mode                                            |
//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_TAB_SIZE_MODE
  {
   CANV_ELEMENT_TAB_SIZE_MODE_NORMAL,                 // By tab title width
   CANV_ELEMENT_TAB_SIZE_MODE_FIXED,                  // Fixed size
   CANV_ELEMENT_TAB_SIZE_MODE_FILL,                   // By TabControl width
  };
//+------------------------------------------------------------------+
//| Integer properties of the graphical element on the canvas        |
//+------------------------------------------------------------------+


Al final de la lista de propiedades de tipo entero del elemento gráfico en el lienzo, añadiremos dos nuevas propiedades y aumentaremos el número de propiedades de tipo entero de 88 a 90:

//+------------------------------------------------------------------+
//| 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_TAB_MULTILINE,                   // Several lines of tabs in TabControl
   CANV_ELEMENT_PROP_TAB_ALIGNMENT,                   // Location of tabs inside the control
   CANV_ELEMENT_PROP_TAB_SIZE_MODE,                   // Tab size setting mode
   CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,                 // Tab index number
   CANV_ELEMENT_PROP_ALIGNMENT,                       // Location of an object inside the control
   
  };
#define CANV_ELEMENT_PROP_INTEGER_TOTAL (90)          // Total number of integer properties
#define CANV_ELEMENT_PROP_INTEGER_SKIP  (0)           // Number of integer properties not used in sorting
//+------------------------------------------------------------------+


Vamos a añadir a la enumeración de posibles criterios de clasificación de los objetos gráficos en el lienzo estas nuevas propiedades:

//+------------------------------------------------------------------+
//| 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_TAB_MULTILINE,                // Sort by the flag of several rows of tabs in TabControl
   SORT_BY_CANV_ELEMENT_TAB_ALIGNMENT,                // Sort by the location of tabs inside the control
   SORT_BY_CANV_ELEMENT_TAB_SIZE_MODE,                // Sort by the mode of setting the tab size
   SORT_BY_CANV_ELEMENT_TAB_PAGE_NUMBER,              // Sort by the tab index number
   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 clasificar y filtrar las listas, así como seleccionar los objetos según estas nuevas propiedades.

Como iremos añadiendo más y más propiedades nuevas, pero no para todos los objetos gráficos, queremos dejar claro que por ahora todas las propiedades recién añadidas estarán disponibles en todos los objetos, pero más adelante, limitaremos su disponibilidad para los objetos en los que tales propiedades no deberían encontrarse, simplemente añadiendo métodos a las clases de estos objetos que retornan una bandera para ofrecer soporte a tal o cual propiedad. Estos métodos son virtuales, los hemos tenido durante mucho tiempo en las clases básicas de la biblioteca para cada objeto. Aquí no los añadimos para cada nuevo objeto por una sencilla razón: cuando consideremos que todos los objetos han sido creados y es el momento de limpiar la disponibilidad de las propiedades, lo haremos todo de una vez, asegurándonos de que todas las propiedades resulten claramente visibles para cada uno de los objetos de control, en su correspondiente panel de propiedades creado en el gráfico.


Ya hemos añadido nuevas propiedades y enumeraciones. Ahora añadiremos los textos para mostrar sus descripciones.

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

   MSG_LIB_TEXT_TOP,                                  // Top
   MSG_LIB_TEXT_BOTTOM,                               // Bottom
   MSG_LIB_TEXT_LEFT,                                 // Left
   MSG_LIB_TEXT_RIGHT,                                // Right
   
   MSG_LIB_TEXT_TAB_SIZE_MODE_NORMAL,                 // By tab title width
   MSG_LIB_TEXT_TAB_SIZE_MODE_FILL,                   // By TabControl width
   MSG_LIB_TEXT_TAB_SIZE_MODE_FIXED,                  // Fixed size
   
   MSG_LIB_TEXT_CORNER_LEFT_UPPER,                    // Center of coordinates at the upper left corner of the chart
   MSG_LIB_TEXT_CORNER_LEFT_LOWER,                    // Center of coordinates at the lower left corner of the chart
   MSG_LIB_TEXT_CORNER_RIGHT_LOWER,                   // Center of coordinates at the lower right corner of the chart
   MSG_LIB_TEXT_CORNER_RIGHT_UPPER,                   // Center of coordinates at the upper right corner of the chart

...

   MSG_GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX,         // ButtonListBox control
   MSG_GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,              // Tab header
   MSG_GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,               // Tab field
   MSG_GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,             // TabControl
   MSG_GRAPH_OBJ_BELONG_PROGRAM,                      // Graphical object belongs to a program

...

//--- CButtonListBox
   MSG_BUTT_LIST_ERR_FAILED_SET_GROUP_BUTTON,         // Failed to set the group for the button with the index 
   MSG_BUTT_LIST_ERR_FAILED_SET_TOGGLE_BUTTON,        // Failed to set the Toggle flag to the button with the index 

//--- CTabControl
   MSG_ELM_LIST_ERR_FAILED_GET_TAB_OBJ,               // Failed to get TabControl tab

//--- Integer properties of graphical elements

...

   MSG_CANV_ELEMENT_PROP_TAB_MULTILINE,               // Several lines of tabs in the control
   MSG_CANV_ELEMENT_PROP_TAB_ALIGNMENT,               // Location of tabs inside the control
   MSG_CANV_ELEMENT_PROP_TAB_SIZE_MODE,               // Tab size setting mode
   MSG_CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,             // Tab index number
   MSG_CANV_ELEMENT_PROP_ALIGNMENT,                   // Location of an object inside the control
//--- Real properties of graphical elements

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

   {"Сверху","Top"},
   {"Снизу","Bottom"},
   {"Слева","Left"},
   {"Справа","Right"},
   
   {"По ширине текста заголовка вкладки","Fit to tab title text width"},
   {"По ширине элемента управления TabControl","Fit TabControl Width"},
   {"Фиксированный размер","Fixed size"},
   
   {"Центр координат в левом верхнем углу графика","Center of coordinates is in the upper left corner of the chart"},
   {"Центр координат в левом нижнем углу графика","Center of coordinates is in the lower left corner of the chart"},
   {"Центр координат в правом нижнем углу графика","Center of coordinates is in the lower right corner of the chart"},
   {"Центр координат в правом верхнем углу графика","Center of coordinates is in the upper right corner of the chart"},

...

   {"Элемент управления \"ButtonListBox\"","Control element \"ButtonListBox\""},
   {"Заголовок вкладки","Tab header"},
   {"Поле вкладки","Tab field"},
   {"Элемент управления \"TabControl\"","Control element \"TabControl\""},
   {"Графический объект принадлежит программе","The graphic object belongs to the program"},

...

//--- CButtonListBox
   {"Не удалось установить группу кнопке с индексом ","Failed to set group for button with index "},
   {"Не удалось установить флаг \"Переключатель\" кнопке с индексом ","Failed to set the \"Toggle\" flag on the button with index "},
   
//--- CTabControl
   {"Не удалось получить вкладку элемента управления TabControl","Failed to get tab of TabControl"},
   
//--- Integer properties of graphical elements

...

   {"Несколько рядов вкладок в элементе управления","Multiple rows of tabs in a control"},
   {"Местоположение вкладок внутри элемента управления","Location of tabs inside the control"},
   {"Режим установки размера вкладок","Tab Size Mode"},
   {"Порядковый номер вкладки","Tab ordinal number"},
   {"Местоположение объекта внутри элемента управления","Location of the object inside the control"},
   
//--- String properties of graphical elements


Como tenemos un nuevo modo para dibujar los encabezados de las pestañas, tendremos que implementar el retorno de la descripción del modo seleccionado. En el archivo de funciones de servicio de la biblioteca \MQL5\Include\DoEasy\Services\DELib.mqh, escribiremos una función que retornará la descripción del modo de configuración del tamaño de la pestaña:

//+------------------------------------------------------------------+
//| Return the description of the tab size setting mode              |
//+------------------------------------------------------------------+
string TabSizeModeDescription(ENUM_CANV_ELEMENT_TAB_SIZE_MODE mode)
  {
   switch(mode)
     {
      case CANV_ELEMENT_TAB_SIZE_MODE_NORMAL :  return CMessage::Text(MSG_LIB_TEXT_TAB_SIZE_MODE_NORMAL);   break;
      case CANV_ELEMENT_TAB_SIZE_MODE_FIXED  :  return CMessage::Text(MSG_LIB_TEXT_TAB_SIZE_MODE_FIXED);    break;
      case CANV_ELEMENT_TAB_SIZE_MODE_FILL   :  return CMessage::Text(MSG_LIB_TEXT_TAB_SIZE_MODE_FILL);     break;
      default                                :  return "Unknown"; break;
     }
  }
//+------------------------------------------------------------------+

El modo de configuración del tamaño de las pestañas se transmitirá a la función y, dependiendo de este, se retornará el mensaje de texto correspondiente.


En el archivo de clase del objeto gráfico básico de la librería \MQL5\Include\DoEasy\Objects\Graph\GBaseObj.mqh, en su método para retornar la descripción del tipo de elemento gráfico, crearemos una sección para objetos auxiliares, trasladaremos allí el retorno de la descripción del encabezado de la pestaña y añadiremos el retorno del nuevo tipo, la descripción del campo de la pestaña:

//+------------------------------------------------------------------+
//| Return the description of the graphical element type             |
//+------------------------------------------------------------------+
string CGBaseObj::TypeElementDescription(const ENUM_GRAPH_ELEMENT_TYPE type)
  {
   return
     (
      type==GRAPH_ELEMENT_TYPE_STANDARD               ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_STANDARD)              :
      type==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED      ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)     :
      type==GRAPH_ELEMENT_TYPE_ELEMENT                ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_ELEMENT)               :
      type==GRAPH_ELEMENT_TYPE_SHADOW_OBJ             ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_SHADOW_OBJ)            :
      type==GRAPH_ELEMENT_TYPE_FORM                   ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_FORM)                  :
      type==GRAPH_ELEMENT_TYPE_WINDOW                 ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WINDOW)                :
      //--- WinForms
      type==GRAPH_ELEMENT_TYPE_WF_UNDERLAY            ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_UNDERLAY)           :
      type==GRAPH_ELEMENT_TYPE_WF_BASE                ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BASE)               :
      //--- Containers
      type==GRAPH_ELEMENT_TYPE_WF_CONTAINER           ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CONTAINER)          :
      type==GRAPH_ELEMENT_TYPE_WF_GROUPBOX            ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_GROUPBOX)           :
      type==GRAPH_ELEMENT_TYPE_WF_PANEL               ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_PANEL)              :
      type==GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL         ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL)        :
      //--- Standard controls
      type==GRAPH_ELEMENT_TYPE_WF_COMMON_BASE         ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_COMMON_BASE)        :
      type==GRAPH_ELEMENT_TYPE_WF_LABEL               ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LABEL)              :
      type==GRAPH_ELEMENT_TYPE_WF_CHECKBOX            ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CHECKBOX)           :
      type==GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON         ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON)        :
      type==GRAPH_ELEMENT_TYPE_WF_BUTTON              ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BUTTON)             :
      type==GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX   ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX)  :
      type==GRAPH_ELEMENT_TYPE_WF_LIST_BOX            ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LIST_BOX)           :
      type==GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM       ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM)      :
      type==GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX    ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX)   :
      type==GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX     ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX)    :
      //--- Auxiliary control objects
      type==GRAPH_ELEMENT_TYPE_WF_TAB_HEADER          ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_HEADER)         :
      type==GRAPH_ELEMENT_TYPE_WF_TAB_FIELD           ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_FIELD)          :
      "Unknown"
     );
  }  
//+------------------------------------------------------------------+


En el último artículo, al final, hablamos de la posibilidad de optimizar ligeramente el método que busca el número de objetos de un determinado tipo para crear su nombre. Allí observamos que los dos métodos que trabajan juntos llaman al método dos veces, creando un nombre de objeto a partir de la representación de la línea de la constante de enumeración que indica el tipo de objeto.

Para evitar la llamada doble del método, en el archivo \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh, donde están ubicados los métodos para crear los nombres de los elementos gráficos, en la clase protegida, añadiremos otro método: el método sobrecargado en el que vamos a transmitir el nombre del objeto anteriormente creado, y ahora conocido para su búsqueda entre otros objetos gráficos en el gráfico:

//--- Return the number of graphical elements (1) by type, (2) by name and type
   int               GetNumGraphElements(const ENUM_GRAPH_ELEMENT_TYPE type) const;
   int               GetNumGraphElements(const string name,const ENUM_GRAPH_ELEMENT_TYPE type) const;
//--- Create and return the graphical element name by its type
   string            CreateNameGraphElement(const ENUM_GRAPH_ELEMENT_TYPE type);
   
private:


Fuera del cuerpo de la clase, escribiremos su implementación:

//+------------------------------------------------------------------+
//| Return the number of graphical elements by type                  |
//+------------------------------------------------------------------+
int CGCnvElement::GetNumGraphElements(const ENUM_GRAPH_ELEMENT_TYPE type) const
  {
//--- Declare a variable with the number of graphical objects and
//--- get the total number of graphical objects on the chart and the subwindow where the graphical element is created
   int n=0, total=::ObjectsTotal(this.ChartID(),this.SubWindow());
//--- Create the name of a graphical object by its type
   string name=TypeGraphElementAsString(type);
//--- In the loop by all chart and subwindow objects,
   for(int i=0;i<total;i++)
     {
      //--- get the name of the next object
      string name_obj=::ObjectName(this.ChartID(),i,this.SubWindow());
      //--- if the object name does not contain the set prefix of the names of the library graphical objects, move on - this is not the object we are looking for
      if(::StringFind(name_obj,this.NamePrefix())==WRONG_VALUE)
         continue;
      //--- If the name of a graphical object selected in the loop has a substring with the created object name by its type,
      //--- then there is a graphical object of this type - increase the counter of objects of this type
      if(::StringFind(name_obj,name)>0)
         n++;
     }
//--- Return the number of found objects by their type
   return n;
  }
//+------------------------------------------------------------------+
//| Return the number of graphical elements by name and type         |
//+------------------------------------------------------------------+
int CGCnvElement::GetNumGraphElements(const string name,const ENUM_GRAPH_ELEMENT_TYPE type) const
  {
//--- Declare a variable with the number of graphical objects and
//--- get the total number of graphical objects on the chart and the subwindow where the graphical element is created
   int n=0, total=::ObjectsTotal(this.ChartID(),this.SubWindow());
//--- In the loop by all chart and subwindow objects,
   for(int i=0;i<total;i++)
     {
      //--- get the name of the next object
      string name_obj=::ObjectName(this.ChartID(),i,this.SubWindow());
      //--- if the object name does not contain the set prefix of the names of the library graphical objects, move on - this is not the object we are looking for
      if(::StringFind(name_obj,this.NamePrefix())==WRONG_VALUE)
         continue;
      //--- If the name of a graphical object selected in the loop has a substring with the created object name by its type,
      //--- then there is a graphical object of this type - increase the counter of objects of this type
      if(::StringFind(name_obj,name)>0)
         n++;
     }
//--- Return the number of found objects by their type
   return n;
  }
//+------------------------------------------------------------------+

En comparación con su método "par", en el que el nombre del objeto se crea partiendo de su tipo, y después se usa en el nombre del elemento gráfico, aquí obtendremos el nombre en los parámetros de entrada del método, y ya después buscaremos en el nombre del objeto la subcadena que contenga el nombre transmitido al método.

En general, no es nada complicado, pero de esta manera, hemos eliminado una llamada al método de creación de nombres. Cuando el nombre no se conoce, llamamos al primer método; cuando se conoce, llamamos al segundo.

Antes, el método que creaba y retornaba el nombre de un elemento gráfico según su tipo daba lugar a una doble llamada al método que creaba un nombre de objeto a partir de su tipo: la primera vez dentro del método y la segunda dentro del método llamado, en el que también se llamaba al método para crear un nombre de objeto a partir de su tipo:

//+------------------------------------------------------------------+
//| Create and return the graphical element name by its type         |
//+------------------------------------------------------------------+
string CGCnvElement::CreateNameGraphElement(const ENUM_GRAPH_ELEMENT_TYPE type)
  {
   return this.NamePrefix()+TypeGraphElementAsString(type)+(string)this.GetNumGraphElements(type);

  }
//+------------------------------------------------------------------+

Ahora vamos a hacer un cambio en este método: crearemos un nombre de objeto, lo usaremos para construir una línea de retorno, y lo enviaremos al nuevo método sobrecargado para encontrar el número de objetos así:

//+------------------------------------------------------------------+
//| Create and return the graphical element name by its type         |
//+------------------------------------------------------------------+
string CGCnvElement::CreateNameGraphElement(const ENUM_GRAPH_ELEMENT_TYPE type)
  {
   string name=TypeGraphElementAsString(type);
   return this.NamePrefix()+name+(string)this.GetNumGraphElements(name,type);
  }
//+------------------------------------------------------------------+


Había un fallo ligeramente molesto en el método que creaba el objeto de elemento gráfico: cuando se daba un error al crear el objeto gráfico, el método siempre retornaba la ausencia de error (código 0), pero no se creaba ningún objeto. Solo podíamos intuir la existencia de un error partiendo de indicios indirectos. Esto se aplica más al desarrollo de las clases de elementos gráficos que a su uso por parte del usuario de la biblioteca, ya que todos los errores en la creación de los objetos son eliminados ya en la fase de desarrollo de la clase. Pero vamos a realizar un cambio para entender mejor la causa del error:

//+------------------------------------------------------------------+
//| Create the graphical element object                              |
//+------------------------------------------------------------------+
bool CGCnvElement::Create(const long chart_id,     // Chart ID
                          const int wnd_num,       // Chart subwindow
                          const int x,             // X coordinate
                          const int y,             // Y coordinate
                          const int w,             // Width
                          const int h,             // Height
                          const bool redraw=false) // Flag indicating the need to redraw
                         
  {
   ::ResetLastError();
   if(this.m_canvas.CreateBitmapLabel((chart_id==NULL ? ::ChartID() : chart_id),wnd_num,this.m_name,x,y,w,h,COLOR_FORMAT_ARGB_NORMALIZE))
     {
      this.Erase(CLR_CANV_NULL);
      this.m_canvas.Update(redraw);
      this.m_shift_y=(int)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_WINDOW_YDISTANCE,wnd_num);
      return true;
     }
   int err=::GetLastError();
   int code=(err==0 ? (w<1 ? MSG_CANV_ELEMENT_ERR_FAILED_SET_WIDTH : h<1 ? MSG_CANV_ELEMENT_ERR_FAILED_SET_HEIGHT : ERR_OBJECT_ERROR) : err);
   string subj=(w<1 ? "Width="+(string)w+". " : h<1 ? "Height="+(string)h+". " : "");
   CMessage::ToLog(DFUN_ERR_LINE+subj,code,true);
   
   return false;
  }
//+------------------------------------------------------------------+

Vamos a leer el código del último error en la variable (que siempre era cero cuando la clase CCanvas de la Biblioteca Estándar fallaba en la creación de un recurso gráfico); luego comprobaremos el código de error, y si es cero, comprobaremos la anchura y la altura del objeto creado. Si alguno de estos valores es inferior a uno, en el código de error se escribirá el código de mensaje correspondiente o bien un error general de creación de objetos gráficos. Si el código de error es distinto a cero, escribiremos el código de error en la variable. A continuación, crearemos una línea con una descripción adicional del código de error, una vez más, según los valores de anchura y altura pasados al método, y mostraremos un mensaje indicando el nombre del método con el número de línea, el mensaje adicional y la descripción del código de error.


Los objetos de elementos gráficos son todos herederos de la clase de objetos de formulario, que a su vez es también descendiente de otras clases, pero tiene la funcionalidad del ratón, por lo que todos los objetos de la interfaz gráfica de usuario se basarán en él de una forma u otra, y cada objeto adjunto a su objeto básico, es decir, creado a partir del objeto básico, heredará las propiedades de su creador. Estas propiedades también incluyen propiedades como la actividad, la visibilidad y la disponibilidad. Si el objeto a partir del cual se crea otro objeto adjunto está inactivo, es decir, no responde al cursor del ratón, entonces su objeto subordinado también deberá heredar este comportamiento (que podrá modificarse posteriormente). Si el objeto no está disponible (no está activo, y está en gris para indicar que está inactivo), el objeto subordinado también deberá estarlo. Y si el objeto es invisible, entonces el objeto subordinado también deberá estar oculto, lo cual también es natural.

En el archivo \MQL5\Include\DoEasy\Objects\Graph\Form.mqh de la clase de panel de objetos, en el método para crear un nuevo elemento adjunto y añadirlo a la lista de objetos adjuntos, escribiremos estas propiedades para todos los objetos subordinados (objetos creados a partir de sus padres):

//+------------------------------------------------------------------+
//| Create a new attached element                                    |
//| and add it to the list of bound objects                          |
//+------------------------------------------------------------------+
CGCnvElement *CForm::CreateAndAddNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                            const int x,
                                            const int y,
                                            const int w,
                                            const int h,
                                            const color colour,
                                            const uchar opacity,
                                            const bool activity)
  {
//--- If the type of a created graphical element is less than the "element", inform of that and return 'false'
   if(element_type<GRAPH_ELEMENT_TYPE_ELEMENT)
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_NOT_INTENDED),::StringSubstr(::EnumToString(element_type),19));
      return NULL;
     }
//--- Specify the element index in the list
   int num=this.m_list_elements.Total();
//--- Create a description of the default graphical element
   string descript=TypeGraphElementAsString(element_type);
//--- Get the screen coordinates of the object relative to the coordinate system of the base object
   int elm_x=x;
   int elm_y=y;
   this.GetCoords(elm_x,elm_y);
//--- Create a new graphical element
   CGCnvElement *obj=this.CreateNewGObject(element_type,num,descript,elm_x,elm_y,w,h,colour,opacity,false,activity);
   if(obj==NULL)
      return NULL;
//--- and add it to the list of bound graphical elements
   if(!this.AddNewElement(obj,elm_x,elm_y))
     {
      delete obj;
      return NULL;
     }
//--- Set the minimum properties for a bound graphical element
   obj.SetBackgroundColor(colour,true);
   obj.SetOpacity(opacity);
   obj.SetActive(activity);
   obj.SetMain(this.GetMain()==NULL ? this.GetObject() : this.GetMain());
   obj.SetBase(this.GetObject());
   obj.SetID(this.GetMaxIDAll()+1);
   obj.SetNumber(num);
   obj.SetCoordXRelative(obj.CoordX()-this.CoordX());
   obj.SetCoordYRelative(obj.CoordY()-this.CoordY());
   obj.SetZorder(this.Zorder(),false);
   obj.SetCoordXRelativeInit(obj.CoordXRelative());
   obj.SetCoordYRelativeInit(obj.CoordYRelative());
   obj.SetVisible(this.IsVisible(),false);
   obj.SetActive(this.Active());
   obj.SetEnabled(this.Enabled());
   return obj;
  }
//+------------------------------------------------------------------+

Ahora cada nuevo objeto adjunto creado heredará inmediatamente estas propiedades desde su objeto básico, y no tendremos un objeto inactivo que de repente creará un objeto subordinado inactivo, o un objeto oculto que de repente generará en el gráfico un objeto subordinado, etc.

De la misma forma, los manejadores de eventos del ratón deberán omitir los objetos inactivos u ocultos.

En el mismo archivo, en el último manejador de eventos del ratón, escribiremos varias líneas que prohibirán el procesamiento de un objeto oculto o inaccesible:

//+------------------------------------------------------------------+
//| Last mouse event handler                                         |
//+------------------------------------------------------------------+
void CForm::OnMouseEventPostProcessing(void)
  {
   if(!this.IsVisible() || !this.Enabled())
      return;
   ENUM_MOUSE_FORM_STATE state=this.GetMouseState();
   switch(state)
     {

Si el objeto es así, simplemente saldremos del método.


Ya hemos hecho que si un objeto tiene un marco, solo se dibuje si para el objeto en el método de borrado y rellenado con el color de fondo se establece la bandera de redibujado. Y eso no está bien. No siempre resultará necesario dibujar objetos con redibujado obligatorio de todo el gráfico, pero, en este caso, si la bandera de redibujado está desactivada, el objeto no tendrá ningún marco al llamar a este método. Sobre todo porque ya existe una condición para dibujar el marco: que su tipo haya sido establecido como no ausente. Por ello, en todas las clases que tengan métodos Erase(), eliminaremos la comprobación de la bandera de redibujado para mostrar el marco del objeto:

if(this.BorderStyle()!=FRAME_STYLE_NONE && redraw)

En el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh, escribiremos un método virtual vacío para dibujar el marco del objeto:

//+------------------------------------------------------------------+
//| Form object class                                                |
//+------------------------------------------------------------------+
class CWinFormBase : public CForm
  {
protected:
   color             m_fore_color_init;                        // Initial color of the control text
   color             m_fore_state_on_color_init;               // Initial color of the control text when the control is "ON"
private:
//--- Return the font flags
   uint              GetFontFlags(void);
public:
//--- Draw a frame
   virtual void      DrawFrame(void){}
//--- Return by type the (1) list, (2) the number of bound controls, (3) the bound control by index in the list
   CArrayObj        *GetListElementsByType(const ENUM_GRAPH_ELEMENT_TYPE type);
   int               ElementsTotalByType(const ENUM_GRAPH_ELEMENT_TYPE type);
   CGCnvElement     *GetElementByType(const ENUM_GRAPH_ELEMENT_TYPE type,const int index);

Hemos diseñado este método para utilizarlo en las clases derivadas en las que se debe dibujar un marco, de forma que este método pueda ser sobrescrito y usado en lugar de los métodos DrawFrameBevel(), DrawFrameFlat(), DrawFrameSimple() y DrawFrameStamp() de la clase del objeto de formulario, pues, al fin y al cabo, estos métodos están pensados para otra cosa: dibujar un marco específico para el objeto de formulario. Si queremos dibujar un marco único propio para un elemento gráfico, deberemos redefinir el método declarado aquí en esa clase y utilizarlo para dibujar el marco deseado.


Los métodos Erase() han eliminado la comprobación de las banderas de actualización para el dibujado del marco:

//+------------------------------------------------------------------+
//| Clear the element filling it with color and opacity              |
//+------------------------------------------------------------------+
void CWinFormBase::Erase(const color colour,const uchar opacity,const bool redraw=false)
  {
//--- Fill the element having the specified color and the redrawing flag
   CGCnvElement::Erase(colour,opacity,redraw);
//--- If the object has a frame, draw it
   if(this.BorderStyle()!=FRAME_STYLE_NONE)
      this.DrawFormFrame(this.BorderSizeTop(),this.BorderSizeBottom(),this.BorderSizeLeft(),this.BorderSizeRight(),this.BorderColor(),this.Opacity(),this.BorderStyle());
//--- Update the element having the specified redrawing flag
   this.Update(redraw);
  }
//+------------------------------------------------------------------+
//| Clear the element with a gradient fill                           |
//+------------------------------------------------------------------+
void CWinFormBase::Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false)
  {
//--- Fill the element having the specified color array and the redrawing flag
   CGCnvElement::Erase(colors,opacity,vgradient,cycle,redraw);
//--- If the object has a frame, draw it
   if(this.BorderStyle()!=FRAME_STYLE_NONE)
      this.DrawFormFrame(this.BorderSizeTop(),this.BorderSizeBottom(),this.BorderSizeLeft(),this.BorderSizeRight(),this.BorderColor(),this.Opacity(),this.BorderStyle());
//--- Update the element having the specified redrawing flag
   this.Update(redraw);
  }
//+------------------------------------------------------------------+

Ahora el marco siempre se dibujará si se especifica un tipo de marco para él. Esto se hará en todos los archivos de todas las clases que tengan métodos Erase.


Al final del método que retorna la descripción de una propiedad de tipo entero de un elemento, añadiremos bloques de código para retornar las nuevas propiedades de los elementos gráficos:

      property==CANV_ELEMENT_PROP_TAB_ALIGNMENT                ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_ALIGNMENT)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+AlignmentDescription((ENUM_CANV_ELEMENT_ALIGNMENT)this.GetProperty(property))
         )  :
      property==CANV_ELEMENT_PROP_TAB_SIZE_MODE                ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_SIZE_MODE)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+TabSizeModeDescription((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)this.GetProperty(property))
         )  :
      property==CANV_ELEMENT_PROP_TAB_PAGE_NUMBER              ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_PAGE_NUMBER)+
         (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))
         )  :
      ""
     );
  }
//+------------------------------------------------------------------+
//| Return the description of the control real property              |
//+------------------------------------------------------------------+

Ahora cualquier elemento gráfico podrá retornar una línea que describa la nueva propiedad especificada y su valor.


En algunos constructores protegidos de los elementos gráficos, al final del código del constructor, ahora tenemos una línea que hace que el objeto creado se redibuje:

this.Redraw(false);

Y eso no está bien. El objeto solo deberá ser redibujado después de su creación final, no en cada constructor sucesivo de toda la jerarquía de herencia del objeto creado.

Si nos imaginamos una cadena de jerarquía de objetos: Obj0 --> Obj1 --> Obj2 --> Obj3 --> Obj4 --> ... ... --> ObjN, donde Obj0 es el primer objeto de la jerarquía, y ObjN es el último en ser creado, entonces, al crear este, todos los constructores de toda la cadena de herencia serán llamados por turno, y si hay una línea de actualización especificada en cada uno de ellos, el objeto se redibujará cada vez.

Vamos a eliminar estas líneas de todos los constructores protegidos de todas las clases.

Como ejemplo, en el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\CommonBase.mqh:

//+------------------------------------------------------------------+
//| Protected constructor with an object type,                       |
//| chart ID and subwindow                                           |
//+------------------------------------------------------------------+
CCommonBase::CCommonBase(const ENUM_GRAPH_ELEMENT_TYPE type,
                         const long chart_id,
                         const int subwindow,
                         const string descript,
                         const int x,
                         const int y,
                         const int w,
                         const int h) : CWinFormBase(type,chart_id,subwindow,descript,x,y,w,h)
  {
//--- Set the specified graphical element type for the object and assign the library object type to the current object
   this.SetTypeElement(type);
   this.m_type=OBJECT_DE_TYPE_GWF_COMMON;
   this.SetCoordX(x);
   this.SetCoordY(y);
   this.SetWidth(w);
   this.SetHeight(h);
   this.Initialize();
   if(this.AutoSize())
      this.AutoSetWH();
   this.SetWidthInit(this.Width());
   this.SetHeightInit(this.Height());
   this.SetCoordXInit(x);
   this.SetCoordYInit(y);
   this.Redraw(false);
  }
//+------------------------------------------------------------------+

Ya se han realizado los mismos cambios en las clases:
CLabel
en el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Label.mqh y CButton en el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Button.mqh.

Aquí, en el mismo archivo de la clase CCommonBase, vamos a hacer los cambios anteriores en los métodos Erase() para eliminar la comprobación de la bandera de redibujado:

//+------------------------------------------------------------------+
//| Clear the element filling it with color and opacity              |
//+------------------------------------------------------------------+
void CCommonBase::Erase(const color colour,const uchar opacity,const bool redraw=false)
  {
//--- Fill the element having the specified color and the redrawing flag
   CGCnvElement::Erase(colour,opacity,redraw);
//--- If the object has a frame, draw it
   if(this.BorderStyle()!=FRAME_STYLE_NONE)
      this.DrawFormFrame(this.BorderSizeTop(),this.BorderSizeBottom(),this.BorderSizeLeft(),this.BorderSizeRight(),this.BorderColor(),255,this.BorderStyle());
//--- Update the element having the specified redrawing flag
   this.Update(redraw);
  }
//+------------------------------------------------------------------+
//| Clear the element with a gradient fill                           |
//+------------------------------------------------------------------+
void CCommonBase::Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false)
  {
//--- Fill the element having the specified color array and the redrawing flag
   CGCnvElement::Erase(colors,opacity,vgradient,cycle,redraw);
//--- If the object has a frame, draw it
   if(this.BorderStyle()!=FRAME_STYLE_NONE)
      this.DrawFormFrame(this.BorderSizeTop(),this.BorderSizeBottom(),this.BorderSizeLeft(),this.BorderSizeRight(),this.BorderColor(),255,this.BorderStyle());
//--- Update the element having the specified redrawing flag
   this.Update(redraw);
  }
//+------------------------------------------------------------------+

No vamos a describir estos cambios más adelante en los otros archivos de clase.


Como, para dibujar el marco del objeto, ahora no tenemos que enviar forzosamente al método la bandera de redibujado establecida, entonces, en el archivo de la clase de objeto de botón \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Button.mqh, en el método de redibujado del objeto, indicaremos de forma no forzosa true, como se hacía antes, cosa que provocaba el redibujado de todo el gráfico, mientras que transmitiremos la bandera redraw, que a su vez se pasa al método desde fuera; de ella dependerá la necesidad de redibujar:

//+------------------------------------------------------------------+
//| Redraw the object                                                |
//+------------------------------------------------------------------+
void CButton::Redraw(bool redraw)
  {
//--- Fill the object with the background color
   this.Erase(this.BackgroundColor(),this.Opacity(),redraw);
//--- Declare the variables for X and Y coordinates and set their values depending on the text alignment
   int x=0,y=0;
   CLabel::SetTextParamsByAlign(x,y);
//--- Draw the text within the set coordinates of the object and the binding point of the text, and update the object 
   this.Text(x,y,this.Text(),this.ForeColor(),this.ForeColorOpacity(),this.TextAnchor());
   this.Update(redraw);
  }
//+------------------------------------------------------------------+


En el método que establece el estado del botón como "pulsado" para todos los «Button» del mismo grupo en el contenedor, al final, añadiremos el redibujado del gráfico en el que se ha creado el objeto de botón, para mostrar inmediatamente los cambios después de que todos los botones hayan sido procesados:

//+------------------------------------------------------------------+
//| Sets the state of the button to "released"                       |
//| for all Buttons of the same group in the container               |
//+------------------------------------------------------------------+
void CButton::UnpressOtherAll(void)
  {
//--- Get the pointer to the base object
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
//--- Get the list of all objects of a certain type from the base object (Button or its descendant)
   CArrayObj *list=base.GetListElementsByType(this.TypeGraphElement());
//--- Select all objects from the received list, except for the given one (the names of the selected objects are not equal to the name of this one)
   list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,this.Name(),NO_EQUAL);
//--- From the received list, select only those objects whose group index matches the group of the current one
   list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_GROUP,this.Group(),EQUAL);
//--- If the list of objects is received,
   if(list!=NULL)
     {
      //--- in the loop through all objects in the list
      for(int i=0;i<list.Total();i++)
        {
         //--- get the next object,
         CButton *obj=list.At(i);
         if(obj==NULL)
            continue;
         //--- set the button status to "released",
         obj.SetState(false);
         //--- set the background color to the original one (the cursor is on another button outside this one)
         obj.SetBackgroundColor(obj.BackgroundColorInit(),false);
         obj.SetForeColor(obj.ForeColorInit(),false);
         obj.SetBorderColor(obj.BorderColorInit(),false);
         //--- Redraw the object to display the changes
         obj.Redraw(false);
        }
     }
   ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+


En el último manejador de eventos del ratón, al principio del evento, añadiremos una comprobación sobre la invisibilidad o inaccesibilidad del elemento, como hicimos anteriormente para el objeto formulario:

//+------------------------------------------------------------------+
//| Last mouse event handler                                         |
//+------------------------------------------------------------------+
void CButton::OnMouseEventPostProcessing(void)
  {
   if(!this.IsVisible() || !this.Enabled())
      return;
   ENUM_MOUSE_FORM_STATE state=GetMouseState();
   switch(state)
     {

Ya hemos realizado las mismas mejoras en las clases:
CCheckBox en el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\CheckBox.mqh y CTabHeader en el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\TabHeader.mqh.

No vamos a analizar más esos cambios.


El objeto TabField - El campo de pestaña del control TabControl

Para el control TabControl, en el artículo anterior creamos la clase de objeto de encabezado auxiliar de la pestaña TabHeader. La clase se hereda del objeto de botón, ya que replica casi toda su funcionalidad. Dicho encabezado está directamente vinculado al campo de la pestaña con el que conforman una pestaña, mientras que el propio control se compone de al menos dos pestañas de este tipo.

En el último artículo, usamos un objeto contenedor para implementar el campo de la pestaña. Se trata del objeto básico para todos los objetos contenedores de la biblioteca. El campo de la pestaña deberá contener objetos subordinados creados en este campo y, en consecuencia, subordinados a él. Obviamente, la funcionalidad del objeto contenedor básico no basta para implementar el campo. Por ello, basándonos en el objeto contenedor básico, crearemos una nueva clase de objeto de campo de pestaña.

En la carpeta de la biblioteca \MQL5\Include\DoEasy\Objects\Graph\WForms\, crearemos el nuevo archivo TabField.mqh de la clase CTabField. La clase deberá ser heredada de la clase básica del objeto contenedor. El archivo de objetos de panel deberá adjuntarse al archivo de clase creado, dándole acceso a todos los archivos de objetos gráficos de la biblioteca:

//+------------------------------------------------------------------+
//|                                                     TabField.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\Panel.mqh"
//+------------------------------------------------------------------+
//| TabHeader object class of WForms TabControl                      |
//+------------------------------------------------------------------+
class CTabField : public CContainer
  {
  }


En la sección privada, declararemos el método que retorna el puntero al objeto de encabezado correspondiente a este campo, así como un método virtual para crear elementos gráficos adjuntos a la pestaña (a este objeto de campo):

//+------------------------------------------------------------------+
//| TabHeader object class of WForms TabControl                      |
//+------------------------------------------------------------------+
class CTabField : public CContainer
  {
private:
//--- Find and return a pointer to the header object corresponding to the number of this tab
   CWinFormBase     *GetHeaderObj(void);
//--- Create a new graphical object
   virtual CGCnvElement *CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                          const int element_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,
                                          const bool activity);
protected:

Porque, si en este archivo especificamos el tipo de objeto de encabezado exactamente como su tipo (CTabHeader), que es visible en esta clase, e intentamos compilar toda la biblioteca compilando la clase principal de la biblioteca CEngine, obtendremos un montón de errores y advertencias sobre el tipo desconocido de CTabHeader. No vamos a calentarnos la cabeza buscando qué se ha «atascado» y dónde en todos los recovecos de todos los archivos de la biblioteca, en cambio, declararemos el tipo retornado como objeto básico de todos los objetos WinForms de la biblioteca. Esto nos bastará para trabajar aquí. Ya fuera de esta clase, podremos obtenerlo desde aquí con su tipo correcto.

El método virtual para crear elementos gráficos adjuntos lo necesitaremos para poder crear objetos subordinados en el campo al acceder a él.

En la sección protegida de la clase, declararemos un constructor protegido:

protected:
//--- Protected constructor with object type, chart ID and subwindow
                     CTabField(const ENUM_GRAPH_ELEMENT_TYPE type,
                               const long chart_id,
                               const int subwindow,
                               const string descript,
                               const int x,
                               const int y,
                               const int w,
                               const int h);
public:


En la sección pública, declararemos los métodos para trabajar con la clase, así como un constructor paramétrico:

public:
//--- Draws a field frame depending on the location of the header
   virtual void      DrawFrame(void);
//--- (1) Set and (2) return the tab index
   void              SetPageNumber(const int value)         { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,value);         }
   int               PageNumber(void)                 const { return (int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER);   }
//--- Clear the element filling it with color and opacity
   virtual void      Erase(const color colour,const uchar opacity,const bool redraw=false);
//--- Clear the element with a gradient fill
   virtual void      Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false);
//--- Constructors
                     CTabField(const long chart_id,
                               const int subwindow,
                               const string descript,
                               const int x,
                               const int y,
                               const int w,
                               const int h);
  };
//+------------------------------------------------------------------+

Aquí, todos los métodos virtuales redefinen los métodos de la clase padre del mismo nombre, mientras que los métodos para establecer y retornar el número de pestaña al que pertenece el campo simplemente establecen el valor transmitido a la propiedad del objeto, y luego lo retornan.


Aquí tenemos los constructores protegido y paramétrico:

//+------------------------------------------------------------------+
//| Protected constructor with an object type,                       |
//| chart ID and subwindow                                           |
//+------------------------------------------------------------------+
CTabField::CTabField(const ENUM_GRAPH_ELEMENT_TYPE type,
                     const long chart_id,
                     const int subwindow,
                     const string descript,
                     const int x,
                     const int y,
                     const int w,
                     const int h) : CContainer(type,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD);
   this.m_type=OBJECT_DE_TYPE_GWF_CONTAINER;
   this.SetBorderSizeAll(1);
   this.SetBorderStyle(FRAME_STYLE_SIMPLE);
   this.SetOpacity(CLR_DEF_CONTROL_TAB_PAGE_OPACITY,true);
   this.SetBackgroundColor(CLR_DEF_CONTROL_TAB_PAGE_BACK_COLOR,true);
   this.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_DOWN);
   this.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_OVER);
   this.SetBorderColor(CLR_DEF_CONTROL_TAB_PAGE_BORDER_COLOR,true);
   this.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_DOWN);
   this.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_OVER);
   this.SetForeColor(CLR_DEF_FORE_COLOR,true);
   this.SetPaddingAll(3);
  }
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTabField::CTabField(const long chart_id,
                     const int subwindow,
                     const string descript,
                     const int x,
                     const int y,
                     const int w,
                     const int h) : CContainer(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD);
   this.m_type=OBJECT_DE_TYPE_GWF_CONTAINER;
   this.SetBorderSizeAll(1);
   this.SetBorderStyle(FRAME_STYLE_SIMPLE);
   this.SetOpacity(CLR_DEF_CONTROL_TAB_PAGE_OPACITY,true);
   this.SetBackgroundColor(CLR_DEF_CONTROL_TAB_PAGE_BACK_COLOR,true);
   this.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_DOWN);
   this.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_OVER);
   this.SetBorderColor(CLR_DEF_CONTROL_TAB_PAGE_BORDER_COLOR,true);
   this.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_DOWN);
   this.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_OVER);
   this.SetForeColor(CLR_DEF_FORE_COLOR,true);
   this.SetPaddingAll(3);
  }
//+------------------------------------------------------------------+

La única diferencia entre ellos reside en que en el constructor protegido se transmite el tipo de objeto creado (si se hereda de él), y este tipo se transmite al objeto padre. En el constructor paramétrico público, se especifica explícitamente el tipo transmitido a la clase padre: el objeto de campo.

En el cuerpo del constructor, el objeto creado se establecerá con los valores por defecto deseados de algunas propiedades. El resto de las propiedades de los objetos se establecerán en las clases padre.

Método que encuentra y retorna el puntero al encabezado correspondiente al número de pestaña:

//+------------------------------------------------------------------+
//| Find and return a pointer to the header                          |
//| corresponding to the tab index                                   |
//+------------------------------------------------------------------+
CWinFormBase *CTabField::GetHeaderObj(void)
  {
//--- Get the pointer to the base object
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return NULL;
//--- From the base object, get the list of tab header objects
   CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
//--- Leave only the object whose tab index matches this one in the obtained list
   list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,this.PageNumber(),EQUAL);
//--- Return the pointer to the found object from the list
   return(list!=NULL ? list.At(0) : NULL);
  }
//+------------------------------------------------------------------+

El método se comenta con detalle en los comentarios al código. Resumiendo: para comparar este campo con su encabezado almacenado en la clase a la que este objeto y sus objetos de encabezado están unidos, necesitaremos acceder al objeto básico. Hace tiempo que tenemos un método de este tipo. A continuación, obtendremos el puntero al objeto subyacente, y de ahí obtendremos una lista de todos los objetos adjuntos con el tipo de objeto de encabezado de la pestaña. Luego filtraremos la lista de forma que quede solo el objeto con el número de pestaña escrito en el objeto. Así encontramos el encabezado correspondiente a este campo, mientras que el puntero al mismo se almacenará en la lista resultante. Eso es lo que estamos retornando. Si no encontramos ningún objeto, el método retornará NULL.


Método que dibuja el marco de un elemento dependiendo de la ubicación del encabezado:

//+------------------------------------------------------------------+
//| Draw the element frame depending on the header position          |
//+------------------------------------------------------------------+
void CTabField::DrawFrame(void)
  {
//--- Set the initial coordinates
   int x1=0;
   int y1=0;
   int x2=this.Width()-1;
   int y2=this.Height()-1;
//--- Get the tab header corresponding to the field
   CTabHeader *header=this.GetHeaderObj();
   if(header==NULL)
      return;
//--- Draw a rectangle that completely outlines the field
   this.DrawRectangle(x1,y1,x2,y2,this.BorderColor(),this.Opacity());
//--- Depending on the location of the header, draw a line on the edge adjacent to the header.
//--- The line size is calculated from the heading size and corresponds to it with a one-pixel indent on each side
//--- thus, visually the edge will not be drawn on the adjacent side of the header
   switch(header.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
        this.DrawLine(header.CoordXRelative()+1,0,header.RightEdgeRelative()-2,0,this.BackgroundColor(),this.Opacity());
        break;
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
        this.DrawLine(header.CoordXRelative()+1,this.Height()-1,header.RightEdgeRelative()-2,this.Height()-1,this.BackgroundColor(),this.Opacity());
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
        // { }
        break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
        // { }
        break;
      default:
        break;
     }
  }
//+------------------------------------------------------------------+

El método se comenta con detalle en los comentarios al código. Por el momento, el método dibujará un marco cuando los encabezados están en la parte superior e inferior. Los implementaremos a la derecha y a la izquierda en artículos posteriores. En pocas palabras: un encabezado, por ejemplo, se adjunta al campo en la parte superior. En el lugar donde se encuentren, no deberá haber una línea. Antes podíamos dibujar el borde de un campo usando una línea discontinua, pero existe algún problema con el número de puntos a dibujar dependiendo de la ubicación del encabezado. Si se encuentra en el lado izquierdo o derecho del campo, el número de puntos de la línea será uno menos que en el caso cuando el encabezado no se encuentra en el borde del campo.

Por ello, será más fácil dibujar primero un rectángulo que delimite completamente el campo y luego, una vez tengamos las coordenadas del encabezado, dibujar una línea en el color de fondo del campo donde el encabezado toca el campo. Esto "borrará" la línea en el lugar donde el encabezado toque el campo para obtener la representación correcta de la pestaña.

Métodos virtuales que limpian un elemento con rellenado de color y opacidad:

//+------------------------------------------------------------------+
//| Clear the element filling it with color and opacity              |
//+------------------------------------------------------------------+
void CTabField::Erase(const color colour,const uchar opacity,const bool redraw=false)
  {
//--- Fill the element having the specified color and the redrawing flag
   CGCnvElement::Erase(colour,opacity,redraw);
//--- If the object has a frame, draw it
   if(this.BorderStyle()!=FRAME_STYLE_NONE)
      this.DrawFrame();
//--- Update the element having the specified redrawing flag
   this.Update(redraw);
  }
//+------------------------------------------------------------------+
//| Clear the element with a gradient fill                           |
//+------------------------------------------------------------------+
void CTabField::Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false)
  {
//--- Fill the element having the specified color array and the redrawing flag
   CGCnvElement::Erase(colors,opacity,vgradient,cycle,redraw);
//--- If the object has a frame, draw it
   if(this.BorderStyle()!=FRAME_STYLE_NONE)
      this.DrawFrame();
//--- Update the element having the specified redrawing flag
   this.Update(redraw);
  }
//+------------------------------------------------------------------+

Los métodos son idénticos a los mismos métodos en otras clases, o las clases padre, y se redefinirán aquí para que el marco se dibuje usando exactamente el método que hemos discutido con anterioridad.

Método virtual para crear un nuevo objeto gráfico:

//+------------------------------------------------------------------+
//| Create a new graphical object                                    |
//+------------------------------------------------------------------+
CGCnvElement *CTabField::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                          const int obj_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,
                                          const bool activity)
  {
   CGCnvElement *element=NULL;
   switch(type)
     {
      case GRAPH_ELEMENT_TYPE_ELEMENT              :
         element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),descript,x,y,w,h,colour,opacity,movable,activity);
        break;
      case GRAPH_ELEMENT_TYPE_FORM                 :
         element=new CForm(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_CONTAINER         :
         element=new CContainer(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_GROUPBOX          :
         element=new CGroupBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_PANEL             :
         element=new CPanel(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_LABEL             :
         element=new CLabel(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKBOX          :
         element=new CCheckBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON       :
         element=new CRadioButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON            :
         element=new CButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX          :
         element=new CListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM     :
         element=new CListBoxItem(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX  :
         element=new CCheckedListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX   :
         element=new CButtonListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER        :
         element=new CTabHeader(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD         :
         element=new CTabField(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL       :
         element=new CTabControl(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      default:
        break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type));
   return element;
  }
//+------------------------------------------------------------------+

Método virtual. También es idéntico a los métodos del mismo nombre en otras clases. Aquí tenemos un nuevo bloque de código para crear el objeto de campo cuya clase estamos viendo. A continuación, añadiremos exactamente los mismos bloques de creación para este objeto a otras clases en los mismos métodos.

Con la ayuda de este método, podremos crear objetos subordinados en el campo de la pestaña.


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

Para ello, en la sección privada, eliminaremos el método para establecer el tamaño y el desplazamiento del objeto:

//--- Sets the width, height and shift of the element depending on the state
   void              SetWH(void);


Allí mismo, declararemos los métodos para establecer en la posición correcta la línea del encabezado de la pestaña que hemos seleccionado:

private:
   int               m_width_off;                        // Object width in the released state
   int               m_height_off;                       // Object height in the released state
   int               m_width_on;                         // Object width in the selected state
   int               m_height_on;                        // Object height in the selected state
   int               m_col;                              // Header column index
   int               m_row;                              // Header row index
//--- Adjust the size and location of the element depending on the state
   bool              WHProcessStateOn(void);
   bool              WHProcessStateOff(void);
//--- Draws a header frame depending on its position
   virtual void      DrawFrame(void);
//--- Set the string of the selected tab header to the correct position, (1) top, (2) bottom, (3) left and (4) right
   void              CorrectSelectedRowTop(void);
   void              CorrectSelectedRowBottom(void);
   void              CorrectSelectedRowLeft(void);
   void              CorrectSelectedRowRight(void);
   
protected:


En la sección pública, declararemos/escribiremos los nuevos métodos y mejoraremos los ya existentes:

public:
//--- Find and return a pointer to the field object corresponding to the tab index
   CWinFormBase     *GetFieldObj(void);
//--- Set the control size in the (1) released and (2) selected state
   bool              SetSizeOff(void)  { return(CGCnvElement::SetWidth(this.m_width_off) && CGCnvElement::SetHeight(this.m_height_off) ? true : false);  }
   bool              SetSizeOn(void)   { return(CGCnvElement::SetWidth(this.m_width_on) && CGCnvElement::SetHeight(this.m_height_on)   ? true : false);  }
//--- Sets the size of the control element
   void              SetWidthOff(const int value)                                            { this.m_width_off=value;  }
   void              SetHeightOff(const int value)                                           { this.m_height_off=value; }
   void              SetWidthOn(const int value)                                             { this.m_width_on=value;   }
   void              SetHeightOn(const int value)                                            { this.m_height_on=value;  }
//--- Set all sizes of the control element
   bool              SetSizes(const int w,const int h);
//--- Returns the control size
   int               WidthOff(void)                                                    const { return this.m_width_off; }
   int               HeightOff(void)                                                   const { return this.m_height_off;}
   int               WidthOn(void)                                                     const { return this.m_width_on;  }
   int               HeightOn(void)                                                    const { return this.m_height_on; }
//--- (1) Set and (2) return the index of the tab title row
   void              SetRow(const int value)                                                 { this.m_row=value;        }
   int               Row(void)                                                         const { return this.m_row;       }
//--- (1) Set and (2) return the index of the tab title column
   void              SetColumn(const int value)                                              { this.m_col=value;        }
   int               Column(void)                                                      const { return this.m_col;       }
//--- Set the tab location
   void              SetTabLocation(const int row,const int col)
                       {
                        this.SetRow(row);
                        this.SetColumn(col);
                       }
//--- (1) Sets and (2) return the location of the object on the control
   void              SetAlignment(const ENUM_CANV_ELEMENT_ALIGNMENT alignment)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT,alignment);
                       }
   ENUM_CANV_ELEMENT_ALIGNMENT Alignment(void)  const { return (ENUM_CANV_ELEMENT_ALIGNMENT)this.GetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT);  }
   
//--- (1) Set and (2) get the mode of setting the tab size
   void              SetTabSizeMode(const ENUM_CANV_ELEMENT_TAB_SIZE_MODE mode)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_TAB_SIZE_MODE,mode);
                       }
   ENUM_CANV_ELEMENT_TAB_SIZE_MODE TabSizeMode(void)const{ return (ENUM_CANV_ELEMENT_TAB_SIZE_MODE)this.GetProperty(CANV_ELEMENT_PROP_TAB_SIZE_MODE);}

//--- (1) Set and (2) return the tab index
   void              SetPageNumber(const int value)         { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,value);         }
   int               PageNumber(void)                 const { return (int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER);   }

//--- Sets the state of the control
   virtual void      SetState(const bool flag);

//--- Clear the element filling it with color and opacity
   virtual void      Erase(const color colour,const uchar opacity,const bool redraw=false);
//--- Clear the element with a gradient fill
   virtual void      Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false);

//--- Last mouse event handler
   virtual void      OnMouseEventPostProcessing(void);
   
//--- Set the gap (1) to the left, (2) at the top, (3) to the right, (4) at the bottom and (5) on all sides inside the control
   virtual void      SetPaddingLeft(const uint value)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_PADDING_LEFT,(value<1 ? 0 : value));
                       }
   virtual void      SetPaddingTop(const uint value)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_PADDING_TOP,(value<1 ? 0 : value));
                       }
   virtual void      SetPaddingRight(const uint value)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_PADDING_RIGHT,(value<1 ? 0 : value));
                       }
   virtual void      SetPaddingBottom(const uint value)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_PADDING_BOTTOM,(value<1 ? 0 : value));
                       }
   virtual void      SetPaddingAll(const uint value)
                       {
                        this.SetPaddingLeft(value); this.SetPaddingTop(value); this.SetPaddingRight(value); this.SetPaddingBottom(value);
                       }
   virtual void      SetPadding(const int left,const int top,const int right,const int bottom)
                       {
                        this.SetPaddingLeft(left); this.SetPaddingTop(top); this.SetPaddingRight(right); this.SetPaddingBottom(bottom);
                       }
protected:
//--- Protected constructor with object type, chart ID and subwindow

Antes, el método SetAlignment() establecía el tamaño del marco, además de establecer la propiedad. Aquí, el marco es siempre del mismo tamaño, 1 píxel, por lo que no deberemos cambiar nada, borraremos todo esto:

//--- (1) Sets and (2) return the location of the object on the control
   void              SetAlignment(const ENUM_CANV_ELEMENT_ALIGNMENT alignment)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT,alignment);
                        if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_TOP)
                           this.SetBorderSize(1,1,1,0);
                        if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_BOTTOM)
                           this.SetBorderSize(1,0,1,1);
                        if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT)
                           this.SetBorderSize(1,1,0,1);
                        if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_RIGHT)
                           this.SetBorderSize(0,1,1,1);
                       }


Los constructores de clase protegido y paramétrico:

//+------------------------------------------------------------------+
//| Protected constructor with an object type,                       |
//| chart ID and subwindow                                           |
//+------------------------------------------------------------------+
CTabHeader::CTabHeader(const ENUM_GRAPH_ELEMENT_TYPE type,
                       const long chart_id,
                       const int subwindow,
                       const string descript,
                       const int x,
                       const int y,
                       const int w,
                       const int h) : CButton(type,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
   this.m_type=OBJECT_DE_TYPE_GWF_COMMON;
   this.SetAlignment(CANV_ELEMENT_ALIGNMENT_TOP);
   this.SetToggleFlag(true);
   this.SetGroupButtonFlag(true);
   this.SetText(TypeGraphElementAsString(this.TypeGraphElement()));
   this.SetForeColor(CLR_DEF_FORE_COLOR,true);
   this.SetOpacity(CLR_DEF_CONTROL_TAB_HEAD_OPACITY,true);
   this.SetBackgroundColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR,true);
   this.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_DOWN);
   this.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_OVER);
   this.SetBackgroundStateOnColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR_ON,true);
   this.SetBackgroundStateOnColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BACK_DOWN_ON);
   this.SetBackgroundStateOnColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BACK_OVER_ON);
   this.SetBorderStyle(FRAME_STYLE_SIMPLE);
   this.SetBorderColor(CLR_DEF_CONTROL_TAB_HEAD_BORDER_COLOR,true);
   this.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_DOWN);
   this.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_OVER);
   this.SetPadding(6,3,6,3);
   this.SetSizes(w,h);
   this.SetState(false);
  }
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTabHeader::CTabHeader(const long chart_id,
                       const int subwindow,
                       const string descript,
                       const int x,
                       const int y,
                       const int w,
                       const int h) : CButton(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
   this.m_type=OBJECT_DE_TYPE_GWF_COMMON;
   this.SetAlignment(CANV_ELEMENT_ALIGNMENT_TOP);
   this.SetToggleFlag(true);
   this.SetGroupButtonFlag(true);
   this.SetText(TypeGraphElementAsString(this.TypeGraphElement()));
   this.SetForeColor(CLR_DEF_FORE_COLOR,true);
   this.SetOpacity(CLR_DEF_CONTROL_TAB_HEAD_OPACITY,true);
   this.SetBackgroundColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR,true);
   this.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_DOWN);
   this.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_OVER);
   this.SetBackgroundStateOnColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR_ON,true);
   this.SetBackgroundStateOnColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BACK_DOWN_ON);
   this.SetBackgroundStateOnColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BACK_OVER_ON);
   this.SetBorderStyle(FRAME_STYLE_SIMPLE);
   this.SetBorderColor(CLR_DEF_CONTROL_TAB_HEAD_BORDER_COLOR,true);
   this.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_DOWN);
   this.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_OVER);
   this.SetPadding(6,3,6,3);
   this.SetSizes(w,h);
   this.SetState(false);
  }
//+------------------------------------------------------------------+

Ahora, en lugar de establecer el tamaño del encabezado por separado:

   this.SetWidthOff(this.Width());
   this.SetHeightOff(this.Height());
   this.SetWidthOn(this.Width()+4);
   this.SetHeightOn(this.Height()+2);

llamaremos al método para establecer el tamaño del encabezado de la pestaña, en el que las dimensiones se establecen según el modo de indicación del encabezado:

//+------------------------------------------------------------------+
//| Set all header sizes                                             |
//+------------------------------------------------------------------+
bool CTabHeader::SetSizes(const int w,const int h)
  {
//--- If the passed width or height is less than 4 pixels, 
//--- make them equal to four pixels
   int width=(w<4 ? 4 : w);
   int height=(h<4 ? 4 : h);
//--- Depending on the header size setting mode
   switch(this.TabSizeMode())
     {
      //--- set the width and height for the Normal mode
      case CANV_ELEMENT_TAB_SIZE_MODE_NORMAL :
        this.TextSize(this.Text(),width,height);
        width+=this.PaddingLeft()+this.PaddingRight();
        height=h+this.PaddingTop()+this.PaddingBottom();
        break;
      //--- For the Fixed and Fill modes, the size remains
      //--- passed to the method and adjusted
      case CANV_ELEMENT_TAB_SIZE_MODE_FIXED  : break;
      //---CANV_ELEMENT_TAB_SIZE_MODE_FILL
      default: break;
     }
//--- Set the results of changing the width and height to 'res'
   bool res=true;
   res &=this.SetWidth(width);
   res &=this.SetHeight(height);
//--- If there is an error in changing the width or height, return 'false'
   if(!res)
      return false;
//--- Set the changed size for different button states
   this.SetWidthOn(this.Width()+4);
   this.SetHeightOn(this.Height()+2);
   this.SetWidthOff(this.Width());
   this.SetHeightOff(this.Height());
   return true;
  }
//+------------------------------------------------------------------+

La lógica del método se explica en los comentarios del código. El tamaño solo se ajustará para el modo en el que la anchura del encabezado coincide con la anchura del texto mostrado en él. Para el modo Fixed, el tamaño del encabezado deberá ser fijo, por lo que seguirá siendo el tamaño transmitido al método en las variables w y h, pero ajustado en el caso de que transmitamos tamaños inferiores a cuatro píxeles (en las variables de anchura y altura). El modo de expansión de la anchura según el tamaño del contenedor lo implementaremos en artículos posteriores.

Método que establece si el estado del control ha sufrido fuertes cambios:

//+------------------------------------------------------------------+
//| Set the state of the control                                     |
//+------------------------------------------------------------------+
void CTabHeader::SetState(const bool flag)
  {
//--- Get the button state and set the new one passed to the method
   bool state=this.State();
   CButton::SetState(flag);
//--- If the previous state of the button does not match the set
   if(state!=this.State())
     {
      //--- If the button is pressed
      if(this.State())
        {
         //--- Call the button resizing method and bring it to the foreground
         this.WHProcessStateOn();
         this.BringToTop();
         //--- Get the base object the tab title is attached to (TabControl)
         CWinFormBase *base=this.GetBase();
         if(base==NULL)
            return;
         //--- Set the index of the selected tab to the TabControl object
         base.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,this.PageNumber());
         //--- Get the list of tab field objects from the base object
         CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD);
         if(list==NULL)
            return;
         //--- In the loop through the received list, hide all fields that do not match the header
         for(int i=0;i<list.Total();i++)
           {
            //--- get the next tab field object
            CWinFormBase *obj=list.At(i);
            //--- If the object is not received or corresponds to the selected header, move on
            if(obj==NULL || obj.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER)==this.PageNumber())
               continue;
            //--- Set the ZOrder tab field as the base object and hide the field
            obj.SetZorder(base.Zorder(),false);
            obj.Hide();
           }
         //--- Get the field object corresponding to the field header (this object)
         CWinFormBase *field=this.GetFieldObj();
         if(field==NULL)
            return;
         //--- Display the field and set its ZOrder higher than other fields of the TabControl object,
         //--- draw the frame of the field object and bring it to the foreground
         field.Show();
         field.SetZorder(base.Zorder()+1,false);
         field.DrawFrame();
         field.BringToTop();
        }
      //--- If the button is not pressed, call the method to restore the title size
      else
         this.WHProcessStateOff();
     }
  }
//+------------------------------------------------------------------+

Si el botón está seleccionado (clicamos en el encabezado de la pestaña), deberemos aumentar el tamaño del botón (encabezado) y traer el encabezado al primer plano. A continuación, tendremos que ocultar todos los campos de la pestaña que no correspondan al encabezado seleccionado y, a la inversa, mostrar y traer a primer plano ese campo del encabezado. Además, el campo de la pestaña mostrada deberá ser clicable, por lo que su parámetro ZOrder deberá ser superior al del resto de los objetos del control, y para los campos no seleccionados, por el contrario, el ZOrder deberá ser inferior al del seleccionado. Precisamente todo eso es lo que hace este método.


En el método que ajusta el tamaño y la posición de un elemento en el estado "seleccionado" dependiendo de su ubicación, necesitaremos llamar a los métodos que desplazan la línea del encabezado seleccionado a la posición en la que el encabezado será unido a su campo, pues, en el caso de que se nos permita tener múltiples filas de encabezados, el encabezado seleccionado podrá estar en una fila no contigua con el campo:

//+------------------------------------------------------------------+
//| Adjust the element size and location                             |
//| in the "selected" state depending on its location                |
//+------------------------------------------------------------------+
bool CTabHeader::WHProcessStateOn(void)
  {
//--- If failed to set a new size, leave
   if(!this.SetSizeOn())
      return false;
//--- Get the base object
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return false;
//--- Depending on the title location,
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
        //--- Adjust the location of the row with the selected header
        this.CorrectSelectedRowTop();
        //--- shift the header by two pixels to the new location coordinates and
        //--- set the new relative coordinates
        if(this.Move(this.CoordX()-2,this.CoordY()-2))
          {
           this.SetCoordXRelative(this.CoordXRelative()-2);
           this.SetCoordYRelative(this.CoordYRelative()-2);
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
        //--- Adjust the location of the row with the selected header
        this.CorrectSelectedRowBottom();
        //--- shift the header by two pixels to the new location coordinates and
        //--- set the new relative coordinates
        if(this.Move(this.CoordX()-2,this.CoordY()))
          {
           this.SetCoordXRelative(this.CoordXRelative()-2);
           this.SetCoordYRelative(this.CoordYRelative());
          }
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
        //--- Adjust the location of the row with the selected header
        this.CorrectSelectedRowLeft();
        //--- shift the header by two pixels to the new location coordinates and
        //--- set the new relative coordinates
        // {   }
        break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
        //--- Adjust the location of the row with the selected header
        this.CorrectSelectedRowRight();
        //--- shift the header by two pixels to the new location coordinates and
        //--- set the new relative coordinates
        // {   }
        break;
      default:
        break;
     }
   return true;
  }
//+------------------------------------------------------------------+

La disposición de los encabezados de las pestañas a la izquierda y a la derecha todavía no la trataremos aquí: dejaremos ese punto para futuros artículos.


En el método que ajusta el tamaño y la ubicación de un elemento en el estado "no seleccionado" dependiendo de su ubicación, añadiremos bloques de código stub para procesar los encabezados ubicados a izquierda y derecha:

//+------------------------------------------------------------------+
//| Adjust the element size and location                             |
//| in the "released" state depending on its location                |
//+------------------------------------------------------------------+
bool CTabHeader::WHProcessStateOff(void)
  {
//--- If failed to set a new size, leave
   if(!this.SetSizeOff())
      return false;
//--- Depending on the title location,
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
        //--- shift the header to its original position and set the previous relative coordinates
        if(this.Move(this.CoordX()+2,this.CoordY()+2))
          {
           this.SetCoordXRelative(this.CoordXRelative()+2);
           this.SetCoordYRelative(this.CoordYRelative()+2);
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
        //--- shift the header to its original position and set the previous relative coordinates
        if(this.Move(this.CoordX()+2,this.CoordY()))
          {
           this.SetCoordXRelative(this.CoordXRelative()+2);
           this.SetCoordYRelative(this.CoordYRelative());
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
        //--- shift the header to its original position and set the previous relative coordinates
        // {   }
        break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
        //--- shift the header to its original position and set the previous relative coordinates
        // {   }
        break;
      default:
        break;
     }
   return true;
  }
//+------------------------------------------------------------------+

Esta será la base para futuras mejoras.


Método que establece la línea del encabezado de la pestaña resaltada en la posición correcta en la parte superior:

//+------------------------------------------------------------------+
//| Set the row of a selected tab header                             |
//| to the correct position at the top                               |
//+------------------------------------------------------------------+
void CTabHeader::CorrectSelectedRowTop(void)
  {
   int row_pressed=this.Row();      // Selected header row
   int y_pressed=this.CoordY();     // Coordinate where all headers with Row() equal to zero should be moved to
   int y0=0;                        // Zero row coordinate (Row == 0)
//--- If the zero row is selected, then nothing needs to be done - leave
   if(row_pressed==0)
      return;
      
//--- Get the tab field object corresponding to this header and set the Y coordinate of the zero line
   CWinFormBase *obj=this.GetFieldObj();
   if(obj==NULL)
      return;
   y0=obj.CoordY()-this.Height()+2;
   
//--- Get the base object (TabControl)
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
//--- Get the list of all tab headers from the base object
   CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
   if(list==NULL)
      return;
//--- Swap rows in the loop through all headers -
//--- set the row of the selected header to the zero position, while the zero one is set to the position of the selected header row
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- If this is a zero row
      if(header.Row()==0)
        {
         //--- move the header to the position of the selected row
         if(header.Move(header.CoordX(),y_pressed))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            //--- Set the Row value to -1. It will be used as a label of the moved zero row instead of the selected one
            header.SetRow(-1);
           }
        }
      //--- If this is the clicked header line,
      if(header.Row()==row_pressed)
        {
         //--- move the header to the position of the zero row
         if(header.Move(header.CoordX(),y0))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            //--- Set the Row value to -2. It will be used as a label of the moved selected row instead of the zero one
            header.SetRow(-2);
           }
        }
     }
//--- Set the correct Row and Col
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- If this is the former zero row moved to the place of the selected one, set Row of the selected row to it
      if(header.Row()==-1)
         header.SetRow(row_pressed);
      //--- If this is the selected row moved to the zero position, set Row of the zero row
      if(header.Row()==-2)
         header.SetRow(0);
     }
  }
//+------------------------------------------------------------------+

La lógica del método se detalla en los comentarios al código. La conclusión es la siguiente: si hemos seleccionado una pestaña (clicando en el botón del encabezado de la pestaña) que se encuentra en la fila cero (esta es adyacente al campo de la pestaña, la primera estará por encima de la fila cero, la segunda estará por encima de la primera, etc.), entonces la fila no necesitará ser desplazada a una nueva ubicación: se encontrará precisamente en su sitio. Si seleccionamos una pestaña cuyo encabezado no está en la fila cero, deberemos mover todos los encabezados de esa fila a la fila cero, y mover la fila sobre cuyo encabezado hemos pulsado con el ratón. De esta forma, siempre se intercambiarán la fila cero y la fila que contiene el encabezado de la pestaña seleccionada.

Este método solo procesará la situación cuando los encabezados de las pestañas están en la parte superior. También podrán estar en la parte inferior, izquierda y derecha, pero crearemos manejadores para estas situaciones en futuros artículos. Ahora vamos a escribir los métodos de stub para ellos:

//+------------------------------------------------------------------+
//| Set the row of a selected tab header                             |
//| to the correct position at the bottom                            |
//+------------------------------------------------------------------+
void CTabHeader::CorrectSelectedRowBottom(void)
  {
  
  }
//+------------------------------------------------------------------+
//| Set the row of a selected tab header                             |
//| to the correct position on the left                              |
//+------------------------------------------------------------------+
void CTabHeader::CorrectSelectedRowLeft(void)
  {
  
  }
//+------------------------------------------------------------------+
//| Set the row of a selected tab header                             |
//| to the correct position on the right                             |
//+------------------------------------------------------------------+
void CTabHeader::CorrectSelectedRowRight(void)
  {
  
  }
//+------------------------------------------------------------------+


Método que busca y retorna el puntero al objeto de campo correspondiente al número de pestaña:

//+------------------------------------------------------------------+
//| Find and return a pointer to the field object                    |
//| corresponding to the tab index                                   |
//+------------------------------------------------------------------+
CWinFormBase *CTabHeader::GetFieldObj(void)
  {
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return NULL;
   CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD);
   list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,this.PageNumber(),EQUAL);
   return(list!=NULL ? list.At(0) : NULL);
  }
//+------------------------------------------------------------------+

El método es idéntico al método GetHeaderObj(), encargado de buscar y retornar el puntero al encabezado de la pestaña que comentamos antes en la clase de objeto de campo de la pestaña. Este método buscará el campo de la pestaña correspondiente a este encabezado.


En el manejador de eventos "Cursor dentro del área activa, botón (izquierdo) del ratón pulsado", añadiremos un bloque de código en el que, para el encabezado sobre el que se ha pulsado, se buscará y mostrará el campo de la pestaña que le corresponde:

//+------------------------------------------------------------------+
//| '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 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);
         this.Redraw(true);
        }
      //--- 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 and draw a frame
            field.Show();
            field.BringToTop();
            field.DrawFrame();
           }
         //--- Redraw an object and a chart
         this.Redraw(true);
        }
      //--- 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);
     }
  }
//+------------------------------------------------------------------+

Si clicamos en un encabezado, el resultado debería ser la visualización del campo de pestaña correspondiente. Aquí, el bloque de código destacado a color se encarga precisamente de ello. Para el botón simple (más tarde implementaremos el aspecto del encabezado en el futuro, y la representación ahí tendrá forma de botones), vamos a añadir el redibujado del gráfico. Sinceramente, no recuerdo qué experimento dio como resultado esta línea, pero dejaremos todo así por ahora: aún no hemos llegado a este punto.


El control completo de los encabezados y campos de las pestañas debe realizarse desde la clase de control TabControl.

Vamos a mejorar la clase en el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\TabControl.mqh.

Para ello, conectaremos al archivo el archivo de la clase de objeto de campo de la pestaña que acabamos de escribir y declararemos las nuevas variables y métodos en la sección privada:

//+------------------------------------------------------------------+
//|                                                   TabControl.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 "Container.mqh"
#include "GroupBox.mqh"
#include "..\TabHeader.mqh"
#include "..\TabField.mqh"
//+------------------------------------------------------------------+
//| TabHeader object class of WForms TabControl                      |
//+------------------------------------------------------------------+
class CTabControl : public CContainer
  {
private:
   int               m_item_width;                    // Fixed width of tab titles
   int               m_item_height;                   // Fixed height of tab titles
   int               m_header_padding_x;              // Additional header width if DrawMode==Fixed
   int               m_header_padding_y;              // Additional header height if DrawMode==Fixed
   int               m_field_padding_top;             // Padding of top tab fields
   int               m_field_padding_bottom;          // Padding of bottom tab fields
   int               m_field_padding_left;            // Padding of left tab fields
   int               m_field_padding_right;           // Padding of right tab fields
//--- Create a new graphical object
   virtual CGCnvElement *CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                          const int element_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,
                                          const bool activity);

//--- Return the list of (1) headers and (2) tab fields
   CArrayObj        *GetListHeaders(void)                { return this.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);  }
   CArrayObj        *GetListFields(void)                 { return this.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD);   }
//--- 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);
public:


En la sección pública de la clase, declararemos los nuevos métodos:

public:
//--- Create the specified number of tabs
   bool              CreateTabPages(const int total,const int selected_page,const int tab_w=0,const int tab_h=0,const string header_text="");
   
//--- Create a new attached element
   bool              CreateNewElement(const int tab_page,
                                      const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                      const int x,
                                      const int y,
                                      const int w,
                                      const int h,
                                      const color colour,
                                      const uchar opacity,
                                      const bool activity,
                                      const bool redraw);
 
//--- Return the number of (1) bound elements and (2) the bound element by the index in the list
   int               TabElementsTotal(const int tab_page);
   CGCnvElement     *GetTabElement(const int tab_page,const int index);
   
//--- Return by type the (1) list, (2) the number of bound controls, (3) the bound control by index in the list in the specified tab
   CArrayObj        *GetListTabElementsByType(const int tab_page,const ENUM_GRAPH_ELEMENT_TYPE type);
   int               TabElementsTotalByType(const int tab_page,const ENUM_GRAPH_ELEMENT_TYPE type);
   CGCnvElement     *GetTabElementByType(const int tab_page,const ENUM_GRAPH_ELEMENT_TYPE type,const int index);
   
//--- Return a pointer to the (1) tab header, (2) field and (3) the number of tabs
   CTabHeader       *GetTabHeader(const int index)       { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,index);    }
   CWinFormBase     *GetTabField(const int index)        { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,index);     }
   int               TabPages(void)                      { return(this.GetListHeaders()!=NULL ? this.GetListHeaders().Total() : 0); }
//--- (1) Set and (2) return the location of tabs on the control
   void              SetAlignment(const ENUM_CANV_ELEMENT_ALIGNMENT alignment)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT,alignment);
                        CArrayObj *list=this.GetListHeaders();
                        if(list==NULL)
                           return;
                        for(int i=0;i<list.Total();i++)
                          {
                           CTabHeader *header=list.At(i);
                           if(header==NULL)
                              continue;
                           header.SetAlignment(alignment);
                          }
                       }
   ENUM_CANV_ELEMENT_ALIGNMENT Alignment(void)  const { return (ENUM_CANV_ELEMENT_ALIGNMENT)this.GetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT);  }

//--- (1) Set and (2) get the mode of setting the tab size
   void              SetTabSizeMode(const ENUM_CANV_ELEMENT_TAB_SIZE_MODE mode)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_TAB_SIZE_MODE,mode);
                        CArrayObj *list=this.GetListHeaders();
                        if(list==NULL)
                           return;
                        for(int i=0;i<list.Total();i++)
                          {
                           CTabHeader *header=list.At(i);
                           if(header==NULL)
                              continue;
                           header.SetTabSizeMode(mode);
                          }
                       }
   ENUM_CANV_ELEMENT_TAB_SIZE_MODE TabSizeMode(void)const{ return (ENUM_CANV_ELEMENT_TAB_SIZE_MODE)this.GetProperty(CANV_ELEMENT_PROP_TAB_SIZE_MODE);}
   
//--- Sets all tab headers to the PaddingWidth and PaddingHeight values
   void              SetHeaderPadding(const int w,const int h);
//--- Set all tab fields to Padding values
   void              SetFieldPadding(const int top,const int bottom,const int left,const int right);
//--- Return the PaddingWidth and PaddingHeight values of the tab titles
   int               HeaderPaddingWidth(void)      const { return this.m_header_padding_x;      }
   int               HeaderPaddingHeight(void)     const { return this.m_header_padding_y;      }
//--- Return the Padding values of the tab fields
   int               FieldPaddingTop(void)         const { return this.m_field_padding_top;     }
   int               FieldPaddingBottom(void)      const { return this.m_field_padding_bottom;  }
   int               FieldPaddingLeft(void)        const { return this.m_field_padding_left;    }
   int               FieldPaddingRight(void)       const { return this.m_field_padding_right;   }
   
//--- (1) Set and (2) return the flag allowing multiple rows of tab headers on the control
   void              SetMultiline(const bool flag)       { this.SetProperty(CANV_ELEMENT_PROP_TAB_MULTILINE,flag);         }
   bool              Multiline(void)               const { return (bool)this.GetProperty(CANV_ELEMENT_PROP_TAB_MULTILINE); }
//--- (1) Set and (2) return the fixed width of tab headers
   void              SetItemWidth(const int value)       { this.m_item_width=value;             }
   int               ItemWidth(void)               const { return this.m_item_width;            }
//--- (1) Set and (2) return the fixed height of tab headers
   void              SetItemHeight(const int value)      { this.m_item_height=value;            }
   int               ItemHeight(void)              const { return this.m_item_height;           }
//--- Set a fixed tab size
   void              SetItemSize(const int w,const int h)
                       {
                        if(this.ItemWidth()!=w)
                           this.SetItemWidth(w);
                        if(this.ItemHeight()!=h)
                           this.SetItemHeight(h);
                       }
//--- Set the title text (1) of the specified tab and (2) by index
   void              SetHeaderText(CTabHeader *header,const string text);
   void              SetHeaderText(const int index,const string text);
   
//--- Set the tab specified by index to selected/not selected
   void              Select(const int index,const bool flag);
//--- Returns the (1) index, (2) the pointer to the selected tab
   int               SelectedTabPageNum(void)      const { return (int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER);}
   CWinFormBase     *SelectedTabPage(void)               { return this.GetTabField(this.SelectedTabPageNum());             }
   
//--- Constructor
                     CTabControl(const long chart_id,
                                 const int subwindow,
                                 const string descript,
                                 const int x,
                                 const int y,
                                 const int w,
                                 const int h);
  };
//+------------------------------------------------------------------+


En el constructor de la clase, estableceremos los valores por defecto para el modo de ajuste del tamaño de las pestañas y estableceremos los valores de Padding para los encabezados y los campos:

//+------------------------------------------------------------------+
//| Constructor indicating the chart and subwindow ID                |
//+------------------------------------------------------------------+
CTabControl::CTabControl(const long chart_id,
                         const int subwindow,
                         const string descript,
                         const int x,
                         const int y,
                         const int w,
                         const int h) : CContainer(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL);
   this.m_type=OBJECT_DE_TYPE_GWF_CONTAINER;
   this.SetBorderSizeAll(0);
   this.SetBorderStyle(FRAME_STYLE_NONE);
   this.SetOpacity(0,true);
   this.SetBackgroundColor(CLR_CANV_NULL,true);
   this.SetBackgroundColorMouseDown(CLR_CANV_NULL);
   this.SetBackgroundColorMouseOver(CLR_CANV_NULL);
   this.SetBorderColor(CLR_CANV_NULL,true);
   this.SetBorderColorMouseDown(CLR_CANV_NULL);
   this.SetBorderColorMouseOver(CLR_CANV_NULL);
   this.SetForeColor(CLR_DEF_FORE_COLOR,true);
   this.SetAlignment(CANV_ELEMENT_ALIGNMENT_TOP);
   this.SetItemSize(58,18);
   this.SetTabSizeMode(CANV_ELEMENT_TAB_SIZE_MODE_NORMAL);
   this.SetHeaderPadding(6,3);
   this.SetFieldPadding(3,3,3,3);
  }
//+------------------------------------------------------------------+


En el método que crea el número de pestañas indicado, estableceremos en los objetos de encabezados y campos creados el puntero al objeto básico, el número de pestaña y el grupo. Para los encabezados y los campos, estableceremos el Padding, añadiremos el texto de la pestaña si se transmite un texto vacío al método para establecer en los encabezados de la pestaña, estableceremos el modo de indicación del tamaño de los encabezados y estableceremos sus tamaños:

//+------------------------------------------------------------------+
//| 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
   CTabHeader *header=NULL;
   CTabField  *field=NULL;
   for(int i=0;i<total;i++)
     {
      //--- Depending on the location of tab titles, set their initial coordinates
      int header_x=2;
      int header_y=0;
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_TOP)
         header_y=0;
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_BOTTOM)
         header_y=this.Height()-h;
      //--- Set the current X coordinate
      header_x=(header==NULL ? header_x : header.RightEdgeRelative());
      //--- Create the TabHeader object
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,header_x,header_y,w,h,clrNONE,255,this.Active(),false))
        {
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER),string(i+1));
         return false;
        }
      header=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,i);
      if(header==NULL)
        {
         ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER),string(i+1));
         return false;
        }
      header.SetBase(this.GetObject());
      header.SetPageNumber(i);
      header.SetGroup(this.Group()+1);
      header.SetBackgroundColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR,true);
      header.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_DOWN);
      header.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_OVER);
      header.SetBackgroundStateOnColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR_ON,true);
      header.SetBackgroundStateOnColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BACK_DOWN_ON);
      header.SetBackgroundStateOnColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BACK_OVER_ON);
      header.SetBorderStyle(FRAME_STYLE_SIMPLE);
      header.SetBorderColor(CLR_DEF_CONTROL_TAB_HEAD_BORDER_COLOR,true);
      header.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_DOWN);
      header.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_OVER);
      header.SetAlignment(this.Alignment());
      header.SetPadding(this.HeaderPaddingWidth(),this.HeaderPaddingHeight(),this.HeaderPaddingWidth(),this.HeaderPaddingHeight());
      if(header_text!="" && header_text!=NULL)
         this.SetHeaderText(header,header_text+string(i+1));
      else
         this.SetHeaderText(header,"TabPage"+string(i+1));
      header.SetTabSizeMode(this.TabSizeMode());
      header.SetSizes(w,h);
      
      //--- Depending on the location of the tab headers, set the initial coordinates of the tab fields
      int field_x=0;
      int field_y=0;
      int field_h=this.Height()-header.Height();
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_TOP)
         field_y=header.BottomEdgeRelative();
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_BOTTOM)
         field_y=0;
      //--- Create the TabField object (tab field)
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,field_x,field_y,this.Width(),field_h,clrNONE,255,true,false))
        {
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD),string(i+1));
         return false;
        }
      field=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,i);
      if(field==NULL)
        {
         ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD),string(i+1));
         return false;
        }
      field.SetBase(this.GetObject());
      field.SetPageNumber(i);
      field.SetGroup(this.Group()+1);
      field.SetBorderSizeAll(1);
      field.SetBorderStyle(FRAME_STYLE_SIMPLE);
      field.SetOpacity(CLR_DEF_CONTROL_TAB_PAGE_OPACITY,true);
      field.SetBackgroundColor(CLR_DEF_CONTROL_TAB_PAGE_BACK_COLOR,true);
      field.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_DOWN);
      field.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_OVER);
      field.SetBorderColor(CLR_DEF_CONTROL_TAB_PAGE_BORDER_COLOR,true);
      field.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_DOWN);
      field.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_OVER);
      field.SetForeColor(CLR_DEF_FORE_COLOR,true);
      field.SetPadding(this.FieldPaddingLeft(),this.FieldPaddingTop(),this.FieldPaddingRight(),this.FieldPaddingBottom());
      field.Hide();
     }
//--- Arrange all titles in accordance with the specified display modes and select the specified tab
   this.ArrangeTabHeaders();
   this.Select(selected_page,true);
   return true;
  }
//+------------------------------------------------------------------+

Una vez creado el número especificado de pestañas, llamaremos al método que ubica los encabezados según los modos de visualización establecidos.


Método que crea un nuevo elemento vinculado:

//+------------------------------------------------------------------+
//| Create a new attached element                                    |
//+------------------------------------------------------------------+
bool CTabControl::CreateNewElement(const int tab_page,
                                   const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                   const int x,
                                   const int y,
                                   const int w,
                                   const int h,
                                   const color colour,
                                   const uchar opacity,
                                   const bool activity,
                                   const bool redraw)
  {
   CTabField *field=this.GetTabField(tab_page);
   if(field==NULL)
     {
      CMessage::ToLog(DFUN,MSG_ELM_LIST_ERR_FAILED_GET_TAB_OBJ);
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_TAB_OBJ)," (Tab",(string)tab_page,")");
      return false;
     }
   return field.CreateNewElement(element_type,x,y,w,h,colour,opacity,activity,redraw);
  }
//+------------------------------------------------------------------+

Luego obtendremos el objeto de pestaña según el número de pestaña indicado y retornaremos el resultado de la llamada a su método para crear un nuevo elemento adjunto.


Método que ubica los encabezados de las pestañas según los modos establecidos:

//+------------------------------------------------------------------+
//| Arrange the tab headers                                          |
//| according to the specified modes                                 |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeaders(void)
  {
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :  this.ArrangeTabHeadersTop();     break;
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :  this.ArrangeTabHeadersBottom();  break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :  this.ArrangeTabHeadersLeft();    break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :  this.ArrangeTabHeadersRight();   break;
      default: break;
     }
  }
//+------------------------------------------------------------------+

Dependiendo del modo de ubicación de pestañas establecido, llamaremos a los métodos correspondientes.


Método que ubica los encabezados de las pestañas en la parte superior:

//+------------------------------------------------------------------+
//| Arrange tab headers on top                                       |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersTop(void)
  {
//--- Get the list of tab headers
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Declare the variables
   int col=0;                                // Column
   int row=0;                                // Row
   int x1_base=2;                            // Initial X coordinate
   int x2_base=this.RightEdgeRelative()-2;   // Final X coordinate
   int x_shift=0;                            // Shift the tab set for calculating their exit beyond the container
   int n=0;                                  // The variable for calculating the column index relative to the loop index
//--- In a loop by the list of headers,
   for(int i=0;i<list.Total();i++)
     {
      //--- get the next tab header object
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- If the flag for positioning headers in several rows is set
      if(this.Multiline())
        {
         //--- CANV_ELEMENT_TAB_SIZE_MODE_FIXED and CANV_ELEMENT_TAB_SIZE_MODE_NORMAL
         if(this.TabSizeMode()<CANV_ELEMENT_TAB_SIZE_MODE_FILL)
           {
            //--- Calculate the value of the right edge of the header, taking into account that
            //---  the origin always comes from the left edge of TabControl + 2 pixels
            int x2=header.RightEdgeRelative()-x_shift;
            //--- If the calculated value does not go beyond the right edge of the TabControl - 2 pixels, 
            //--- set the column number equal to the loop index minus the value in the n variable
            if(x2<x2_base)
               col=i-n;
            //--- If the calculated value goes beyond the right edge of the TabControl - 2 pixels,
            else
              {
               //--- Increase the row index, calculate the new shift (so that the next object is compared with the TabControl left edge + 2 pixels),
               //--- set the loop index for the n variable, while the column index is set to zero, this is the start of the new row
               row++;
               x_shift=header.CoordXRelative()-2;
               n=i;
               col=0;
              }
            //--- Assign the row and column indices to the tab header and shift it to the calculated coordinates
            header.SetTabLocation(row,col);
            if(header.Move(header.CoordX()-x_shift,header.CoordY()-header.Row()*header.Height()))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
           }
         //--- Stretch the headers along the container width
         //--- CANV_ELEMENT_TAB_SIZE_MODE_FILL
         else
           {
            
           }
        }
      //--- If only one row of headers is allowed
      else
        {
         
        }
     }
//--- The location of all tab titles is set. Now place them all together with the fields
//--- according to the header row and column indices
//--- Get the last title in the list
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
//--- If the object is received and its row value is greater than zero
   if(last!=NULL && last.Row()>0)
     {
      //--- Calculate the offset of the tab field Y coordinate
      int y_shift=last.Row()*last.Height();
      //--- In a loop by the list of headers,
      for(int i=0;i<list.Total();i++)
        {
         //--- get the next object
         CTabHeader *header=list.At(i);
         if(header==NULL)
            continue;
         //--- get the tab field corresponding to the received header
         CTabField  *field=header.GetFieldObj();
         if(field==NULL)
            continue;
         //--- shift the tab header by the calculated row coordinates
         if(header.Move(header.CoordX(),header.CoordY()+y_shift))
           {
            header.SetCoordXRelative(header.CoordX()-this.CoordX());
            header.SetCoordYRelative(header.CoordY()-this.CoordY());
           }
         //--- shift the tab field by the calculated shift
         if(field.Move(field.CoordX(),field.CoordY()+y_shift))
           {
            field.SetCoordXRelative(field.CoordX()-this.CoordX());
            field.SetCoordYRelative(field.CoordY()-this.CoordY());
            //--- change the size of the shifted field by the value of its shift
            field.Resize(field.Width(),field.Height()-y_shift,false);
           }
        }
     }
  }
//+------------------------------------------------------------------+

La lógica del método se detalla en los comentarios al código. Hasta ahora, el método solo se ha implementado para organizar los encabezados en varias líneas en la parte superior del contenedor. Aquí comprobaremos si cada encabezado sucesivo del ciclo se coloca en la siguiente posición de la fila, de forma que no sobrepase el borde del contenedor. Si se sale, deberemos comenzar una nueva fila por encima de la anterior. Después recalcularemos el origen del punto de referencia para que cuando accedamos a las coordenadas del objeto menos el desplazamiento calculado, la fila vuelva a empezar desde el borde izquierdo del contenedor. A continuación, recalcularemos si los objetos caben dentro del contenedor y, si no es así, volveremos a cambiar de fila. Después de cada nueva fila, los valores Row (fila) se incrementarán en los objetos y los valores Col (columna) se comenzarán de nuevo. Al final del ciclo, tenemos una lista con los valores de la fila y la columna donde deben situarse los encabezados.

A continuación, en un nuevo ciclo por la lista de encabezados, los posicionaremos en las nuevas coordenadas correspondientes a los valores de fila y columna registrados en el objeto, y sus correspondientes objetos de campo de la pestaña se desplazarán en la distancia calculada partiendo del valor máximo de la fila y se reducirán luego en la altura en la misma cantidad. Cuando el ciclo se haya completado, tendremos los encabezados y sus correspondientes campos correctamente posicionados.

En artículos posteriores, complementaremos el método con la disposición de los encabezados para los demás modos.

Los otros métodos similares se han implementado hasta ahora como métodos stub:

//+------------------------------------------------------------------+
//| Arrange tab headers at the bottom                                |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersBottom(void)
  {
  
  }
//+------------------------------------------------------------------+
//| Arrange tab headers on the left                                  |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersLeft(void)
  {
  
  }
//+------------------------------------------------------------------+
//| Arrange tab headers to the right                                 |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersRight(void)
  {
  
  }
//+------------------------------------------------------------------+


Método que establece todos los encabezados de las pestañas con los valores de Padding:

//+------------------------------------------------------------------+
//| Set all tab titles to Padding values                             |
//+------------------------------------------------------------------+
void CTabControl::SetHeaderPadding(const int w,const int h)
  {
   this.m_header_padding_x=w;
   this.m_header_padding_y=h;
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      header.SetPadding(this.m_header_padding_x,this.m_header_padding_y,this.m_header_padding_x,this.m_header_padding_y);
     }
  }
//+------------------------------------------------------------------+

Al método se transmiten los valores añadidos adicionalmente a la anchura y altura del encabezado cuando este se establece en el tamaño Normal. Los valores transmitidos al método se escribirán inmediatamente en las variables correspondientes. Luego obtendremos una lista de todos los encabezados y estableceremos en un ciclo los valores de Padding para cada encabezado de la lista a los valores transmitidos al método.


Método que asigna a todos los campos de la pestaña los valores de Padding:

//+------------------------------------------------------------------+
//| Set all tab fields to Padding                                    |
//+------------------------------------------------------------------+
void CTabControl::SetFieldPadding(const int top,const int bottom,const int left,const int right)
  {
   this.m_field_padding_top=top;
   this.m_field_padding_bottom=bottom;
   this.m_field_padding_left=left;
   this.m_field_padding_right=right;
   CArrayObj *list=this.GetListFields();
   if(list==NULL)
      return;
   for(int i=0;i<list.Total();i++)
     {
      CTabField *field=list.At(i);
      if(field==NULL)
         continue;
      field.SetPadding(left,top,right,bottom);
     }
  }
//+------------------------------------------------------------------+

El método es análogo al anterior, pero aquí transmitiremos los valores de Padding en la parte superior, inferior, derecha e izquierda del campo de pestaña. Estos valores se asignarán a las variables correspondientes y luego, en un ciclo, en cada campo de objeto de la pestaña.


El método que establece la pestaña seleccionada ha sido rediseñado:

//+------------------------------------------------------------------+
//| Set the tab as selected                                          |
//+------------------------------------------------------------------+
void CTabControl::SetSelected(const int index)
  {
//--- Get the header by index and
   CTabHeader *header=this.GetTabHeader(index);
   if(header==NULL)
      return;
//--- set it to the "selected" state
   if(!header.State())
      header.SetState(true);
//--- save the index of the selected tab
   this.SetSelectedTabPageNum(index);
  }
//+------------------------------------------------------------------+

Todas las manipulaciones con el desplazamiento del objeto al primer plano y la selección de su campo correspondiente se realizan ahora en el método SetState() de la clase de objeto de encabezado de la pestaña que comentamos antes.


El método que establece una pestaña como no seleccionada se ha rediseñado de manera similar:

//+------------------------------------------------------------------+
//| Select the tab as released                                       |
//+------------------------------------------------------------------+
void CTabControl::SetUnselected(const int index)
  {
//--- Get the header by index and
   CTabHeader *header=this.GetTabHeader(index);
   if(header==NULL)
      return;
//--- set it to the "released" state
   if(header.State())
      header.SetState(false);
  }
//+------------------------------------------------------------------+


Método que retorna el número de elementos vinculados en la pestaña especificada:

//+------------------------------------------------------------------+
//| Get the number of bound elements in the specified tab            |
//+------------------------------------------------------------------+
int CTabControl::TabElementsTotal(const int tab_page)
  {
   CTabField *field=this.GetTabField(tab_page);
   return(field!=NULL ? field.ElementsTotal() : 0);
  }
//+------------------------------------------------------------------+

A continuación, obtendremos el campo de objeto de la pestaña según el número especificado y retornaremos el número de objetos vinculados a él.
El método permite averiguar cuántos objetos están vinculados a la pestaña con el número especificado.


Método que retorna un elemento vinculado según el índice en la lista en la pestaña especificada:

//+------------------------------------------------------------------+
//| Returns the bound element by index in the list                   |
//| in the specified tab                                             |
//+------------------------------------------------------------------+
CGCnvElement *CTabControl::GetTabElement(const int tab_page,const int index)
  {
   CTabField *field=this.GetTabField(tab_page);
   return(field!=NULL ? field.GetElement(index) : NULL);
  }
//+------------------------------------------------------------------+

Luego obtendremos el objeto de campo según el número especificado y retornaremos el puntero al elemento adjunto de la lista según el índice especificado.
El método recuperará el puntero al elemento necesario según su índice en la pestaña especificada.


Método que retorna según el tipo de objeto una lista de elementos vinculados en la pestaña especificada:

//+------------------------------------------------------------------+
//| Return the list of bound controls by type                        |
//| in the specified tab                                             |
//+------------------------------------------------------------------+
CArrayObj *CTabControl::GetListTabElementsByType(const int tab_page,const ENUM_GRAPH_ELEMENT_TYPE type)
  {
   CTabField *field=this.GetTabField(tab_page);
   return(field!=NULL ? field.GetListElementsByType(type) : NULL);
  }
//+------------------------------------------------------------------+

Después obtendremos un objeto de campo según el número especificado y retornaremos la lista de elementos vinculados según el tipo especificado.
El método recuperará la lista de elementos de un tipo especificado de la pestaña deseada.


Método que retorna según el tipo de objeto el número de elementos vinculados en la pestaña especificada:

//+------------------------------------------------------------------+
//| Get the list of bound elements by type                           |
//| in the specified tab                                             |
//+------------------------------------------------------------------+
int CTabControl::TabElementsTotalByType(const int tab_page,const ENUM_GRAPH_ELEMENT_TYPE type)
  {
   CTabField *field=this.GetTabField(tab_page);
   return(field!=NULL ? field.ElementsTotalByType(type) : 0);
  }
//+------------------------------------------------------------------+

A continuación, obtendremos un objeto de campo según el número especificado y retornaremos el número de elementos del tipo especificado situados en la pestaña.
El método permitirá averiguar cuántos elementos del tipo indicado están colocados en la pestaña especificada.


Método que retorna según el tipo de objeto el elemento vinculado según el índice en la lista de la pestaña especificada:

//+------------------------------------------------------------------+
//| Get (by type) the bound element by index in the list             |
//| in the specified tab                                             |
//+------------------------------------------------------------------+
CGCnvElement *CTabControl::GetTabElementByType(const int tab_page,const ENUM_GRAPH_ELEMENT_TYPE type,const int index)
  {
   CTabField *field=this.GetTabField(tab_page);
   return(field!=NULL ? field.GetElementByType(type,index) : NULL);
  }
//+------------------------------------------------------------------+

Luego obtendremos un objeto de campo según el número indicado y retornaremos un elemento del tipo necesario según el índice especificado en la lista.
El método permitirá recuperar un elemento del tipo deseado según su número desde una pestaña especificada.


En el método que crea un nuevo objeto gráfico, al final del mismo, añadiremos un bloque de código para crear un objeto de campo de la pestaña (fragmento de código):

      case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER        :
         element=new CTabHeader(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD         :
         element=new CTabField(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL       :
         element=new CTabControl(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      default:
        break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type));
   return element;
  }
//+------------------------------------------------------------------+


Vamos a incluir en el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Panel.mqh de la clase de objeto de panel el archivo de clase de objeto de campo de la pestaña:

//+------------------------------------------------------------------+
//|                                                        Panel.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 "Container.mqh"
#include "..\TabField.mqh"
#include "GroupBox.mqh"
#include "TabControl.mqh"
#include "..\..\WForms\Common Controls\ListBox.mqh"
#include "..\..\WForms\Common Controls\CheckedListBox.mqh"
#include "..\..\WForms\Common Controls\ButtonListBox.mqh"
//+------------------------------------------------------------------+


Al final del método que crea el nuevo objeto gráfico, escribiremos un bloque de código para crear el objeto de campo de la pestaña:

      case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER        :
         element=new CTabHeader(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD         :
         element=new CTabField(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL       :
         element=new CTabControl(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      default:
        break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type));
   return element;
  }
//+------------------------------------------------------------------+

Ahora podremos crear objetos de este tipo en el objeto de panel y sus herederos.


En el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Container.mqh de la clase de objeto contenedor básico, en el método que establece los parámetros del objeto adjunto, añadiremos el establecimiento de los parámetros del objeto de campo de la pestaña en el bloque de código para establecer los parámetros de los objetos Button, TabHeader y ListBoxItem (fragmento de código):

      //--- For "Label", "CheckBox" and "RadioButton" WinForms objects
      case GRAPH_ELEMENT_TYPE_WF_LABEL             :
      case GRAPH_ELEMENT_TYPE_WF_CHECKBOX          :
      case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON       :
        //--- set the object text color depending on the one passed to the method:
        //--- either the container text color, or the one passed to the method.
        //--- The frame color is set equal to the text color
        //--- Set the background color to transparent
        obj.SetForeColor(colour==clrNONE ? this.ForeColor() : colour,true);
        obj.SetBorderColor(obj.ForeColor(),true);
        obj.SetBackgroundColor(CLR_CANV_NULL,true);
        obj.SetOpacity(0,false);
        break;
      //--- For "Button", "TabHeader", TabField and "ListBoxItem" WinForms objects
      case GRAPH_ELEMENT_TYPE_WF_BUTTON            :
      case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER        :
      case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD         :
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM     :
        //--- set the object text color as a container text color depending on the one passed to the method:
        //--- set the background color depending on the one passed to the method:
        //--- either the default standard control background color, or the one passed to the method.
        //--- The frame color is set equal to the text color
        obj.SetForeColor(this.ForeColor(),true);
        obj.SetBackgroundColor(colour==clrNONE ? CLR_DEF_CONTROL_STD_BACK_COLOR : colour,true);
        obj.SetBorderColor(obj.ForeColor(),true);
        obj.SetBorderStyle(FRAME_STYLE_SIMPLE);
        break;
      //--- For "ListBox", "CheckedListBox" and "ButtonListBox" WinForms object
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX          :
      case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX  :
      case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX   :

Una vez se ha creado un objeto en este método, le asignaremos las propiedades especificadas aquí. Estas podrán ser modificadas posteriormente.


En el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\GroupBox.mqh de la clase de objeto GroupBox, haremos virtual el método de dibujado del marco:

//+------------------------------------------------------------------+
//| GroupBox object class of the WForms controls                     |
//+------------------------------------------------------------------+
class CGroupBox : public CContainer
  {
private:
//--- Draw a frame
   virtual void      DrawFrame(void);
//--- Create a new graphical object

Dado que hemos declarado dicho método como virtual en el objeto básico de todos los objetos WinForms, ahora todos los métodos del mismo nombre en las clases herederas necesitarán ser también virtuales, para poder redefinirlos correctamente y poder referirnos a ellos desde otras clases.


En el método para crear un nuevo objeto gráfico, añadiremos un bloque de código para crear un objeto de campo de la pestaña:

      case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER        :
         element=new CTabHeader(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD         :
         element=new CTabField(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL       :
         element=new CTabControl(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      default:
        break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type));
   return element;
  }
//+------------------------------------------------------------------+


En la clase de colección de elementos gráficos, en el archivo \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh, en los métodos para crear los elementos gráficos en el lienzo, hemos sustituido los nombres de las variables "name" por "descript"; se trata de los restos del cambio de algoritmo de denominación de elementos gráficos en el artículo anterior. Ya funcionaba sin errores, porque el tipo de variable es string, pero para nombrar correctamente los parámetros formales del método, hemos cambiado a "descripción", en lugar de "nombre", que es lo correcto. Como ejemplo:

//--- Create a graphical form object on canvas on a specified chart and subwindow with the cyclic horizontal gradient filling
   int               CreateFormHGradientCicle(const long chart_id,
                                              const int subwindow,
                                              const string descript,
                                              const int x,
                                              const int y,
                                              const int w,
                                              const int h,
                                              color &clr[],
                                              const uchar opacity,
                                              const bool movable,
                                              const bool activity,
                                              const bool shadow=false,
                                              const bool redraw=false)
                       {
                        int id=this.GetMaxID()+1;
                        CForm *obj=new CForm(chart_id,subwindow,descript,x,y,w,h);
                        ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id);
                        if(res==ADD_OBJ_RET_CODE_ERROR)
                           return WRONG_VALUE;
                        obj.SetID(id);
                        obj.SetActive(activity);
                        obj.SetMovable(movable);
                        obj.SetBackgroundColors(clr,true);
                        obj.SetBorderColor(clr[0],true);
                        obj.SetOpacity(opacity,false);
                        obj.SetShadow(shadow);
                        obj.DrawRectangle(0,0,obj.Width()-1,obj.Height()-1,obj.BorderColor(),obj.Opacity());
                        obj.Done();
                        obj.Erase(clr,opacity,false,true,redraw);
                        return obj.ID();
                       }
 
//--- Create the 'GroupBox' WinForms graphical object on canvas on the specified chart and subwindow

El resto de los cambios no los discutiremos aquí, son idénticos; ya hemos implementado todos en el archivo de la biblioteca, que el lector podrá encontrar en los archivos adjuntos al artículo.

En el método que busca los objetos de interacción, añadiremos una comprobación de la visibilidad y disponibilidad de los objetos. Si un objeto es invisible o inaccesible, no podrá ser procesado, deberá estar indisponible para la interacción con el ratón:

//+------------------------------------------------------------------+
//| Search for interaction objects                                   |
//+------------------------------------------------------------------+
CForm *CGraphElementsCollection::SearchInteractObj(CForm *form,const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- If a non-empty pointer is passed
   if(form!=NULL)
     {
      //--- Create the list of interaction objects
      int total=form.CreateListInteractObj();
      //--- In the loop by the created list
      for(int i=total-1;i>WRONG_VALUE;i--)
        {
         //--- get the next form object
         CForm *obj=form.GetInteractForm(i);
         //--- If the object is received and the mouse cursor is located above the object, return the pointer to the found object
         if(obj==NULL)
            continue;
         if(!obj.IsVisible())
           {
            continue;
           }
         if(!obj.Enabled())
           {
            continue;
           }
         if(obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL)
           {
            CTabControl *tab_ctrl=obj;
            CForm *elm=tab_ctrl.SelectedTabPage();
            if(elm!=NULL && elm.MouseFormState(id,lparam,dparam,sparam)>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL)
               return elm;
           }
         if(obj.MouseFormState(id,lparam,dparam,sparam)>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL)
            return obj;
        }
     }
//--- Return the same pointer
   return form;
  }
//+------------------------------------------------------------------+

Aquí: si el objeto está oculto o no está disponible, lo omitiremos. Si se trata de un objeto TabControl, obtendremos de él la pestaña seleccionada.
Si el cursor está sobre la pestaña seleccionada, retornaremos el puntero al objeto de campo de la pestaña.


En el método de procesamiento posterior de un formulario anteriormente activo bajo el cursor, omitiremos todos los objetos ocultos e inaccesibles, no deberemos procesarlos:

//+------------------------------------------------------------------+
//| Post-processing of the former active form under the cursor       |
//+------------------------------------------------------------------+
void CGraphElementsCollection::FormPostProcessing(CForm *form,const int id, const long &lparam, const double &dparam, const string &sparam)
  {
//--- Get the main object the form is attached to
   CForm *main=form.GetMain();
   if(main==NULL)
      main=form;
//--- Get all the elements attached to the form
   CArrayObj *list=main.GetListElements();
   if(list==NULL)
      return;
   //--- In the loop by the list of received elements
   int total=list.Total();
   for(int i=0;i<total;i++)
     {
      //--- get the pointer to the object
      CForm *obj=list.At(i);
      //--- if failed to get the pointer, move on to the next one in the list
      if(obj==NULL)
         continue;
      obj.OnMouseEventPostProcessing();
      //--- Create the list of interaction objects and get their number
      int count=obj.CreateListInteractObj();
      //--- In the loop by the obtained list
      for(int j=0;j<count;j++)
        {
         //--- get the next object
         CWinFormBase *elm=obj.GetInteractForm(j);
         if(elm==NULL || !elm.IsVisible() || !elm.Enabled())
            continue;
         //--- determine the location of the cursor relative to the object 
         //--- and call the mouse event handling method for the object
         elm.MouseFormState(id,lparam,dparam,sparam);
         elm.OnMouseEventPostProcessing();
        }
     }
   ::ChartRedraw(main.ChartID());
  }
//+------------------------------------------------------------------+


En el archivo \MQL5\Include\DoEasy\Engine.mqh del objeto principal de la biblioteca, cambiaremos el nombre del método GetWFPanel() que retorna un objeto según su nombre a GetWFPanelByName(), y haremos que el método GetWFPanel() retorne un objeto según su descripción:

//--- Return the WForm Panel object by object name on the current chart
   CPanel              *GetWFPanelByName(const string name)
                          {
                           string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_WF_PANEL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,::ChartID(),EQUAL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
//--- Return the WForm Panel object according to the description of the object on the current chart
   CPanel              *GetWFPanel(const string descript)
                          {
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_WF_PANEL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,::ChartID(),EQUAL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_DESCRIPTION,descript,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
//--- Return the WForm Panel object by chart ID and object name

Como ambos métodos tenían parámetros formales del mismo tipo, la sobrecarga de los métodos no será posible en esta situación. Por este motivo, hemos cambiado el nombre de uno de los métodos.

Al igual que en la clase de colección de elementos gráficos, todas las apariciones de "name" en los parámetros formales de los métodos que crean los objetos WinForms se renombrarán a "descript".

Por ejemplo:

//--- Create the WinForm Element object
   CGCnvElement        *CreateWFElement(const long chart_id,
                                        const int subwindow,
                                        const string descript,
                                        const int x,
                                        const int y,
                                        const int w,
                                        const int h,
                                        color &clr[],
                                        const uchar opacity,
                                        const bool v_gradient=true,
                                        const bool c_gradient=false,
                                        const bool redraw=false)
                          {
                           //--- Get the created object ID
                           int obj_id=
                             (
                              //--- In case of a vertical gradient:
                              v_gradient ?
                                (
                                 //--- if not a cyclic gradient, create an object with the vertical gradient filling
                                 !c_gradient ? this.m_graph_objects.CreateElementVGradient(chart_id,subwindow,descript,x,y,w,h,clr,opacity,false,true,redraw) :
                                 //--- otherwise, create an object with the cyclic vertical gradient filling
                                 this.m_graph_objects.CreateElementVGradientCicle(chart_id,subwindow,descript,x,y,w,h,clr,opacity,false,true,redraw)
                                ) :
                              //--- If this is not a vertical gradient:
                              !v_gradient ?
                                (
                                 //--- if not a cyclic gradient, create an object with the horizontal gradient filling
                                 !c_gradient ? this.m_graph_objects.CreateElementHGradient(chart_id,subwindow,descript,x,y,w,h,clr,opacity,false,true,redraw) :
                                 //--- otherwise, create an object with the cyclic horizontal gradient filling
                                 this.m_graph_objects.CreateElementHGradientCicle(chart_id,subwindow,descript,x,y,w,h,clr,opacity,false,true,redraw)
                                ) :
                              WRONG_VALUE
                             );
                           //--- return the pointer to an object by its ID
                           return this.GetWFElement(obj_id);
                          }
//--- Create the WinForm Element object in the specified subwindow on the current chart

Esos son todos los cambios y mejoras hasta ahora.


Simulación

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

Para que podamos seleccionar el modo de asignación del tamaño a los encabezados de las pestañas, y, en este caso, considerando que en la versión inglesa de la compilación las constantes de enumeración estarán en inglés, mientras que en ruso estarán en ruso, vamos a crear nuevas enumeraciones para la compilación condicional:

enum ENUM_ELEMENT_ALIGNMENT
  {
   ELEMENT_ALIGNMENT_TOP=CANV_ELEMENT_ALIGNMENT_TOP,                    // Top
   ELEMENT_ALIGNMENT_BOTTOM=CANV_ELEMENT_ALIGNMENT_BOTTOM,              // Bottom
   ELEMENT_ALIGNMENT_LEFT=CANV_ELEMENT_ALIGNMENT_LEFT,                  // Left
   ELEMENT_ALIGNMENT_RIGHT=CANV_ELEMENT_ALIGNMENT_RIGHT,                // Right
  };
enum ENUM_ELEMENT_TAB_SIZE_MODE
  {
   ELEMENT_TAB_SIZE_MODE_NORMAL=CANV_ELEMENT_TAB_SIZE_MODE_NORMAL,      // Fit to tab title text width
   ELEMENT_TAB_SIZE_MODE_FIXED=CANV_ELEMENT_TAB_SIZE_MODE_FIXED,        // Fixed size
   ELEMENT_TAB_SIZE_MODE_FILL=CANV_ELEMENT_TAB_SIZE_MODE_FILL,          // Fit TabControl Width
  };
#else 
enum ENUM_AUTO_SIZE_MODE
  {
   AUTO_SIZE_MODE_GROW=CANV_ELEMENT_AUTO_SIZE_MODE_GROW,                // Increase only
   AUTO_SIZE_MODE_GROW_SHRINK=CANV_ELEMENT_AUTO_SIZE_MODE_GROW_SHRINK   // Increase and decrease
  };
enum ENUM_BORDER_STYLE
  {
   BORDER_STYLE_NONE=FRAME_STYLE_NONE,                                  // No frame
   BORDER_STYLE_SIMPLE=FRAME_STYLE_SIMPLE,                              // Simple frame
   BORDER_STYLE_FLAT=FRAME_STYLE_FLAT,                                  // Flat frame
   BORDER_STYLE_BEVEL=FRAME_STYLE_BEVEL,                                // Embossed (convex)
   BORDER_STYLE_STAMP=FRAME_STYLE_STAMP,                                // Embossed (concave)
  };
enum ENUM_CHEK_STATE
  {
   CHEK_STATE_UNCHECKED=CANV_ELEMENT_CHEK_STATE_UNCHECKED,              // Unchecked
   CHEK_STATE_CHECKED=CANV_ELEMENT_CHEK_STATE_CHECKED,                  // Checked
   CHEK_STATE_INDETERMINATE=CANV_ELEMENT_CHEK_STATE_INDETERMINATE,      // Undefined
  };
enum ENUM_ELEMENT_ALIGNMENT
  {
   ELEMENT_ALIGNMENT_TOP=CANV_ELEMENT_ALIGNMENT_TOP,                    // Top
   ELEMENT_ALIGNMENT_BOTTOM=CANV_ELEMENT_ALIGNMENT_BOTTOM,              // Bottom
   ELEMENT_ALIGNMENT_LEFT=CANV_ELEMENT_ALIGNMENT_LEFT,                  // Left
   ELEMENT_ALIGNMENT_RIGHT=CANV_ELEMENT_ALIGNMENT_RIGHT,                // Right
  };
enum ENUM_ELEMENT_TAB_SIZE_MODE
  {
   ELEMENT_TAB_SIZE_MODE_NORMAL=CANV_ELEMENT_TAB_SIZE_MODE_NORMAL,      // By tab title width
   ELEMENT_TAB_SIZE_MODE_FIXED=CANV_ELEMENT_TAB_SIZE_MODE_FIXED,        // Fixed size
   ELEMENT_TAB_SIZE_MODE_FILL=CANV_ELEMENT_TAB_SIZE_MODE_FILL,          // By TabControl width
  };
#endif 
//--- input parameters


En los parámetros de entrada del asesor, añadiremos una nueva variable que establecerá el modo de ajuste del tamaño de los encabezados de las pestañas:

//--- input parameters
sinput   bool                          InpMovable           =  true;                   // Panel Movable flag
sinput   ENUM_INPUT_YES_NO             InpAutoSize          =  INPUT_YES;              // Panel Autosize
sinput   ENUM_AUTO_SIZE_MODE           InpAutoSizeMode      =  AUTO_SIZE_MODE_GROW;    // Panel Autosize mode
sinput   ENUM_BORDER_STYLE             InpFrameStyle        =  BORDER_STYLE_SIMPLE;    // Label border style
sinput   ENUM_ANCHOR_POINT             InpTextAlign         =  ANCHOR_CENTER;          // Label text align
sinput   ENUM_INPUT_YES_NO             InpTextAutoSize      =  INPUT_NO;               // Label autosize
sinput   ENUM_ANCHOR_POINT             InpCheckAlign        =  ANCHOR_LEFT;            // Check flag align
sinput   ENUM_ANCHOR_POINT             InpCheckTextAlign    =  ANCHOR_LEFT;            // Check label text align
sinput   ENUM_CHEK_STATE               InpCheckState        =  CHEK_STATE_UNCHECKED;   // Check flag state
sinput   ENUM_INPUT_YES_NO             InpCheckAutoSize     =  INPUT_YES;              // CheckBox autosize
sinput   ENUM_BORDER_STYLE             InpCheckFrameStyle   =  BORDER_STYLE_NONE;      // CheckBox border style
sinput   ENUM_ANCHOR_POINT             InpButtonTextAlign   =  ANCHOR_CENTER;          // Button text align
sinput   ENUM_INPUT_YES_NO             InpButtonAutoSize    =  INPUT_YES;              // Button autosize
sinput   ENUM_AUTO_SIZE_MODE           InpButtonAutoSizeMode=  AUTO_SIZE_MODE_GROW;    // Button Autosize mode
sinput   ENUM_BORDER_STYLE             InpButtonFrameStyle  =  BORDER_STYLE_NONE;      // Button border style
sinput   bool                          InpButtonToggle      =  true ;                  // Button toggle flag
sinput   bool                          InpListBoxMColumn    =  true;                   // ListBox MultiColumn flag
sinput   bool                          InpButtListMSelect   =  false;                  // ButtonListBox Button MultiSelect flag
//sinput   ENUM_ELEMENT_ALIGNMENT        InpHeaderAlignment   =  ELEMENT_ALIGNMENT_TOP;  // TabHeader Alignment
sinput   ENUM_ELEMENT_TAB_SIZE_MODE    InpTabPageSizeMode   =  ELEMENT_TAB_SIZE_MODE_NORMAL; // TabHeader Size Mode
//--- global variables


La creación de los objetos WinForms en el manejador OnInit() del asesor será ahora así

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Set EA global variables
   ArrayResize(array_clr,2);        // Array of gradient filling colors
   array_clr[0]=C'26,100,128';      // Original ≈Dark-azure color
   array_clr[1]=C'35,133,169';      // Lightened original color
//--- Create the array with the current symbol and set it to be used in the library
   string array[1]={Symbol()};
   engine.SetUsedSymbols(array);
   //--- Create the timeseries object for the current symbol and period, and show its description in the journal
   engine.SeriesCreate(Symbol(),Period());
   engine.GetTimeSeriesCollection().PrintShort(false); // Short descriptions

//--- Create WinForms Panel object
   CPanel *pnl=NULL;
   pnl=engine.CreateWFPanel("WFPanel",50,50,400,200,array_clr,200,true,true,false,-1,FRAME_STYLE_BEVEL,true,false);
   if(pnl!=NULL)
     {
      //--- Set Padding to 4
      pnl.SetPaddingAll(4);
      //--- Set the flags of relocation, auto resizing and auto changing mode from the inputs
      pnl.SetMovable(InpMovable);
      pnl.SetAutoSize(InpAutoSize,false);
      pnl.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpAutoSizeMode,false);

      //--- In the loop, create 2 bound panel objects
      CPanel *obj=NULL;
      for(int i=0;i<2;i++)
        {
         //--- create the panel object with calculated coordinates, width of 90 and height of 40
         CPanel *prev=pnl.GetElement(i-1);
         int xb=0, yb=0;
         int x=(prev==NULL ? xb : xb+prev.Width()+20);
         int y=0;
         if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_PANEL,x,y,90,40,C'0xCD,0xDA,0xD7',200,true,false))
           {
            obj=pnl.GetElement(i);
            if(obj==NULL)
               continue;
            obj.SetBorderSizeAll(3);
            obj.SetBorderStyle(FRAME_STYLE_BEVEL);
            obj.SetBackgroundColor(obj.ChangeColorLightness(obj.BackgroundColor(),4*i),true);
            obj.SetForeColor(clrRed,true);
            //--- Calculate the width and height of the future text label object
            int w=obj.Width()-obj.BorderSizeLeft()-obj.BorderSizeRight();
            int h=obj.Height()-obj.BorderSizeTop()-obj.BorderSizeBottom();
            //--- Create a text label object
            obj.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_LABEL,0,0,w,h,clrNONE,255,false,false);
            //--- Get the pointer to a newly created object
            CLabel *lbl=obj.GetElement(0);
            if(lbl!=NULL)
              {
               //--- If the object has an even or zero index in the list, set the default text color for it
               if(i % 2==0)
                  lbl.SetForeColor(CLR_DEF_FORE_COLOR,true);
               //--- If the object index in the list is odd, set the object opacity to 127
               else
                  lbl.SetForeColorOpacity(127);
               //--- Set the font Black width type and
               //--- specify the text alignment from the EA settings
               lbl.SetFontBoldType(FW_TYPE_BLACK);
               lbl.SetTextAlign(InpTextAlign);
               lbl.SetAutoSize((bool)InpTextAutoSize,false);
               //--- For an object with an even or zero index, specify the Bid price for the text, otherwise - the Ask price of the symbol 
               lbl.SetText(GetPrice(i % 2==0 ? SYMBOL_BID : SYMBOL_ASK));
               //--- Set the frame width, type and color for a text label and update the modified object
               lbl.SetBorderSizeAll(1);
               lbl.SetBorderStyle((ENUM_FRAME_STYLE)InpFrameStyle);
               lbl.SetBorderColor(CLR_DEF_BORDER_COLOR,true);
               lbl.Update(true);
              }
           }
        }
      //--- Create the WinForms GroupBox1 object
      CGroupBox *gbox1=NULL;
      //--- The indent from the attached panels by 6 pixels will be the Y coordinate of GrotupBox1
      int w=pnl.GetUnderlay().Width();
      int y=obj.BottomEdgeRelative()+6;
      //--- If the attached GroupBox object is created
      if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,0,y,200,150,C'0x91,0xAA,0xAE',0,true,false))
        {
         //--- get the pointer to the GroupBox object by its index in the list of bound GroupBox type objects
         gbox1=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,0);
         if(gbox1!=NULL)
           {
            //--- set the "indented frame" type, the frame color matches the main panel background color,
            //--- while the text color is the background color of the last attached panel darkened by 1
            gbox1.SetBorderStyle(FRAME_STYLE_STAMP);
            gbox1.SetBorderColor(pnl.BackgroundColor(),true);
            gbox1.SetForeColor(gbox1.ChangeColorLightness(obj.BackgroundColor(),-1),true);
            gbox1.SetText("GroupBox1");
            //--- Create the CheckBox object
            gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_CHECKBOX,2,10,50,20,clrNONE,255,true,false);
            //--- get the pointer to the CheckBox object by its index in the list of bound CheckBox type objects
            CCheckBox *cbox=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_CHECKBOX,0);
            //--- If CheckBox is created and the pointer to it is received
            if(cbox!=NULL)
              {
               //--- Set the CheckBox parameters from the EA inputs
               cbox.SetAutoSize((bool)InpCheckAutoSize,false);
               cbox.SetCheckAlign(InpCheckAlign);
               cbox.SetTextAlign(InpCheckTextAlign);
               //--- Set the displayed text, frame style and color, as well as checkbox status
               cbox.SetText("CheckBox");
               cbox.SetBorderStyle((ENUM_FRAME_STYLE)InpCheckFrameStyle);
               cbox.SetBorderColor(CLR_DEF_BORDER_COLOR,true);
               cbox.SetChecked(true);
               cbox.SetCheckState((ENUM_CANV_ELEMENT_CHEK_STATE)InpCheckState);
              }
            //--- Create 4 RadioButton WinForms objects
            CRadioButton *rbtn=NULL;
            for(int i=0;i<4;i++)
              {
               //--- Create the RadioButton object
               int yrb=(rbtn==NULL ? cbox.BottomEdgeRelative() : rbtn.BottomEdgeRelative());
               gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,2,yrb+4,50,20,clrNONE,255,true,false);
               //--- get the pointer to the RadioButton object by its index in the list of bound RadioButton type objects
               rbtn=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,i);
               //--- If RadioButton1 is created and the pointer to it is received
               if(rbtn!=NULL)
                 {
                  //--- Set the RadioButton parameters from the EA inputs
                  rbtn.SetAutoSize((bool)InpCheckAutoSize,false);
                  rbtn.SetCheckAlign(InpCheckAlign);
                  rbtn.SetTextAlign(InpCheckTextAlign);
                  //--- Set the displayed text, frame style and color, as well as checkbox status
                  rbtn.SetText("RadioButton"+string(i+1));
                  rbtn.SetBorderStyle((ENUM_FRAME_STYLE)InpCheckFrameStyle);
                  rbtn.SetBorderColor(CLR_DEF_BORDER_COLOR,true);
                  rbtn.SetChecked(!i);
                  rbtn.SetGroup(2);
                 }
              }
            //--- Create 3 Button WinForms objects
            CButton *butt=NULL;
            for(int i=0;i<3;i++)
              {
               //--- Create the Button object
               int ybtn=(butt==NULL ? 12 : butt.BottomEdgeRelative()+4);
               gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_BUTTON,(int)fmax(rbtn.RightEdgeRelative(),cbox.RightEdgeRelative())+20,ybtn,78,18,clrNONE,255,true,false);
               //--- get the pointer to the Button object by its index in the list of bound Button type objects
               butt=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_BUTTON,i);
               //--- If Button is created and the pointer to it is received
               if(butt!=NULL)
                 {
                  //--- Set the Button parameters from the EA inputs
                  butt.SetAutoSize((bool)InpButtonAutoSize,false);
                  butt.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpButtonAutoSizeMode,false);
                  butt.SetTextAlign(InpButtonTextAlign);
                  //--- Set the text color, as well as frame style and color
                  butt.SetForeColor(butt.ChangeColorLightness(CLR_DEF_FORE_COLOR,2),true);
                  butt.SetBorderStyle((ENUM_FRAME_STYLE)InpButtonFrameStyle);
                  butt.SetBorderColor(butt.ChangeColorLightness(butt.BackgroundColor(),-10),true);
                  //--- Set the 'toggle' mode depending on the settings
                  butt.SetToggleFlag(InpButtonToggle);
                  //--- Set the displayed text on the button depending on the 'toggle' flag
                  string txt="Button"+string(i+1);
                  if(butt.Toggle())
                     butt.SetText("Toggle-"+txt);
                  else
                     butt.SetText(txt);
                  if(i<2)
                    {
                     butt.SetGroup(2);
                     if(butt.Toggle())
                       {
                        butt.SetBackgroundColorMouseOver(butt.ChangeColorLightness(butt.BackgroundColor(),-5));
                        butt.SetBackgroundColorMouseDown(butt.ChangeColorLightness(butt.BackgroundColor(),-10));
                        butt.SetBackgroundStateOnColor(C'0xE2,0xC5,0xB1',true);
                        butt.SetBackgroundStateOnColorMouseOver(butt.ChangeColorLightness(butt.BackgroundStateOnColor(),-5));
                        butt.SetBackgroundStateOnColorMouseDown(butt.ChangeColorLightness(butt.BackgroundStateOnColor(),-10));
                       }
                    }
                 }
              }
            //--- Create 2 RadioButton WinForms objects
            rbtn=NULL;
            for(int i=0;i<2;i++)
              {
               //--- Create the RadioButton object
               int yrb=(rbtn==NULL ? butt.BottomEdgeRelative() : rbtn.BottomEdgeRelative());
               gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,butt.CoordXRelative()-4,yrb+3,50,20,clrNONE,255,true,false);
               //--- get the pointer to the RadioButton object by its index in the list of bound RadioButton type objects
               rbtn=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,i+4);
               //--- If RadioButton1 is created and the pointer to it is received
               if(rbtn!=NULL)
                 {
                  //--- Set the RadioButton parameters from the EA inputs
                  rbtn.SetAutoSize((bool)InpCheckAutoSize,false);
                  rbtn.SetCheckAlign(InpCheckAlign);
                  rbtn.SetTextAlign(InpCheckTextAlign);
                  //--- Set the displayed text, frame style and color, as well as checkbox status
                  rbtn.SetText("RadioButton"+string(i+5));
                  rbtn.SetBorderStyle((ENUM_FRAME_STYLE)InpCheckFrameStyle);
                  rbtn.SetBorderColor(CLR_DEF_BORDER_COLOR,true);
                  rbtn.SetChecked(!i);
                  rbtn.SetGroup(3);
                 }
              }
           }
        }
      
      //--- Create the GroupBox2 WinForms object
      CGroupBox *gbox2=NULL;
      //--- The indent from the attached panels by 6 pixels will be the Y coordinate of GrotupBox2
      w=gbox1.Width()-1;
      int x=gbox1.RightEdgeRelative()+1;
      int h=gbox1.BottomEdgeRelative()-6;
      //--- If the attached GroupBox object is created
      if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,x,2,w,h,C'0x91,0xAA,0xAE',0,true,false))
        {
         //--- get the pointer to the GroupBox object by its index in the list of bound GroupBox type objects
         gbox2=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,1);
         if(gbox2!=NULL)
           {
            //--- set the "indented frame" type, the frame color matches the main panel background color,
            //--- while the text color is the background color of the last attached panel darkened by 1
            gbox2.SetBorderStyle(FRAME_STYLE_STAMP);
            gbox2.SetBorderColor(pnl.BackgroundColor(),true);
            gbox2.SetForeColor(gbox2.ChangeColorLightness(obj.BackgroundColor(),-1),true);
            gbox2.SetText("GroupBox2");
            
            //--- Create the TabControl object
            gbox2.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,4,12,gbox2.Width()-12,gbox2.Height()-20,clrNONE,255,true,false);
            //--- get the pointer to the TabControl object by its index in the list of bound objects of the TabControl type
            CTabControl *tab_ctrl=gbox2.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,0);
            //--- If TabControl is created and the pointer to it is received
            if(tab_ctrl!=NULL)
              {
               //--- Set the location of the tab titles on the element and the tab text, as well as create nine tabs
               tab_ctrl.SetTabSizeMode((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)InpTabPageSizeMode);
               tab_ctrl.SetAlignment(CANV_ELEMENT_ALIGNMENT_TOP/*(ENUM_CANV_ELEMENT_ALIGNMENT)InpHeaderAlignment*/);
               tab_ctrl.SetMultiline(true);
               tab_ctrl.SetHeaderPadding(6,0);
               tab_ctrl.CreateTabPages(9,0,50,16,TextByLanguage("Вкладка","TabPage"));

               //--- Create the CheckedListBox object on the first tab
               tab_ctrl.CreateNewElement(0,GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,4,12,160,20,clrNONE,255,true,false);
               //--- get the pointer to the CheckedListBox object from the first tab by its index in the list of bound objects of the CheckBox type
               CCheckedListBox *check_lbox=tab_ctrl.GetTabElementByType(0,GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,0);
               //--- If CheckedListBox is created and the pointer to it is received
               if(check_lbox!=NULL)
                 {
                  check_lbox.SetBackgroundColor(tab_ctrl.BackgroundColor(),true);
                  check_lbox.SetMultiColumn(InpListBoxMColumn);
                  check_lbox.SetColumnWidth(0);
                  check_lbox.CreateCheckBox(4,66);
                 }
               
               //--- Create the ButtonListBox object on the second tab
               tab_ctrl.CreateNewElement(1,GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX,4,12,160,30,clrNONE,255,true,false);
               //--- get the pointer to the ButtonListBox object from the first tab by its index in the list of attached objects of the Button type
               CButtonListBox *butt_lbox=tab_ctrl.GetTabElementByType(1,GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX,0);
               //--- If ButtonListBox is created and the pointer to it is received
               if(butt_lbox!=NULL)
                 {
                  butt_lbox.SetBackgroundColor(tab_ctrl.BackgroundColor(),true);
                  butt_lbox.SetMultiColumn(true);
                  butt_lbox.SetColumnWidth(0);
                  butt_lbox.CreateButton(4,66,16);
                  butt_lbox.SetMultiSelect(InpButtListMSelect);
                  butt_lbox.SetToggle(InpButtonToggle);
                  for(int i=0;i<butt_lbox.ElementsTotal();i++)
                    {
                     butt_lbox.SetButtonGroup(i,(i % 2==0 ? butt_lbox.Group()+1 : butt_lbox.Group()+2));
                     butt_lbox.SetButtonGroupFlag(i,(i % 2==0 ? true : false));
                    }
                 }
               
               //--- Create the ListBox object on the third tab
               int lbw=146;
               if(!InpListBoxMColumn)
                  lbw=100;
               tab_ctrl.CreateNewElement(2,GRAPH_ELEMENT_TYPE_WF_LIST_BOX,4,12,lbw,60,clrNONE,255,true,false);
               //--- get the pointer to the ListBox object from the third tab by its index in the list of attached objects of the ListBox type
               CListBox *list_box=tab_ctrl.GetTabElementByType(2,GRAPH_ELEMENT_TYPE_WF_LIST_BOX,0);
               //--- If ListBox has been created and the pointer to it has been received
               if(list_box!=NULL)
                 {
                  list_box.SetBackgroundColor(tab_ctrl.BackgroundColor(),true);
                  list_box.SetMultiColumn(InpListBoxMColumn);
                  list_box.CreateList(8,68);
                 }
               
               //--- On the remaining tabs (3 - 8), place text labels with the name of the tab
               for(int i=3;i<9;i++)
                 {
                  CTabField *field=tab_ctrl.GetTabField(i);
                  if(field==NULL)
                     continue;
                  tab_ctrl.CreateNewElement(i,GRAPH_ELEMENT_TYPE_WF_LABEL,1,1,field.Width()-2,field.Height()-2,clrNONE,255,true,false);
                  CLabel *label=tab_ctrl.GetTabElementByType(i,GRAPH_ELEMENT_TYPE_WF_LABEL,0);
                  if(label!=NULL)
                    {
                     label.SetTextAlign(ANCHOR_CENTER);
                     label.SetText(tab_ctrl.GetTabHeader(i).Text());
                    }
                 }
              }
           }
        }
      //--- Redraw all objects according to their hierarchy
      pnl.Redraw(true);
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Esperamos que la secuencia de creación de los objetos esté lo suficientemente clara en los comentarios del código. Aquí, en el segundo contenedor GroupBox, crearemos un control TabControl con nueve pestañas: específicamente para comprobar cómo se organizarán en filas. En las tres primeras pestañas, crearemos los objetos que creamos anteriormente en el contenedor GroupBox2. Estos tres controles se colocarán ahora en sus propias pestañas. En el resto de pestañas, colocaremos etiquetas de texto con descripciones de las pestañas tomadas del texto de sus encabezados.

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


Bien... El tiempo de creación de los objetos resulta bastante largo. Pronto deberemos cambiar la lógica de representación de los objetos durante su creación en masa. En breve llegaremos a ese punto. Al seleccionar un tamaño fijo para los encabezados de las pestañas y un tamaño ajustado a la anchura de la fuente, podemos ver que los tamaños de las pestañas son diferentes. La selección de la pestaña derecha y la reorganización de las hileras de pestañas funciona correctamente. Los objetos de las pestañas están disponibles para la interacción con el ratón. Hasta aquí ha ido todo bien, lo cual significa que podemos seguir desarrollando la funcionalidad del control.


¿Qué es lo próximo?

En el próximo artículo, continuaremos desarrollando el objeto WinForms TabControl.

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 10): Objetos WinForms: dando vida a la interfaz
DoEasy. Elementos de control (Parte 11): Objetos WinForms: grupos, el objeto WinForms CheckedListBox
DoEasy. Elementos de control (Parte 12): Objeto de lista básico, objetos WinForms ListBox y ButtonListBox
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



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

Archivos adjuntos |
MQL5.zip (4432.19 KB)
Aprendiendo a diseñar un sistema de trading con Bulls Power Aprendiendo a diseñar un sistema de trading con Bulls Power
Bienvenidos a un nuevo artículo de la serie dedicada a la creación de sistemas comerciales basados en indicadores técnicos populares. En esta ocasión, hablaremos sobre el índice de fuerza alcista Bulls Power y crearemos un sistema comercial basado en sus indicadores.
Redes neuronales: así de sencillo (Parte 24): Mejorando la herramienta para el Transfer Learning Redes neuronales: así de sencillo (Parte 24): Mejorando la herramienta para el Transfer Learning
En el último artículo, creamos una herramienta capaz de crear y editar arquitecturas de redes neuronales. Hoy querríamos proponerles continuar con el desarrollo de esta herramienta, para lograr que resulte más fácil de usar. En cierto modo, esto se aleja un poco de nuestro tema, pero estará de acuerdo con que la organización del espacio de trabajo desempeña un papel importante en el resultado final.
Algoritmos de optimización de la población Algoritmos de optimización de la población
Artículo de introducción a los algoritmos de optimización (AO). Clasificación. En el artículo, intentaremos crear un banco de pruebas (un conjunto de funciones) que servirá en el futuro para comparar los AO entre sí, e incluso, quizás, para identificar el algoritmo más universal de todos los ampliamente conocidos.
Aprendiendo a diseñar un sistema de trading con Bears Power Index Aprendiendo a diseñar un sistema de trading con Bears Power Index
Bienvenidos a un nuevo artículo de la serie dedicada a la creación de sistemas comerciales basados en indicadores técnicos populares. En esta ocasión, hablaremos sobre el Bears Power Index y crearemos un sistema comercial basado en sus indicadores.