English Русский 中文 Español Deutsch 日本語
preview
DoEasy. Controles (Parte 16): Objeto WinForms TabControl - múltiplas fileiras de cabeçalhos de guias, modo esticamento de cabeçalhos consoante o tamanho do contêiner

DoEasy. Controles (Parte 16): Objeto WinForms TabControl - múltiplas fileiras de cabeçalhos de guias, modo esticamento de cabeçalhos consoante o tamanho do contêiner

MetaTrader 5Exemplos | 22 novembro 2022, 11:01
249 0
Artyom Trishkin
Artyom Trishkin

Conteúdo


Ideia

No último artigo, explicamos os modos de exibição do cabeçalho da guia:

Se um controle tiver mais guias do que pode caber na largura do objeto (estando localizadas na parte superior), os cabeçalhos que não se encaixam no elemento podem ser recortados com a presença de botões para rolagem, ou, se o objeto tiver o sinalizador do modo Multiline, serão colocados vários cabeçalhos (tantos quantos estiverem compreendidos no tamanho do elemento) em várias linhas. Para o modo de várias linhas, existem três maneiras de definir o tamanho das guias (SizeMode):

  • Normal - a largura das guias é definida conforme a largura do texto do cabeçalho, o espaço especificado nos valores PaddingWidth e PaddingHeight do cabeçalho é adicionado ao longo das bordas do cabeçalho;
  • Fixed - tamanho fixo especificado nas configurações do controle. O texto do cabeçalho é truncado se não couber nas dimensões do controle;
  • FillToRight - as guias que cabem na largura do controle são esticadas para preencher toda a largura.

Ao selecionar uma guia quando o modo Multiline está ativo, seu cabeçalho, que não faz borda com o campo da guia, junto com toda a linha em que está localizado, é movido para o campo da guia, e os cabeçalhos que foram incorporados no campo são inseridos no lugar da linha da guia selecionada.


E já realizamos a funcionalidade de colocação de guias em cima do controle nos modos Normal e Fixed.

Hoje vamos usar o modo Multiline para colocar as guias em todos os lados do controle, e vamos adicionar o modo FillToRight para definir o tamanho das guias, em particular para facilitar o esticamento das guias de acordo com o tamanho do controle. Assim, ao colocar fileiras de cabeçalhos de guias na parte superior ou inferior do contêiner, os cabeçalhos serão esticados até a altura do controle. Quando os cabeçalhos das guias são posicionados à esquerda ou à direita, eles serão esticados até a altura do controle Nesse caso, a área na qual será calculado o tamanho e sobre qual os cabeçalhos precisam ser esticados será dois pixels menor em cada lado, pois a guia selecionada (isto é, seu cabeçalho) aumenta de tamanho em 4 pixels quando clicamos nele. Por isso, se não for deixado um espaço de dois pixels para o cabeçalho mais externo, ao selecioná-lo e redimensioná-lo de acordo, sua borda se estenderá além do controle.

Quanto à exibição de cabeçalhos de guias em uma linha, podemos dizer que se houver mais cabeçalhos do que os que podem ser acomodados conforme o tamanho do controle, todos os cabeçalhos que se estendem além do contêiner serão colocados fora do mesmo. Como ainda não temos funcionalidade suficiente para recortar esses elementos gráficos que se esticam além do contêiner, esse modo ainda não é considerado. Trataremos de sua implementação em artigos posteriores.


Modificando as classes da biblioteca

Precisaremos pesquisar nas listas de cabeçalhos de guias todos os cabeçalhos localizados na mesma linha, para, assim, poder manusear apenas os cabeçalhos de dada linha. A maneira mais fácil de fazer isso é adicionar novas propriedades ao objeto WinForms da biblioteca e usar a funcionalidade de longa data da biblioteca para pesquisar e filtrar listas de objetos.

No arquivo \MQL5\Include\DoEasy\Defines.mqh, adicionamos duas novas propriedades à lista de propriedades inteiras do elemento gráfico na tela e aumentamos seu número total de 90 para 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
//+------------------------------------------------------------------+


Adicionamos novas propriedades à lista de critérios para ordenar os elementos gráficos na tela:

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

Agora podemos encontrar e adicionar rapidamente à lista todos os cabeçalhos de guias localizados na mesma linha e calcular seu tamanho conjunto para que se estendam consoante o tamanho do controle e sejam poscionados corretamente.


Se novas propriedades são adicionadas ao objeto, precisamos acrescentar sua descrição. Todas as descrições de propriedades estão localizadas em um array multidimensional, onde a primeira dimensão contém o índice da mensagem e as demais dimensões contêm textos em diferentes idiomas. No momento, temos mensagens em russo e inglês, mas outros idiomas podem ser facilmente adicionados aumentando a dimensão do array e adicionando textos nos idiomas correspondentes nas dimensões necessárias.

No arquivo \MQL5\Include\DoEasy\Data.mqh, escrevemos os índices das novas mensagens:

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

e as mensagens de texto correspondentes aos índices recém-adicionados:

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

Já analisamos a classe de mensagens da biblioteca e seu conceito de armazenamento de dados em um artigo separado.


Ao usar a classe CCanvas da Biblioteca Padrão MQL5, nem sempre é possível obter um código de erro correspondente à falha na criação de elemento gráfico. Estamos gradualmente adicionando correções à biblioteca para nos ajudar a entender por que um elemento não foi criado.

No arquivo do elemento gráfico \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh, além de exibir a mensagem de erro, adicione uma descrição da causa do erro de redimensionamento aos métodos de configuração de largura e altura:

//+------------------------------------------------------------------+
//| Set a new width                                                  |
//+------------------------------------------------------------------+
bool CGCnvElement::SetWidth(const int width)
  {
   if(this.GetProperty(CANV_ELEMENT_PROP_WIDTH)==width)
      return true;
   if(!this.m_canvas.Resize(width,this.m_canvas.Height()))
     {
      CMessage::ToLog(DFUN+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;
  }
//+------------------------------------------------------------------+

Aqui adicionamos à substituição de macro que retorna o nome do método, uma descrição do tipo de elemento que não pode ser redimensionado e uma indicação de qual valor de parâmetro foi passado para o método. Isto facilita a compreensão de erros ao desenvolver classes de bibliotecas e não diz respeito ao usuário final. Mas às vezes você também precisa se lembrar de si mesmo ;)


Para poder exibir as descrições de duas novas propriedades de um elemento gráfico, precisamos adicionar um bloco de código ao método que retorna a descrição da propriedade inteira do elemento no arquivo \MQL5\Include\DoEasy\Objects\Graph\WForms\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))
         )  :
      ""
     );
  }
//+------------------------------------------------------------------+

Aqui: dependendo da propriedade passada ao método, criamos uma mensagem de texto e a retornamos desde o método.


Agora vamos tratar diretamente da modificação das classes do objeto WinForms TabControl

O objeto consiste em um contêiner no qual estão localizadas as guias, que por sua vez consistem em dois objetos auxiliares - o campo da guia e seu cabeçalho. Todos os objetos que devem estar localizados na guia são colocados no campo da guia, e selecionamos a guia com a qual queremos trabalhar. Nossos cabeçalhos de guia são elaborados na classe TabHeader herdada do objeto WinForms Button no arquivo \MQL5\Include\DoEasy\Objects\Graph\WForms\TabHeader.mqh.

Como agora temos duas novas propriedades que indicam para a guia, ou melhor, para o cabeçalho da guia, sua localização na lista geral de cabeçalhos, isto é, a linha e a coluna do cabeçalho em que está localizada no controle, agora removeremos duas variáveis privadas desnecessárias, nas quais anteriormente essas propriedades eram armazenadas:

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


Nos métodos públicos que definem e retornam essas duas propriedades, agora seus valores serão escritos nas propriedades do objeto e retornados a partir de suas propriedades, e não em variáveis, como era 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);
                       }


O objeto-cabeçalho da guia é criado no construtor com valores de dimensionamento padrão e, em seguida, é dimensionado para corresponder ao modo de dimensionamento de cabeçalho definido na classe-contêiner do objeto. Isto porque para todos os objetos WinForms da biblioteca usamos os mesmos valores para seus parâmetros, comuns a todos os objetos, e definimos parâmetros adicionais que pertencem a um determinado objeto depois que ele é criado. Isso traz vantagens e limitações. A vantagem está no fato de que podemos criar qualquer objeto com um método, e a limitação está em que nem sempre é possível construir imediatamente um objeto com os valores de propriedades desejados, e estes têm de ser definidos após a criação do objeto.

Neste caso, trata-se dos tamanhos dos cabeçalhos, que dependem do modo de dimensionamento. Aqui nos deparamos com o fato de que o objeto inicialmente criado nas coordenadas especificadas muda ainda mais suas dimensões, e sua coordenada inicial, localizada no canto superior esquerdo do objeto construído, não está mais onde foi originalmente planejada. Portanto, além de alterar o tamanho do cabeçalho, também precisamos controlar sua localização na coordenada desejada, pois em alguns casos o objeto é deslocado e se projeta além de seu contêiner.

Para o modo de dimensionamento Normal, podemos descobrir antecipadamente quais tamanhos o cabeçalho receberá, porque, neste modo, o tamanho do objeto se ajusta ao texto escrito nele, e esse texto é conhecido por nós. Quando os cabeçalhos estão localizados na parte superior e inferior do contêiner, os valores de Padding definidos para o objeto (PaddingLeft e PaddingRight) são adicionados à largura e altura calculadas pelo tamanho do texto do objeto, e PaddingTop e PaddingBottom são adicionado à altura. Quando os cabeçalhos são posicionados à esquerda e à direita do contêiner (verticalmente), PaddingLeft e PaddingRight são adicionados à altura do objeto e PaddingTop e PaddingBottom são adicionados à largura, pois visualmente parece que o cabeçalho é simplesmente girado 90 ° e seu texto é vertical, então a altura real do elemento gráfico é a largura aparente do objeto girado verticalmente.

Vamos fazer alterações no método que define todos os tamanhos de cabeçalho. No bloco de código para ajustar o tamanho do cabeçalho ao tamanho do texto no modo Normal, escrevemos um controle para o lado do contêiner no qual os cabeçalhos estão localizados, assim: para cabeçalhos colocados em cima e em baixo, o Padding é adicionado na ordem correta, isto é, o Padding é adicionado à esquerda e à direita na largura, e o Padding é adicionado em cima e em baixo na altura, enquanto que para cabeçalhos colocados à esquerda e à direita, o Padding é invertido, isto é, o Padding é adicionado em cima e em baixo na largura, e Padding é adicionado à esquerda e à direita na altura:

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


No final do método, exatamente da mesma maneira, os tamanhos são definidos para o cabeçalho nos estados selecionado e não selecionado. Como a seleção de uma guia ao clicar no cabeçalho aumenta o cabeçalho da guia selecionada em dois pixels em três lados, o tamanho real do cabeçalho horizontal deve ser aumentado em 4 pixels de largura e dois de altura. Quando o cabeçalho é posicionado verticalmente, a largura real do objeto é a altura do cabeçalho girado verticalmente e a altura real é a largura do cabeçalho. Para este caso, a largura é aumentada em dois pixels e a altura é aumentada em quatro:

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


Agora o método com todas as alterações feitas fica assim:

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


No método que ajusta o tamanho e a posição do elemento no estado "selecionado" dependendo de sua localização, não fizemos anteriormente um deslocamento de cabeçalho estendido para os casos em que os cabeçalhos estão à esquerda e à direita do controle. Além disso, ocorreu uma pequena falha no bloco de processamento da posição do cabeçalho inferior - o operador break foi ignorado, o que não causou nenhum erro, pois todos os casos estavam vazios e nenhum código foi chamado. Agora, isso causará um comportamento incorreto, isto é, o caso, após o operador break ignorado, será processado.

Vamos adicionar blocos de código que deslocam o cabeçalho estendido dois pontos na direção certa para posicionar os cabeçalhos à esquerda e à direita:

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


Da mesma forma, finalizaremos o método que ajusta o tamanho e a posição do elemento no estado "não selecionado" dependendo de sua localização - adicionaremos blocos de código que deslocam o cabeçalho com o tamanho restaurado para sua localização original após o o cabeçalho é deslocado quando o tamanho aumenta no método acima quando é selecionado:

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

Agora, após essas melhorias, ao selecionar os cabeçalhos das guias localizadas à esquerda ou à direita, elas aumentarão corretamente seu tamanho quando selecionadas e diminuirão quando desmarcadas, ficando visualmente um pouco maiores e visualmente permanecendo em seu local original.

Quando temos várias linhas de cabeçalhos de guias, ao selecionar uma guia cujo cabeçalho não esteja diretamente adjacente à guia em si, mas em algum lugar entre as linhas de outras guias, precisamos mover toda a linha contendo o cabeçalho da guia selecionada para os campos de guias, e mover a linha que estava anteriormente adjacente aos campos para a linha com o cabeçalho selecionado. No último artigo já fizemos o método para posicionar os cabeçalhos das guias em cima do controle. Agora precisamos criar métodos semelhantes para posicionar cabeçalhos na parte inferior, esquerda e direita.

Método que define a linha de cabeçalho da guia selecionada para a posição correta na 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 define a linha de cabeçalho da guia selecionada para a posição esquerda correta:

//+------------------------------------------------------------------+
//| 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 define a linha de cabeçalho da guia selecionada para a posição correta à direita:

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

Consideramos um método semelhante no último artigo, para mover uma série de cabeçalhos quando eles estão localizados na parte superior do controle. A lógica por trás destes novos métodos é exatamente a mesma, mas, para a localização na parte inferior, movemos as linhas de cabeçalho ao longo do eixo Y, e para a localização à esquerda e à direita, movemo-las ao longo do eixo X. Toda a lógica está detalhada nos comentários ao código, que deixaremos para que você aprenda por conta própria.


Quando uma guia é selecionada clicando no cabeçalho, este fica um pouco maior, e, se necessário, ele é transferido desde a lista de linhas de cabeçalhos próximas ao campo da guia, e a borda visível resultante (por conta da moldura do campo) entre os cabeçalhos e o campo da guia são apagados para que o campo e o cabeçalho pareçam um todo indivisível. Tratamos do apagamento da borda entre o campo e o cabeçalho no último artigo, mas fizemos isso apenas para a localização do cabeçalho acima e abaixo. Agora precisamos adicionar um desfoque de borda entre o campo e o cabeçalho quando o último estiver localizado à esquerda e à direita.

No arquivo \MQL5\Include\DoEasy\Objects\Graph\WForms\TabField.mqh da classe de objeto-campo da guia, no método que desenha a moldura do elemento dependendo da localização do cabeçalho, adicionamos um desenho de linha com a cor de fundo no local do cabeçalho à esquerda e à direita:

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

Basicamente aqui pegamos um ponteiro para o objeto-cabeçalho correspondente a este campo, a partir dele obtemos suas dimensões e, de acordo com sua localização definida para o cabeçalho, traçamos uma linha com a cor de fundo no local do campo ao qual o cabeçalho é adjacente. Visualmente, isso desfoca a borda entre o campo e o cabeçalho, e os dois objetos começam a aparecer como um, isto é, como uma guia do controle TabControl, que alteraremos ainda mais.


No arquivo de classe de objeto de controle TabControl \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\TabControl.mqh, vamos declarar quatro métodos privados para esticar linhas de cabeçalhos em largura e altura:

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


Escrevemos a implementação deste método fora do corpo da classe:

Método que estica os cabeçalhos das guias conforme o tamanho do controle:

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

O método simplesmente chama os métodos apropriados dependendo da localização dos cabeçalhos das guias. Para o alongamento em largura, apenas um método é suficiente, pois todos os cabeçalhos estão sempre localizados da esquerda para a direita, enquanto para o alongamento em altura, importa de que lado os cabeçalhos estão localizados. Quando estão localizados à esquerda, sua ordem vai de baixo para cima e, quando estão localizados à esquerda, vão de cima para baixo. Portanto, temos dois métodos separados para o alongamento de altura para posicionar os cabeçalhos à esquerda e à direita.

Método que estica os cabeçalhos das guias para caber na largura do controle:

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

Aqui, primeiro devemos saber o número de fileiras de cabeçalho. Esse número pode ser encontrado obtendo o último da lista de cabeçalhos, porque ele conterá seu número de linha na propriedade Row. Como os números das linhas começam do zero, para indicar o número de linhas, precisamos adicionar um ao valor resultante.
Em seguida, precisamos obter uma lista dos cabeçalhos localizados em cada linha e esticar todos os cabeçalhos até a largura do objeto. Como adicionamos os valores Row e Column às propriedades do objeto, ficou bem simples obter uma lista de cabeçalhos de uma linha - filtramos a lista de os cabeçalhos segundo o valor da linha e obtemos uma lista contendo ponteiros para objetos com o número de linha especificado. Em um loop pela lista resultante, alteramos a largura de cada cabeçalho para o valor calculado anteriormente - a largura do contêiner dividida pelo número de cabeçalhos na linha. Da largura do contêiner, não tiramos sua largura total, mas removemos dois pixels à esquerda e à direita, para que os cabeçalhos extremos não ultrapassem o contêiner quando forem selecionados e aumentados de tamanho. Como estamos dividindo o tamanho por um valor desconhecido antecipadamente, para evitar a divisão por zero, verificamos o divisor por esse valor e, se for zero, dividimos por 1. Se não houver um cabeçalho anterior na lista (o índice do ciclo aponta para o primeiro cabeçalho), esse cabeçalho não precisa ser deslocado para nenhum lugar, isto é, ele permanece em seu lugar, enquanto todos os subsequentes precisam ser movidos para a borda direita do cabeçalho anterior, porque, afinal, todos os cabeçalhos mudaram de largura, uma vez que eles se tornaram maiores e se sobrepõem.

Método que estica os cabeçalhos das guias até a altura do controle quando posicionado à esquerda:

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

A lógica do método é semelhante à anterior, mas aqui é um pouco mais complicada. Como os cabeçalhos são posicionados à esquerda, começando na borda inferior de seu contêiner, e o ponto de ancoragem do cabeçalho está no canto superior esquerdo, redimensioná-lo fará com que a borda inferior do cabeçalho fique abaixo da borda inferior do contêiner. Portanto, aqui precisamos mover o primeiro cabeçalho para cima consoante o deslocamento calculado. Para fazer isso, antes de redimensionar o cabeçalho, lembramos sua altura e, após o redimensionamento, calculamos o tamanho do redimensionamento. Movemos o primeiro cabeçalho ao longo do eixo Y com base nessa quantidade para que sua borda inferior não ultrapasse seu contêiner.


Método que estica os cabeçalhos das guias até a altura do controle quando posicionado à direita:

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

O método é idêntico ao método que estica os cabeçalhos até a largura do contêiner, mas aqui nós os esticamos em altura. Como aqui os cabeçalhos estão à esquerda e sua contagem vai de cima para baixo, não precisamos ajustar a localização do primeiro cabeçalho depois de alterar seu tamanho, porque suas coordenadas iniciais coincidem com as coordenadas de seu ponto de posicionamento e o objeto irá aumentar para baixo sem ir além do contêiner.

O método que cria o número especificado de guias foi alterado porque precisamos calcular as coordenadas iniciais e os tamanhos com base na localização dos cabeçalhos. Para dispor os cabeçalhos à esquerda e à direita, atribuímos a largura do cabeçalho à altura e a altura do cabeçalho à largura passada para o método. Se o cabeçalho estiver à esquerda, giramos o texto do cabeçalho verticalmente em 90° e, se estiver à direita, em 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;
  }
//+------------------------------------------------------------------+

A lógica do método e as melhorias estão descritas nos comentários ao código, e as principais características são indicadas antes da listagem do método e marcadas com cores. Observe que, para posicionar os cabeçalhos à esquerda, também precisamos lembrar o tamanho do cabeçalho antes de alterá-lo e, em seguida, calculamos a quantidade de deslocamento e mover o cabeçalho redimensionado para o local correto.

O método que coloca cabeçalhos de guias no topo, que escrevemos anteriormente no último artigo, também sofreu alterações:

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

A lógica do método está totalmente descrita nos comentários ao código, vamos deixar para estudo independente.


Método que coloca cabeçalhos de guias na 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);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

O método é idêntico ao método que coloca cabeçalhos no topo. A única diferença está na direção de deslocamento das linhas de cabeçalho, pois elas estão na parte inferior e são deslocadas espelhadamente em relação ao método anterior.


Método que posiciona os cabeçalhos das guias à esquerda:

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

Aqui, os cabeçalhos estão localizados à esquerda, e as linhas são deslocadas ao longo do eixo X. Quanto ao resto, a lógica é idêntica aos métodos anteriores.


Método que posiciona os cabeçalhos das guias à direita:

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

Mesma lógica do método anterior, mas os deslocamentos de linha são espelhados porque os cabeçalhos estão à direita.

Todos os métodos acima são comentados em detalhes no código, logo vamos deixá-los para estudo independente. Em qualquer caso, todas as perguntas podem ser colocadas na discussão do artigo.

Agora podemos testar todas as mudanças e melhorias. Note que para posicionar os cabeçalhos das guias em uma linha, não temos funcionalidade suficiente para recortar a parte visível/invisível do elemento gráfico. Portanto, se houver muitas guias, se você selecionar o modo localização de cabeçalhos em uma linha (modo Multiline desativado), todos os cabeçalhos serão alinhados, indo além do controle. Vamos lidar com esse problema em artigos posteriores e, nos métodos das classes consideradas para esse modo, restam "stubs" - inseriremos o código para processar esse modo lá.

Teste

Para testar, vamos pegar o Expert Advisor do artigo anterior e salvá-lo na nova pasta \MQL5\Experts\TestDoEasy\Part116\ com o novo nome TestDoEasy116.mq5.

Vamos adicionar variáveis aos parâmetros de entrada do EA para especificar o modo Multiline e o lado onde os cabeçalhos das guias são colocados:

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


Vamos aumentar um pouco (em 10 pixels) a largura do painel criado:

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

e a largura do segundo contêiner GroupBox em 12 pixels:

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

Tudo isso é feito apenas porque não temos a funcionalidade de recortar a parte invisível dos elementos gráficos, e todos os objetos WinForm localizados em seus objetos pai e com tamanhos maiores que o contêiner (objeto pai) irão além de seus limites. Por exemplo, uma CheckBox colocada no campo da guia se estenderá além do campo da guia quando os cabeçalhos forem colocados à esquerda, ou também se estenderá além do campo da guia e cobrirá os cabeçalhos da guia colocados no lado direito do controle TabControl. Embora não haja funcionalidade suficiente, precisamos ocultar essas falhas 🙂.

No manipulador OnInit(), após criar o controle TabControl, definimos a localização dos cabeçalhos das guias e permitimos a localização dos cabeçalhos em várias linhas, especificadas nos parâmetros de entrada do EA:

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


Ao criar o controle ListBox na terceira guia do controle TabControl, definimos sua coordenada Y mais próxima do topo da guia:

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

Anteriormente, o objeto era posicionado na coordenada 12, o que faz com que ele se estenda além do campo da guia na parte inferior quando os cabeçalhos das guias estão dispostos em várias fileiras (porque o tamanho da guia diminui proporcionalmente ao número de fileiras de cabeçalhos).

Compilamos o Expert Advisor e o iniciamos no gráfico:


Como você pode ver, a localização dos cabeçalhos das guias à esquerda e à direita funciona corretamente. Há algumas falhas, que descreveremos no próximo artigo e corrigiremos, mas por enquanto tudo é satisfatório.


O que virá a seguir?

No próximo artigo, continuaremos trabalhando no controle TabControl.

Todos os arquivos da versão atual da biblioteca, os arquivos do EA de teste e o indicador de controle de eventos do gráfico para MQL5 estão anexados abaixo. Você pode baixá-los e testar tudo sozinho. Se você tiver dúvidas, comentários e sugestões, pode colocá-los nos comentários do artigo.

Voltar ao conteúdo

*Artigos desta série:

DoEasy. Controles (Parte 10): Objetos WinForms, dando vida à interface
DoEasy. Controles (Parte 11): Objetos WinForms - grupos, objeto WinForms CheckedListBox
DoEasy. Controles (Parte 12): Objeto base lista, objetos ListBox e ButtonListBox do WinForms
DoEasy. Controles (Parte 13): Otimizando a interação de objetos WinForms com o mouse, dando início ao desenvolvimento do objeto WinForms TabControl
DoEasy. Controles (Parte 14): Novo algoritmo para nomear elementos gráficos. Continuando o trabalho no objeto WinForms TabControl
DoEasy. Controles (Parte 15): Objeto WinForms TabControl - múltiplas fileiras de cabeçalhos de guias, métodos de manuseio de guias

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/11356

Arquivos anexados |
MQL5.zip (4435.36 KB)
Aprendendo a construindo um EA que opera de forma automática (Parte 07): Tipos de Contas (II) Aprendendo a construindo um EA que opera de forma automática (Parte 07): Tipos de Contas (II)
Aprenda como criar um EA que opera de forma automática, isto de forma simples e o mais seguro possível. É preciso sempre ficar atento, ao que um EA automatizado, esta fazendo, e se ele sair da linha, removê-lo o mais rápido possível do gráfico, encerrando o que ele estava fazendo, a fim de evitar que as coisas fugam do controle.
Como desenvolver um sistema de negociação baseado no indicador Bull's Power Como desenvolver um sistema de negociação baseado no indicador Bull's Power
Bem-vindo a um novo artigo em nossa série sobre como desenvolver um sistema de negociação com base nos indicadores técnicos mais populares, aqui está um novo artigo sobre como aprender a desenvolver um sistema de negociação pelo indicador técnico Bull's Power.
A matemática do mercado: lucro, prejuízo e custos A matemática do mercado: lucro, prejuízo e custos
Neste artigo, eu mostrarei como calcular o lucro ou prejuízo total de qualquer negociação, incluindo comissão e swap. Eu fornecerei o modelo matemático mais preciso e o usarei para escrever o código e compará-lo com o padrão. Além disso, eu também tentarei entrar na função principal da MQL5 para calcular o lucro e chegar ao fundo de todos os valores necessários da especificação.
Redes neurais de maneira fácil (Parte 27): Aprendizado Q profundo (DQN) Redes neurais de maneira fácil (Parte 27): Aprendizado Q profundo (DQN)
Continuamos nosso estudo sobre aprendizado por reforço. E, neste artigo, vamos nos familiarizar com o método de aprendizado Q profundo. Com esse método, a equipe do DeepMind criou um modelo que pode superar um humano ao jogar jogos do Atari. Acho que será útil avaliar as possibilidades de tal tecnologia para resolver problemas de negociação.