English Русский 中文 Deutsch 日本語 Português
preview
DoEasy. Elementos de control (Parte 16): Objeto WinForms TabControl - múltiples filas de encabezados de pestañas, modo de expansión de encabezados para ajustarse al tamaño del contenedor

DoEasy. Elementos de control (Parte 16): Objeto WinForms TabControl - múltiples filas de encabezados de pestañas, modo de expansión de encabezados para ajustarse al tamaño del contenedor

MetaTrader 5Ejemplos | 29 noviembre 2022, 12:50
215 0
Artyom Trishkin
Artyom Trishkin

Contenido


Concepto

En el artículo anterior, explicamos los modos de visualización de los encabezados de las pestañas:

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.


Así, hemos implementado la funcionalidad necesaria para colocar pestañas encima del control en los modos Normal y Fixed.

Hoy vamos a implementar la colocación de pestañas en el modo multilínea en todos los lados del control y añadir el modo de ajuste del tamaño de las pestañas FillToRight, para estirar las filas de las pestañas según el tamaño del control. Si colocamos filas de encabezados de pestañas en la parte superior o inferior del contenedor, los encabezados se estirarán según la anchura del control. Cuando los encabezados de las pestañas se colocan a la izquierda o a la derecha, estos se estiran según la altura del control. El área sobre la que se van a dimensionar los encabezados será dos píxeles más pequeña por cada lado de esta área, porque la pestaña seleccionada (su encabezado), aumentará de tamaño en 4 píxeles al clicar sobre ella. Por consiguiente, si no dejamos un espacio de dos píxeles para el encabezado más externo, al seleccionarlo y cambiar este su tamaño, su borde se extenderá más allá del control.

Para mostrar los encabezados de las pestañas en una sola fila, y si hay más de los que pueden ubicarse según el tamaño del control, cualquier encabezado que se extienda más allá del contenedor será colocado fuera del mismo. Aún no disponemos de suficiente funcionalidad para recortar esos gráficos fuera del contenedor, por lo que todavía no vamos analizar este modo. Llegaremos al fondo de esto en artículos posteriores.


Mejorando las clases de la biblioteca

Nuestra misión es buscar en las listas de encabezados de las pestañas todos los encabezados dispuestos en la misma fila, de forma que solo podamos trabajar con los encabezados de esa fila. La forma más sencilla de lograrlo sería añadir nuevas propiedades a la biblioteca de objetos de WinForms, y luego utilizar la funcionalidad de la biblioteca preparada hace tiempo para buscar y filtrar listas de objetos.

En el archivo \MQL5\Include\DoEasy\Defines.mqh añadiremos dos nuevas propiedades a la lista de propiedades enteras del elemento gráfico en el lienzo y aumentaremos su número total de 90 a 92:

//+------------------------------------------------------------------+
//| 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_SIZE_MODE,                   // Tab size setting mode
   CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,                 // Tab index number
   CANV_ELEMENT_PROP_TAB_PAGE_ROW,                    // Tab row index
   CANV_ELEMENT_PROP_TAB_PAGE_COLUMN,                 // Tab column index
   CANV_ELEMENT_PROP_ALIGNMENT,                       // Location of an object inside the control
   
  };
#define CANV_ELEMENT_PROP_INTEGER_TOTAL (92)          // Total number of integer properties
#define CANV_ELEMENT_PROP_INTEGER_SKIP  (0)           // Number of integer properties not used in sorting
//+------------------------------------------------------------------+


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

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

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

   SORT_BY_CANV_ELEMENT_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_TAB_PAGE_ROW,                 // Sort by tab row index
   SORT_BY_CANV_ELEMENT_TAB_PAGE_COLUMN,              // Sort by tab column index
   SORT_BY_CANV_ELEMENT_ALIGNMENT,                    // Sort by the location of the object inside the control
//--- Sort by real properties

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

Ahora podemos encontrar y añadir rápidamente a la lista todos los encabezados de pestaña de la misma fila y calcular su tamaño añadido para que se ajusten al tamaño del control y se posicionen correctamente.


Si estamos añadiendo a un objeto nuevas propiedades, también deberemos añadir su descripción. Todas las descripciones de las propiedades se organizan en un array multidimensional, cuya primera dimensión contendrá el índice del mensaje. Las dimensiones restantes contendrán textos en diferentes idiomas. Actualmente tenemos textos de mensajes para el ruso y el inglés, pero podemos añadir fácilmente otros idiomas aumentando la dimensionalidad del array y añadiendo textos en los idiomas correspondientes en las dimensiones necesarias.

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

   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_TAB_PAGE_ROW,                // Tab row index
   MSG_CANV_ELEMENT_PROP_TAB_PAGE_COLUMN,             // Tab column index
   MSG_CANV_ELEMENT_PROP_ALIGNMENT,                   // Location of an object inside the control
//--- Real properties of graphical elements

//--- String properties of graphical elements
   MSG_CANV_ELEMENT_PROP_NAME_OBJ,                    // Graphical element object name
   MSG_CANV_ELEMENT_PROP_NAME_RES,                    // Graphical resource name
   MSG_CANV_ELEMENT_PROP_TEXT,                        // Graphical element text
   MSG_CANV_ELEMENT_PROP_DESCRIPTION,                 // Graphical element description
  };
//+------------------------------------------------------------------+

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

   {"Режим установки размера вкладок","Tab Size Mode"},
   {"Порядковый номер вкладки","Tab ordinal number"},
   {"Номер ряда вкладки","Tab row number"},
   {"Номер столбца вкладки","Tab column number"},
   {"Местоположение объекта внутри элемента управления","Location of the object inside the control"},
   
//--- String properties of graphical elements
   {"Имя объекта-графического элемента","The name of the graphic element object"},
   {"Имя графического ресурса","Image resource name"},
   {"Текст графического элемента","Text of the graphic element"},
   {"Описание графического элемента","Description of the graphic element"},
  };
//+---------------------------------------------------------------------+

Ya hemos analizado la clase de mensajes de la biblioteca y su concepto de almacenamiento de datos en otro artículo.


Al utilizar la clase CCanvas de la Biblioteca Estándar MQL5, no siempre resulta posible obtener el código del error que ha impedido crear el elemento gráfico. Poco a poco iremos añadiendo correcciones a la biblioteca para poder entender los motivos por los que no se ha creado un elemento.

En el archivo \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh del elemento gráfico, además de generar un mensaje de error, añadiremos a los métodos de ajuste de la anchura y la altura una descripción del motivo del error de redimensionamiento:

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

Aquí hemos añadido a la macrosustitución que retorna el nombre del método una descripción del tipo de elemento cuyas dimensiones no se han podido cambiar, así como una indicación del valor del parámetro transmitido al método. Esto facilitará la comprensión de los errores al desarrollar las clases de la biblioteca y no afectará al usuario final. No obstante, de vez en cuando tendremos que pensar un poco en nosotros mismos ;)


Para poder mostrar las descripciones para las dos nuevas propiedades del elemento gráfico, necesitaremos añadir un bloque de código al método que retorna la descripción de la propiedad entera del elemento en el archivo \MQL5\Include\DoEasy\Objects\Graph\Forms\WinFormBase.mqh:

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

      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_TAB_PAGE_ROW                 ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_PAGE_ROW)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==CANV_ELEMENT_PROP_TAB_PAGE_COLUMN              ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_PAGE_COLUMN)+
         (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))
         )  :
      ""
     );
  }
//+------------------------------------------------------------------+

Aquí, dependiendo de la propiedad transmitida al método, crearemos un mensaje de texto y lo retornaremos desde el método.


Ahora vamos a pasar directamente a mejorar las clases del objeto WinForms TabControl.

El objeto consta de un contenedor en el que se encuentran las pestañas, que a su vez consta de dos objetos auxiliares: el campo de la pestaña y su encabezado. En el campo de la pestaña se colocan todos los objetos que debemos colocar en la pestaña, y con el encabezado de la pestaña, seleccionaremos la pestaña que queremos ver y con la que queremos trabajar. Nuestros encabezados de pestaña se implementan en la clase TabHeader heredada del objeto WinForms Button en el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\TabHeader.mqh.

Desde ahora tenemos dos nuevas propiedades que indican la ubicación del encabezado de la pestaña en la lista general de encabezados (la fila y la columna del encabezado se usan para colocarlo en un control), así que ahora eliminaremos las dos variables privadas innecesarias en las que antes se almacenaban estas propiedades:

//+------------------------------------------------------------------+
//| TabHeader object class of WForms TabControl                      |
//+------------------------------------------------------------------+
class CTabHeader : public CButton
  {
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 los métodos públicos que establecen y retornan estas dos propiedades, ahora escribiremos sus valores en las propiedades del objeto y retornaremos dichos valores desde las mismas, en lugar de hacerlo en las variables, como antes:

//--- 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 Tab row index
   void              SetRow(const int value)                { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_ROW,value);            }
   int               Row(void)                        const { return (int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_ROW);      }
//--- (1) Set and (2) return the Tab column index
   void              SetColumn(const int value)             { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_COLUMN,value);         }
   int               Column(void)                     const { return (int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_COLUMN);   }
//--- Set the tab location
   void              SetTabLocation(const int row,const int col)
                       {
                        this.SetRow(row);
                        this.SetColumn(col);
                       }


El objeto de encabezado de la pestaña se crea en el constructor con los valores de tamaño por defecto y luego se ajusta para que coincida con el modo de tamaño de encabezado establecido en la clase contenedor de objetos. Esto se debe al hecho de que para todos los objetos WinForms de la biblioteca usaremos los mismos valores para sus parámetros comunes a todos los objetos, mientras que los parámetros adicionales que pertenecen a un objeto en particular los configuramos después de su creación. Esto tiene sus ventajas y sus desventajas. La bueno es que podemos crear cualquier objeto con un solo método. Por otra parte, no siempre podremos construir de inmediato un objeto con los valores correctos de sus propiedades, así que deberemos añadirlos después de que el objeto haya sido creado.

En este caso, esto se relacionará con el tamaño del encabezado, que dependerá del modo de ajuste del tamaño. Aquí nos encontraremos con que el objeto originalmente creado en la coordenada indicada cambiará aún más sus dimensiones, y su coordenada inicial, situada en la esquina superior izquierda del objeto construido, ya no se encontrará donde estaba previsto originalmente. Por ello, además de redimensionar el encabezado, deberemos comprobar su posición en la coordenada correcta, ya que en algunos casos el objeto se desplazará y sobresaldrá de su contenedor.

Para el dimensionamiento Normal, podremos saber de antemano qué tamaño tendrá el encabezado, pues en este modo el tamaño del objeto se adapta al texto escrito en él, y este texto ya lo conocemos. Cuando los encabezados se encuentran en la parte superior e inferior del contenedor, los valores de Padding establecidos para el objeto (PaddingLeft y PaddingRight) se añadirán a la anchura y la altura calculadas según el tamaño del texto del objeto, mientras que PaddingTop y PaddingBottom se añadirán a la altura. Cuando los encabezados se colocan a la izquierda y a la derecha del contenedor (verticalmente), PaddingLeft y PaddingRight se añadirán a la altura y PaddingTop y PaddingBottom a la anchura, porque visualmente parece que el encabezado está simplemente girado 90° y su texto es vertical, por lo que la altura real del elemento gráfico será la anchura visible del objeto girado verticalmente.

Vamos a introducir los cambios en el método que establece todos los tamaños del encabezado. En el bloque de código encargado de establecer el tamaño del encabezado según el tamaño del texto para el modo Normal, añadiremos el control de los lados del contenedor en el que se ubica el encabezado: para ubicar los encabezados arriba y abajo, los valores de Padding se añadirán el orden correcto, es decir, a la anchura se añadirá el Padding a la izquierda y a la derecha, mientras que a la altura se añadirá el Padding arriba y abajo. Entre tanto, para ubicar los encabezados a la izquierda y a la derecha, los valores de Padding añadidos a la anchura y a la altura intercambiarán su lugar: a la anchura se añadirá el Padding arriba y abajo, mientras que a la altura se añadirá el Padding a la izquierda y a la derecha:

//--- 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 :
        switch(this.Alignment())
          {
           case CANV_ELEMENT_ALIGNMENT_TOP      :
           case CANV_ELEMENT_ALIGNMENT_BOTTOM   :
              this.TextSize(this.Text(),width,height);
              width+=this.PaddingLeft()+this.PaddingRight();
              height=h+this.PaddingTop()+this.PaddingBottom();
             break;
           case CANV_ELEMENT_ALIGNMENT_LEFT     :
           case CANV_ELEMENT_ALIGNMENT_RIGHT    :
              this.TextSize(this.Text(),height,width);
              height+=this.PaddingLeft()+this.PaddingRight();
              width=w+this.PaddingTop()+this.PaddingBottom();
             break;
           default:
             break;
          }
        break;
      //---CANV_ELEMENT_TAB_SIZE_MODE_FIXED
      //---CANV_ELEMENT_TAB_SIZE_MODE_FILL
      //--- For the Fixed mode, the dimensions remain specified,
      //--- In case of Fill, they are calculated in the StretchHeaders methods of the TabControl class
      default: break;
     }
//--- Set the results of changing the width and height to 'res'


Al final del método, las dimensiones del encabezado en el estado seleccionado y no seleccionado se establecerán exactamente de la misma forma. Como al seleccionar una pestaña clicando en el encabezado, el encabezado de la pestaña seleccionada aumentará su tamaño en dos píxeles en tres lados, el tamaño real de un encabezado ubicado horizontalmente se deberá aumentar en 4 píxeles de anchura y en 2 píxeles de altura. Cuando el encabezado se posiciona verticalmente, la anchura real del objeto será la altura del encabezado girado verticalmente, mientras que la altura real será la anchura del encabezado. En este caso, la anchura se incrementará en dos píxeles mientras que la altura aumentará en cuatro:

//--- Set the changed size for different button states
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
         this.SetWidthOn(this.Width()+4);
         this.SetHeightOn(this.Height()+2);
         this.SetWidthOff(this.Width());
         this.SetHeightOff(this.Height());
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
         this.SetWidthOn(this.Width()+2);
         this.SetHeightOn(this.Height()+4);
         this.SetWidthOff(this.Width());
         this.SetHeightOff(this.Height());
        break;
      default:
        break;
     }


El método con todos los cambios realizados tendrá ahora este aspecto en su totalidad:

//+------------------------------------------------------------------+
//| 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 :
        switch(this.Alignment())
          {
           case CANV_ELEMENT_ALIGNMENT_TOP      :
           case CANV_ELEMENT_ALIGNMENT_BOTTOM   :
              this.TextSize(this.Text(),width,height);
              width+=this.PaddingLeft()+this.PaddingRight();
              height=h+this.PaddingTop()+this.PaddingBottom();
             break;
           case CANV_ELEMENT_ALIGNMENT_LEFT     :
           case CANV_ELEMENT_ALIGNMENT_RIGHT    :
              this.TextSize(this.Text(),height,width);
              height+=this.PaddingLeft()+this.PaddingRight();
              width=w+this.PaddingTop()+this.PaddingBottom();
             break;
           default:
             break;
          }
        break;
      //---CANV_ELEMENT_TAB_SIZE_MODE_FIXED
      //---CANV_ELEMENT_TAB_SIZE_MODE_FILL
      //--- For the Fixed mode, the dimensions remain specified,
      //--- In case of Fill, they are calculated in the StretchHeaders methods of the TabControl class
      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
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
         this.SetWidthOn(this.Width()+4);
         this.SetHeightOn(this.Height()+2);
         this.SetWidthOff(this.Width());
         this.SetHeightOff(this.Height());
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
         this.SetWidthOn(this.Width()+2);
         this.SetHeightOn(this.Height()+4);
         this.SetWidthOff(this.Width());
         this.SetHeightOff(this.Height());
        break;
      default:
        break;
     }
   return true;
  }
//+------------------------------------------------------------------+


En el método que ajusta el tamaño y la posición del elemento en el estado "seleccionado" según su ubicación, no hemos realizado previamente un desplazamiento del encabezado ampliado para los casos en que os encabezados están a la izquierda y a la derecha del control. Además, había un pequeño error en el bloque de procesamiento de la ubicación del encabezado en la parte inferior: el operador break se había omitido, lo cual no causaba ningún error, ya que todos los casos sucesivos estaban vacíos y no se llamaba a ningún código. Ahora, esto provocará un comportamiento erróneo, pues se procesará el siguiente caso después del operador break omitido.

Vamos a añadir bloques de código que desplacen el encabezado expandido dos puntos en la dirección correcta para colocar los encabezados a la izquierda y a la derecha:

//+------------------------------------------------------------------+
//| 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());
          }
        break;
      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
        if(this.Move(this.CoordX()-2,this.CoordY()-2))
          {
           this.SetCoordXRelative(this.CoordXRelative()-2);
           this.SetCoordYRelative(this.CoordYRelative()-2);
          }
        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
        if(this.Move(this.CoordX(),this.CoordY()-2))
          {
           this.SetCoordXRelative(this.CoordXRelative());
           this.SetCoordYRelative(this.CoordYRelative()-2);
          }
        break;
      default:
        break;
     }
   return true;
  }
//+------------------------------------------------------------------+


Del mismo modo, mejoraremos el método que ajusta el tamaño y la posición de un elemento en el estado "no seleccionado" dependiendo de su ubicación: para ello, añadiremos bloques de código que retornarán el encabezado redimensionado a su ubicación original después de que el encabezado haya sido redimensionado en el método anterior al seleccionarlo:

//+------------------------------------------------------------------+
//| 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
        if(this.Move(this.CoordX()+2,this.CoordY()+2))
          {
           this.SetCoordXRelative(this.CoordXRelative()+2);
           this.SetCoordYRelative(this.CoordYRelative()+2);
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
        //--- shift the header to its original position and set the previous relative coordinates
        if(this.Move(this.CoordX(),this.CoordY()+2))
          {
           this.SetCoordXRelative(this.CoordXRelative());
           this.SetCoordYRelative(this.CoordYRelative()+2);
          }
        break;
      default:
        break;
     }
   return true;
  }
//+------------------------------------------------------------------+

Ahora, tras realizar estos ajustes, al seleccionar los encabezados de las pestañas situadas a la izquierda o a la derecha, aumentarán correctamente su tamaño al ser seleccionados y lo disminuirán al ser dejar de estarlo, haciéndose visualmente más grandes y permaneciendo visualmente en su posición original.

Cuando tenemos varias filas de encabezados de pestañas, al seleccionar una pestaña cuyo encabezado no está directamente adyacente a la propia pestaña, sino en algún lugar entre las filas de otras pestañas, deberemos mover toda la fila que contiene el encabezado de la pestaña seleccionada a ras de los campos de pestañas, y mover la fila que antes estaba adyacente a los campos a la fila con el encabezado seleccionado. En el artículo anterior, ya implementamos el método para ubicar los encabezados de las pestañas encima del control. Ahora deberemos hacer lo mismo con los encabezados de la parte inferior, izquierda y derecha.

Método que establece la fila del encabezado de pestaña seleccionado en la posición correcta en la parte inferior:

//+------------------------------------------------------------------+
//| Set the row of a selected tab header                             |
//| to the correct position at the bottom                            |
//+------------------------------------------------------------------+
void CTabHeader::CorrectSelectedRowBottom(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()+obj.Height();
   
//--- 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);
     }
  }
//+------------------------------------------------------------------+


Método que establece la fila del encabezado de pestaña seleccionado en la posición correcta en la parte izquierda:

//+------------------------------------------------------------------+
//| Set the row of a selected tab header                             |
//| to the correct position on the left                              |
//+------------------------------------------------------------------+
void CTabHeader::CorrectSelectedRowLeft(void)
  {
   int row_pressed=this.Row();      // Selected header row
   int x_pressed=this.CoordX();     // Coordinate where all headers with Row() equal to zero should be moved to
   int x0=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 X coordinate of the zero line
   CWinFormBase *obj=this.GetFieldObj();
   if(obj==NULL)
      return;
   x0=obj.CoordX()-this.Width()+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(x_pressed,header.CoordY()))
           {
            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(x0,header.CoordY()))
           {
            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);
     }
  }
//+------------------------------------------------------------------+


Método que establece la fila del encabezado de pestaña seleccionado en la posición correcta en la parte derecha:

//+------------------------------------------------------------------+
//| Set the row of a selected tab header                             |
//| to the correct position on the right                             |
//+------------------------------------------------------------------+
void CTabHeader::CorrectSelectedRowRight(void)
  {
   int row_pressed=this.Row();      // Selected header row
   int x_pressed=this.CoordX();     // Coordinate where all headers with Row() equal to zero should be moved to
   int x0=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 X coordinate of the zero line
   CWinFormBase *obj=this.GetFieldObj();
   if(obj==NULL)
      return;
   x0=obj.RightEdge();
   
//--- 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(x_pressed,header.CoordY()))
           {
            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(x0,header.CoordY()))
           {
            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);
     }
  }
//+------------------------------------------------------------------+

Ya vimos un método similar en un artículo anterior; se encargaba de desplazar una fila de encabezados cuando estos se ubicaban encima de un control. La lógica detrás de estos nuevos métodos es exactamente la misma, pero desplazaremos las filas de encabezados en el eje vertical Y para la parte inferior, y en el eje horizontal X para los encabezados de la izquierda y la derecha. La lógica completa se detalla en los comentarios del código: podrá estudiarla por sí mismo.


Al seleccionar una pestaña clicando en el encabezado, este se hará ligeramente más grande, y si fuera necesario, se trasladará desde la lista de filas de encabezados cerca del campo de la pestaña, y el borde visible resultante (debido al borde del campo) entre el encabezado y el campo de la pestaña, se borrará para que el campo y el encabezado parezcan un todo inseparable. En el último artículo, borramos el borde entre el campo y el encabezado, pero solo lo hicimos para la ubicación del encabezado en la parte superior e inferior. Ahora deberemos añadir la eliminación del borde entre el campo y el encabezado cuando este se posiciona a la izquierda y a la derecha.

En el archivo \MQL5\Include\DoEasy\Objects\Graph\WForms\TabField.mqh de la clase de objeto de campo de la pestaña, en el método que dibuja el marco del elemento dependiendo de la ubicación del encabezado, añadiremos el dibujado de una línea con el color de fondo en la ubicación del encabezado a la izquierda y a la derecha:

//+------------------------------------------------------------------+
//| 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    :
        this.DrawLine(0,header.BottomEdgeRelative()-2,0,header.CoordYRelative()+1,this.BackgroundColor(),this.Opacity());
        break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
        this.DrawLine(this.Width()-1,header.BottomEdgeRelative()-2,this.Width()-1,header.CoordYRelative()+1,this.BackgroundColor(),this.Opacity());
        break;
      default:
        break;
     }
  }
//+------------------------------------------------------------------+

Aquí todo es sencillo: obtenemos el puntero al objeto de encabezado correspondiente al campo; luego, a partir de él obtendremos sus dimensiones y, según la ubicación establecida para el encabezado, dibujaremos una línea con el color de fondo en el punto del campo donde el encabezado sea adyacente. Visualmente, esto difuminará el límite entre el campo y el encabezado, y los dos objetos comenzarán a aparecer como uno solo: la pestaña del control TabControl, que mejoraremos a continuación.


En el archivo de la clase de objeto de control TabControl \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\TabControl.mqh, declararemos cuatro métodos privados para expandir las filas del encabezado a lo ancho y a lo alto:

//--- Arrange the tab headers at the (1) top, (2) bottom, (3) left and (4) right
   void              ArrangeTabHeadersTop(void);
   void              ArrangeTabHeadersBottom(void);
   void              ArrangeTabHeadersLeft(void);
   void              ArrangeTabHeadersRight(void);
//--- Stretch tab headers by control size
   void              StretchHeaders(void);
//--- Stretch tab headers by (1) control width and height when positioned on the (2) left and (3) right
   void              StretchHeadersByWidth(void);
   void              StretchHeadersByHeightLeft(void);
   void              StretchHeadersByHeightRight(void);
public:


Fuera del cuerpo de la clase, escribiremos la implementación de estos métodos.

Método que expande los encabezados de las pestañas según el tamaño del control:

//+------------------------------------------------------------------+
//| Stretch tab headers by control size                              |
//+------------------------------------------------------------------+
void CTabControl::StretchHeaders(void)
  {
//--- Leave if the headers are in one row
   if(!this.Multiline())
      return;
//--- Depending on the location of headers
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
        this.StretchHeadersByWidth();
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
        this.StretchHeadersByHeightLeft();
        break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
        this.StretchHeadersByHeightRight();
        break;
      default:
        break;
     }
  }
//+------------------------------------------------------------------+

El método simplemente llama a los métodos correspondientes dependiendo de la ubicación de los encabezados de las pestañas. Un solo método bastará para ampliar la anchura, ya que todos los encabezados son siempre de izquierda a derecha, mientras que para expandir la altura, importará el lado en el que se encuentren los encabezados. Cuando se ubiquen a la izquierda, su orden será de abajo hacia arriba, y cuando se ubiquen a la izquierda, su orden será de arriba hacia abajo. Por consiguiente, tendremos dos métodos distintos para expandir la altura del encabezado hacia la izquierda y hacia la derecha.

Método que expande los encabezados de las pestañas a lo ancho del control:

//+------------------------------------------------------------------+
//| Stretch tab headers by control width                             |
//+------------------------------------------------------------------+
void CTabControl::StretchHeadersByWidth(void)
  {
//--- Get the list of tab headers
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Get the last title in the list
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
   if(last==NULL)
      return;
//--- In the loop by the number of header rows
   for(int i=0;i<last.Row()+1;i++)
     {
      //--- Get the list with the row index equal to the loop index
      CArrayObj *list_row=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_ROW,i,EQUAL);
      if(list_row==NULL)
         continue;
      //--- Get the width of the container, as well as the number of headers in a row, and calculate the width of each header
      int base_size=this.Width()-4;
      int num=list_row.Total();
      int w=base_size/(num>0 ? num : 1);
      //--- In the loop by row headers
      for(int j=0;j<list_row.Total();j++)
        {
         //--- Get the current and previous headers from the list by loop index
         CTabHeader *header=list_row.At(j);
         CTabHeader *prev=list_row.At(j-1);
         if(header==NULL)
            continue;
         //--- If the header size is changed
         if(header.Resize(w,header.Height(),false))
           {
            //--- Set new sizes for the header for pressed/unpressed states
            header.SetWidthOn(w+4);
            header.SetWidthOff(w);
            //--- If this is the first header in the row (there is no previous header in the list),
            //--- then it is not necessary to shift it - move on to the next iteration
            if(prev==NULL)
               continue;
            //--- Shift the header to the coordinate of the right edge of the previous header
            if(header.Move(prev.RightEdge(),header.CoordY()))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

En primer lugar, averiguaremos el número de filas del encabezado. Este número se puede averiguar recuperando el último encabezado de la lista de encabezados: su número de fila se escribirá en la propiedad Row del encabezado. Como los números de fila empiezan por cero, deberemos añadir un uno a este valor para indicar el número de filas.
A continuación, tendremos que obtener la lista de los encabezados dispuestos en cada fila y expandir todos los encabezados en ella a lo ancho del objeto. Como hemos añadido los valores de Row y Column a las propiedades del objeto, obtener la lista de encabezados de una fila se ha convertido en algo muy simple: solo tendremos que filtrar la lista con todos los encabezados según el valor de la fila y obtendremos una lista con los punteros a los objetos con el número de fila especificado. En un ciclo por la lista resultante, cambiaremos la anchura de cada encabezado al valor calculado anteriormente: la anchura del contenedor dividida por el número de encabezados de la fila. De la anchura del contenedor, no tomaremos toda la anchura del mismo, sino que quitaremos dos píxeles a la izquierda y a la derecha, para que los encabezados más externos no se extiendan más allá del contenedor cuando se seleccionen y cambien de tamaño. Como estamos dividiendo la dimensión por un valor desconocido de antemano, para evitar dividir por cero, comprobaremos el divisor por este valor, y si es cero, dividiremos por 1. Si el encabezado anterior no se encuentra en la lista (el índice del ciclo indica el primer encabezado), este encabezado no necesitará ser desplazado a ningún sitio: permanecerá en su lugar, mientras que todos los encabezados posteriores necesitarán desplazarse al borde derecho del anterior, ya que todos los encabezados han cambiado su anchura y se harán más grandes de tamaño, superponiéndose unos a otros.

Método que expande los encabezados de las pestañas según la altura del control cuando se posicionan a la izquierda:

//+------------------------------------------------------------------+
//| Stretch tab headers by control height                            |
//| when placed on the left                                          |
//+------------------------------------------------------------------+
void CTabControl::StretchHeadersByHeightLeft(void)
  {
//--- Get the list of tab headers
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Get the last title in the list
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
   if(last==NULL)
      return;
//--- In the loop by the number of header rows
   for(int i=0;i<last.Row()+1;i++)
     {
      //--- Get the list with the row index equal to the loop index
      CArrayObj *list_row=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_ROW,i,EQUAL);
      if(list_row==NULL)
         continue;
      //--- Get the height of the container, as well as the number of headers in a row, and calculate the height of each header
      int base_size=this.Height()-4;
      int num=list_row.Total();
      int h=base_size/(num>0 ? num : 1);
      //--- In the loop by row headers
      for(int j=0;j<list_row.Total();j++)
        {
         //--- Get the current and previous headers from the list by loop index
         CTabHeader *header=list_row.At(j);
         CTabHeader *prev=list_row.At(j-1);
         if(header==NULL)
            continue;
         //--- Save the initial header height
         int h_prev=header.Height();
         //--- If the header size is changed
         if(header.Resize(header.Width(),h,false))
           {
            //--- Set new sizes for the header for pressed/unpressed states
            header.SetHeightOn(h+4);
            header.SetHeightOff(h);
            //--- If this is the first header in the row (there is no previous header in the list),
            if(prev==NULL)
              {
               //--- Calculate the Y offset
               int y_shift=header.Height()-h_prev;
               //--- Shift the header by its calculated offset and move on to the next one
               if(header.Move(header.CoordX(),header.CoordY()-y_shift))
                 {
                  header.SetCoordXRelative(header.CoordX()-this.CoordX());
                  header.SetCoordYRelative(header.CoordY()-this.CoordY());
                 }
               continue;
              }
            //--- Shift the header by its calculated offset and move on to the next one
            if(header.Move(header.CoordX(),prev.CoordY()-header.Height()))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

La lógica del método es similar a la anterior, pero un poco más complicada. Como los encabezados se posicionan a la izquierda, partiendo del borde inferior de su contenedor, y el punto de anclaje del encabezado está en su esquina superior izquierda, el redimensionamiento del mismo hará que el borde inferior del encabezado quede por debajo del borde inferior del contenedor. Así que aquí deberemos desplazar el primer encabezado hacia arriba según el desplazamiento calculado. Para ello, recordaremos la altura del encabezado antes de redimensionarlo, y después de hacerlo, calcularemos en qué magnitud se ha modificado el tamaño. Luego desplazaremos el primer encabezado en el eje Y en esta misma magnitud, de forma que su borde inferior no sobrepase su contenedor.


Método que expande los encabezados de las pestañas según la altura del control cuando se ubica a la derecha:

//+------------------------------------------------------------------+
//| Stretch tab headers by control height                            |
//| when placed on the right                                         |
//+------------------------------------------------------------------+
void CTabControl::StretchHeadersByHeightRight(void)
  {
//--- Get the list of tab headers
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Get the last title in the list
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
   if(last==NULL)
      return;
//--- In the loop by the number of header rows
   for(int i=0;i<last.Row()+1;i++)
     {
      //--- Get the list with the row index equal to the loop index
      CArrayObj *list_row=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_ROW,i,EQUAL);
      if(list_row==NULL)
         continue;
      //--- Get the height of the container, as well as the number of headers in a row, and calculate the height of each header
      int base_size=this.Height()-4;
      int num=list_row.Total();
      int h=base_size/(num>0 ? num : 1);
      //--- In the loop by row headers
      for(int j=0;j<list_row.Total();j++)
        {
         //--- Get the current and previous headers from the list by loop index
         CTabHeader *header=list_row.At(j);
         CTabHeader *prev=list_row.At(j-1);
         if(header==NULL)
            continue;
         //--- If the header size is changed
         if(header.Resize(header.Width(),h,false))
           {
            //--- Set new sizes for the header for pressed/unpressed states
            header.SetHeightOn(h+4);
            header.SetHeightOff(h);
            //--- If this is the first header in the row (there is no previous header in the list),
            //--- then it is not necessary to shift it - move on to the next iteration
            if(prev==NULL)
               continue;
            //--- Shift the header to the coordinate of the bottom edge of the previous header
            if(header.Move(header.CoordX(),prev.BottomEdge()))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

El método es idéntico al que expande los encabezados a lo ancho del contenedor, pero aquí los expandiremos a lo alto. Como los encabezados están a la izquierda y su conteo se realiza de arriba hacia abajo, no deberemos ajustar la ubicación del primer encabezado después de redimensionarlo: sus coordenadas iniciales serán las mismas que en su punto de ubicación, y el objeto se expandirá hacia abajo sin salirse del contenedor.

El método que crea el número especificado de pestañas ha cambiado, ya que necesitaremos calcular las coordenadas y dimensiones iniciales basándonos en las ubicaciones de los encabezados. En este caso, para colocar los encabezados a la izquierda y a la derecha, le asignaremos a la anchura y la altura del encabezado la altura y anchura correspondientes transmitidas al método. Si el encabezado se encuentra a la izquierda, giraremos el texto del encabezado 90° en vertical, y si está a la derecha, 270°:

//+------------------------------------------------------------------+
//| 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;
      int header_w=w;
      int header_h=h;
      
      //--- Set the current X and Y coordinate depending on the location of the tab headers
      switch(this.Alignment())
        {
         case CANV_ELEMENT_ALIGNMENT_TOP     :
           header_w=w;
           header_h=h;
           header_x=(header==NULL ? 2 : header.RightEdgeRelative());
           header_y=0;
           break;
         case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
           header_w=w;
           header_h=h;
           header_x=(header==NULL ? 2 : header.RightEdgeRelative());
           header_y=this.Height()-header_h;
           break;
         case CANV_ELEMENT_ALIGNMENT_LEFT    :
           header_w=h;
           header_h=w;
           header_x=2;
           header_y=(header==NULL ? this.Height()-header_h-2 : header.CoordYRelative()-header_h);
           break;
         case CANV_ELEMENT_ALIGNMENT_RIGHT   :
           header_w=h;
           header_h=w;
           header_x=this.Width()-header_w;
           header_y=(header==NULL ? 2 : header.BottomEdgeRelative());
           break;
         default:
           break;
        }
      //--- Create the TabHeader object
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,header_x,header_y,header_w,header_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));
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT)
         header.SetFontAngle(90);
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_RIGHT)
         header.SetFontAngle(270);
      header.SetTabSizeMode(this.TabSizeMode());

      
      //--- Save the initial height of the header and set its size in accordance with the header size setting mode
      int h_prev=header_h;
      header.SetSizes(header_w,header_h);
      //--- Save the initial height of the header and set its size in accordance with the header size setting mode
      //--- shift it by the calculated value only for headers on the left
      int y_shift=header.Height()-h_prev;
      if(header.Move(header.CoordX(),header.CoordY()-(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT ? y_shift : 0)))
        {
         header.SetCoordXRelative(header.CoordX()-this.CoordX());
         header.SetCoordYRelative(header.CoordY()-this.CoordY());
        }
      
      //--- 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_w=this.Width();
      int field_h=this.Height()-header.Height();
      int header_shift=0;
      
      switch(this.Alignment())
        {
         case CANV_ELEMENT_ALIGNMENT_TOP     :
           field_x=0;
           field_y=header.BottomEdgeRelative();
           field_w=this.Width();
           field_h=this.Height()-header.Height();
           break;
         case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
           field_x=0;
           field_y=0;
           field_w=this.Width();
           field_h=this.Height()-header.Height();
           break;
         case CANV_ELEMENT_ALIGNMENT_LEFT    :
           field_x=header.RightEdgeRelative();
           field_y=0;
           field_h=this.Height();
           field_w=this.Width()-header.Width();
           break;
         case CANV_ELEMENT_ALIGNMENT_RIGHT   :
           field_x=0;
           field_y=0;
           field_h=this.Height();
           field_w=this.Width()-header.Width();
           break;
         default:
           break;
        }
      
      //--- Create the TabField object (tab field)
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,field_x,field_y,field_w,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;
  }
//+------------------------------------------------------------------+

La lógica y las mejoras del método se describen en los comentarios del código. Además, las características principales se indican antes del listado del método, y están resaltadas a color. Tenga en cuenta que para ubicar el encabezado a la izquierda, aún deberemos recordar el tamaño del encabezado antes de cambiarlo; luego calcularemos la magnitud del desplazamiento y moveremos el encabezado redimensionado a la posición correcta.

El método para colocar los encabezados de las pestañas en la parte superior, que escribimos en el artículo anterior, también ha cambiado:

//+------------------------------------------------------------------+
//| 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())
        {
         //--- 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 minus 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 minus 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),
            //--- Increase the row index, calculate the new shift (so that the next object is compared with the TabControl left edge + 2 pixels),
            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());
           }
        }
      //--- 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
   if(last!=NULL)
     {
      //--- If the mode of stretching headers to the width of the container is set, call the stretching method
      if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL)
         this.StretchHeaders();
      //--- If this is not the first row (with index 0)
      if(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;
            //--- get the tab field corresponding to the received header
            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);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Hemos descrito la lógica del método al completo en los comentarios al código, así que no la repetiremos aquí: el lector podrá estudiarla por sí mismo.


Método que coloca los encabezados de las pestañas en la parte inferior:

//+------------------------------------------------------------------+
//| Arrange tab headers at the bottom                                |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersBottom(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())
        {
         //--- 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 minus 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 minus 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),
            //--- Increase the row index, calculate the new shift (so that the next object is compared with the TabControl left edge + 2 pixels),
            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());
           }
        }
      //--- 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
   if(last!=NULL)
     {
      //--- If the mode of stretching headers to the width of the container is set, call the stretching method
      if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL)
         this.StretchHeaders();
      //--- If this is not the first row (with index 0)
      if(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;
            //--- get the tab field corresponding to the received header
            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()))
              {
               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);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

El método es idéntico al que coloca los encabezados en la parte superior. La única diferencia es la dirección en la que se desplazan las filas del encabezado, ya que en el método anterior están en la parte inferior y se desplazan de forma inversa.


Método para colocar encabezados de pestañas a la izquierda:

//+------------------------------------------------------------------+
//| Arrange tab headers on the left                                  |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersLeft(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 y1_base=this.BottomEdgeRelative()-2;  // Initial Y coordinate
   int y2_base=2;                            // Final Y coordinate
   int y_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())
        {
         //--- Calculate the value of the upper edge of the header, taking into account that
         //--- the origin always comes from the bottom edge of TabControl minus 2 pixels
         int y2=header.CoordYRelative()+y_shift;
         //--- If the calculated value does not go beyond the upper edge of the TabControl minus 2 pixels,
         //--- set the column number equal to the loop index minus the value in the n variable
         if(y2>=y2_base)
            col=i-n;
         //--- If the calculated value goes beyond the upper edge of the TabControl minus 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),
            //--- Increase the row index, calculate the new shift (so that the next object is compared with the TabControl left edge + 2 pixels),
            row++;
            y_shift=this.BottomEdge()-header.BottomEdge()-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()-header.Row()*header.Width(),header.CoordY()+y_shift))
           {
            header.SetCoordXRelative(header.CoordX()-this.CoordX());
            header.SetCoordYRelative(header.CoordY()-this.CoordY());
           }
        }
      //--- 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
   if(last!=NULL)
     {
      //--- If the mode of stretching headers to the width of the container is set, call the stretching method
      if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL)
         this.StretchHeaders();
      //--- If this is not the first row (with index 0)
      if(last.Row()>0)
        {
         //--- Calculate the offset of the tab field X coordinate
         int x_shift=last.Row()*last.Width();
         //--- 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;
            //--- get the tab field corresponding to the received header
            if(header.Move(header.CoordX()+x_shift,header.CoordY()))
              {
               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()+x_shift,field.CoordY()))
              {
               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()-x_shift,field.Height(),false);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Aquí los encabezados se colocan a la izquierda y las filas se desplazan en el eje X. Por lo demás, la lógica es idéntica a la de los métodos anteriores.


Método que coloca los encabezados de las pestañas en el lado derecho:

//+------------------------------------------------------------------+
//| Arrange tab headers to the right                                 |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersRight(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 y1_base=2;                            // Initial Y coordinate
   int y2_base=this.BottomEdgeRelative()-2;  // Final Y coordinate
   int y_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())
        {
         //--- Calculate the value of the bottom edge of the header, taking into account that
         //--- the origin always comes from the upper edge of TabControl + 2 pixels
         int y2=header.BottomEdgeRelative()-y_shift;
         //--- If the calculated value does not go beyond the bottom edge of the TabControl minus 2 pixels,
         //--- set the column number equal to the loop index minus the value in the n variable
         if(y2<y2_base)
            col=i-n;
         //--- If the calculated value goes beyond the bottom edge of the TabControl minus 2 pixels,
         else
           {
            //--- Increase the row index, calculate the new shift (so that the next object is compared with the TabControl bottom edge + 2 pixels),
            //--- Increase the row index, calculate the new shift (so that the next object is compared with the TabControl left edge + 2 pixels),
            row++;
            y_shift=header.CoordYRelative()-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()+header.Row()*header.Width(),header.CoordY()-y_shift))
           {
            header.SetCoordXRelative(header.CoordX()-this.CoordX());
            header.SetCoordYRelative(header.CoordY()-this.CoordY());
           }
        }
      //--- 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
   if(last!=NULL)
     {
      //--- If the mode of stretching headers to the width of the container is set, call the stretching method
      if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL)
         this.StretchHeaders();
      //--- If this is not the first row (with index 0)
      if(last.Row()>0)
        {
         //--- Calculate the offset of the tab field X coordinate
         int x_shift=last.Row()*last.Width();
         //--- 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;
            //--- get the tab field corresponding to the received header
            if(header.Move(header.CoordX()-x_shift,header.CoordY()))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
               //--- change the tab field size to the X offset value
               field.Resize(field.Width()-x_shift,field.Height(),false);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

La lógica es la misma que en el método anterior, pero los desplazamientos de las filas se dan a la inversa, porque los encabezados están a la derecha.

Todos los métodos anteriores están comentados con detalle en el código: podrá analizarlos por su cuenta. En cualquier caso, podrá plantear cualquier duda en los comentarios al artículo.

Ahora ya podemos probar todos los cambios y mejoras. Destacaremos que para disponer los encabezados de las pestañas en una sola fila, nos faltará la funcionalidad necesaria para recortar la parte visible/invisible del elemento gráfico. Por ello, si teniendo varias pestañas seleccionamos el modo de encabezado de una sola fila (modo multilínea desactivado), todos los encabezados se alinearán en una sola línea, extendiéndose más allá del control. Trataremos este problema en artículos posteriores, y dejaremos "stubs" en los métodos de las clases que hemos revisado para este modo: allí escribiremos el código para procesar este modo.

Simulación

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

En los parámetros de entrada de asesor, añadiremos variables para especificar el modo multilínea y el lado de ubicación del encabezado 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                          InpButtListMSelect   =  false;                  // ButtonListBox Button MultiSelect flag
sinput   bool                          InpListBoxMColumn    =  true;                   // ListBox MultiColumn flag
sinput   bool                          InpTabCtrlMultiline  =  true;                   // Tab Control Multiline 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


Luego aumentaremos ligeramente(en 10 píxeles) la anchura del panel a crear:

//--- Create WinForms Panel object
   CPanel *pnl=NULL;
   pnl=engine.CreateWFPanel("WFPanel",50,50,410,200,array_clr,200,true,true,false,-1,FRAME_STYLE_BEVEL,true,false);
   if(pnl!=NULL)
     {

y la anchura del segundo contenedor GroupBox, en 12 píxeles:

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

Haremos esto solo porque no tenemos la funcionalidad necesaria para recortar las partes invisibles de los elementos gráficos, y todos los objetos WinForm que están encima de sus objetos padre y sean más grandes que el contenedor (el objeto padre) quedarán fuera del contenedor. Por ejemplo, un CheckBox colocado en el campo de pestañas se extenderá más allá del mismo cuando los encabezados se coloquen a la izquierda, o también se extenderá más allá del mismo y cubrirá los encabezados de las pestañas colocados a la derecha del TabControl. Hasta que no haya suficiente funcionalidad, deberemos ocultar esas carencias :)

En el manejador OnInit(), después de crear el TabControl, estableceremos el diseño del encabezado de la pestaña y la resolución del encabezado en varias filas especificadas en los parámetros de entrada de asesor:

            //--- 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((ENUM_CANV_ELEMENT_ALIGNMENT)InpHeaderAlignment);
               tab_ctrl.SetMultiline(InpTabCtrlMultiline);
               tab_ctrl.SetHeaderPadding(6,0);
               tab_ctrl.CreateTabPages(9,0,50,16,TextByLanguage("Вкладка","TabPage"));


Al crear el control ListBox en la tercera pestaña de TabControl, estableceremos su coordenada Y más cerca de la parte superior de la pestaña:

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

Antes, el objeto se posicionaba en la coordenada 12, lo cual hacía que se extendiera más allá del campo de pestañas en la parte inferior cuando los encabezados de las pestañas estaban dispuestos en varias filas (porque el tamaño del campo de pestañas disminuye en proporción al número de filas de encabezados).

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


Como podemos ver, la ubicación de los encabezados de las pestañas a la izquierda y a la derecha funciona correctamente. Existen algunos defectos que describiremos y solucionaremos en el próximo artículo, pero por ahora todo resulta satisfactorio.


¿Qué es lo próximo?

En el próximo artículo, continuaremos trabajando con el control 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
DoEasy. Elementos de control (Parte 15): Objeto WinForms TabControl - múltiples filas de encabezados de pestañas, métodos de trabajo con pestañas

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

Archivos adjuntos |
MQL5.zip (4435.36 KB)
Redes neuronales: así de sencillo (Parte 27): Aprendizaje Q profundo (DQN) Redes neuronales: así de sencillo (Parte 27): Aprendizaje Q profundo (DQN)
Seguimos explorando el aprendizaje por refuerzo. En este artículo, hablaremos del método de aprendizaje Q profundo o deep Q-learning. El uso de este método permitió al equipo de DeepMind crear un modelo capaz de superar a los humanos jugando a los videojuegos de ordenador de Atari. Nos parece útil evaluar el potencial de esta tecnología para las tareas comerciales.
Redes neuronales: así de sencillo (Parte 26): Aprendizaje por refuerzo Redes neuronales: así de sencillo (Parte 26): Aprendizaje por refuerzo
Continuamos estudiando los métodos de aprendizaje automático. En este artículo, iniciaremos otro gran tema llamado «Aprendizaje por refuerzo». Este enfoque permite a los modelos establecer ciertas estrategias para resolver las tareas. Esperamos que esta propiedad del aprendizaje por refuerzo abra nuevos horizontes para la construcción de estrategias comerciales.
Aprendiendo a diseñar un sistema de trading con VIDYA Aprendiendo a diseñar un sistema de trading con VIDYA
Bienvenidos a un nuevo artículo de la serie dedicada a la creación de sistemas comerciales basados en indicadores técnicos populares. En este artículo hablaremos sobre el indicador VIDYA (Variable Index Dynamic Average) y crearemos un sistema comercial basado en sus lecturas.
Indicador técnico de preparación propia Indicador técnico de preparación propia
En este artículo, analizaremos algunos algoritmos que nos permitirán crear nuestro propio indicador técnico. Asimismo, veremos cómo, con unos supuestos iniciales muy sencillos, podremos obtener resultados bastante complejos e interesantes.