English Русский 中文 Español Deutsch 日本語
preview
DoEasy. Controles (Parte 18): Preparando a funcionalidade para rolagem de guias no TabControl

DoEasy. Controles (Parte 18): Preparando a funcionalidade para rolagem de guias no TabControl

MetaTrader 5Exemplos | 7 dezembro 2022, 13:17
182 0
Artyom Trishkin
Artyom Trishkin

Conteúdo


Ideia

Continuamos trabalhando no objeto WinForms TabControl. Se observarmos como este controle funciona no MS Visual Studio, vemos o seguinte comportamento: se uma fileira de cabeçalhos não cabe na largura do controle então, por exemplo, as bordas dos cabeçalhos são cortadas. Se selecionamos um cabeçalho que foi aparado na borda direita do controle, toda a linha de cabeçalhos se desloca para a esquerda para que o cabeçalho selecionado se ajuste dentro da largura do contêiner, ou seja, o primeiro cabeçalho previamente visível no lado esquerdo vai para além da borda esquerda do controle, e o cabeçalho posterior se tornar visível. Da mesma forma, a rolagem da fileira de cabeçalhos funciona quando clicamos em seus botões de deslocamento, que aparecem quando a fileira de cabeçalhos não cabe dentro do tamanho do controle.

Hoje faremos este comportamento também para nosso objeto WinForms TabControl. Bem, hoje não faremos nenhuma rolagem com botões, mas vamos deslocar a linha de cabeçalhos para a esquerda ao selecionar uma linha de cabeçalhos cortada do lado direito. E isso somente se os cabeçalhos forem colocados na parte superior ou inferior do controle. Este será um teste preliminar de tal funcionalidade e, no próximo artigo, vamos desenvolvê-lo para criar métodos completos de rolagem de todos os cabeçalhos em cada lado do controle, tanto ao selecionar o cabeçalho cortado quanto ao rolar a linha de cabeçalho com os botões de controle.

A propósito, hoje colocaremos estes botões nas posições corretas, caso precisemos deles. E eles serão necessários quando a fileira de cabeçalhos não couber dentro do controle. Neste caso, se os cabeçalhos são colocados na parte superior ou inferior, então os botões de controle de rolagem são colocados na parte superior ou inferior direita, respectivamente. Quando os cabeçalhos estão à esquerda, os botões de controle de rolagem estão na parte superior esquerda e quando os cabeçalhos estão à direita, os botões estão na parte inferior direita. Os controles de rolagem da fileira de cabeçalhos serão recuados um pixel em relação ao lugar onde o campo de guias é colocado, e os cabeçalhos das guias serão aparados até a borda desses botões (ao invés da borda do contêiner), também recuados um pixel. Assim, estes controles serão colocados de acordo com sua colocação no controle TabControl no MS Visual Studio.


Modificando as classes da biblioteca


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

//--- CDataPropObj
   MSG_DATA_PROP_OBJ_OUT_OF_PROP_RANGE,               // Passed property is out of object property range
   MSG_GRAPH_OBJ_FAILED_CREATE_NEW_HIST_OBJ,          // Failed to create an object of the graphical object change history
   MSG_GRAPH_OBJ_FAILED_ADD_OBJ_TO_HIST_LIST,         // Failed to add the change history object to the list
   MSG_GRAPH_OBJ_FAILED_GET_HIST_OBJ,                 // Failed to receive the change history object
   MSG_GRAPH_OBJ_FAILED_INC_ARRAY_SIZE,               // Failed to increase the array size

//--- CGraphElementsCollection
   MSG_GRAPH_OBJ_FAILED_GET_ADDED_OBJ_LIST,           // Failed to get the list of newly added objects
   MSG_GRAPH_OBJ_FAILED_GET_OBJECT_NAMES,             // Failed to get object names
   MSG_GRAPH_OBJ_FAILED_DETACH_OBJ_FROM_LIST,         // Failed to remove a graphical object from the list
   MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_LIST,         // Failed to remove a graphical object from the list
   MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART,        // Failed to remove a graphical object from the chart
   MSG_GRAPH_OBJ_FAILED_ADD_OBJ_TO_DEL_LIST,          // Failed to set a graphical object to the list of removed objects
   MSG_GRAPH_OBJ_FAILED_ADD_OBJ_TO_RNM_LIST,          // Failed to set a graphical object to the list of renamed objects

e o texto da mensagem correspondente ao índice recém-adicionado:

//--- CDataPropObj
   {"Переданное свойство находится за пределами диапазона свойств объекта","The passed property is outside the range of the object's properties"},
   {"Не удалось создать объект истории изменений графического объекта","Failed to create a graphical object change history object"},
   {"Не удалось добавить объект истории изменений в список","Failed to add change history object to the list"},
   {"Не удалось получить объект истории изменений","Failed to get change history object"},
   {"Не удалось увеличить размер массива","Failed to increase array size"},
   
//--- CGraphElementsCollection
   {"Не удалось получить список вновь добавленных объектов","Failed to get the list of newly added objects"},
   {"Не удалось получить имена объектов","Failed to get object names"},
   {"Не удалось изъять графический объект из списка","Failed to detach graphic object from the list"},
   {"Не удалось удалить графический объект из списка","Failed to delete graphic object from the list"},
   {"Не удалось удалить графический объект с графика","Failed to delete graphic object from the chart"},
   {"Не удалось поместить графический объект в список удалённых объектов","Failed to place graphic object in the list of deleted objects"},
   {"Не удалось поместить графический объект в список переименованных объектов","Failed to place graphic object in the list of renamed objects"},


Quando clicamos nos controles, podemos nem sempre ser capazes de processar este evento na mesma classe. Pode haver situações em que, ao termos esse evento, precisamos chamar a funcionalidade a partir de outras classes que não estão disponíveis na classe onde o manipulador de eventos do mouse está escrito para o objeto que foi clicado. Aqui vemos uma saída, e é que quando clicamos no controle, enviamos um evento para o gráfico do programa de controle, e a biblioteca processará este evento, enviando-o para a classe que contém a função para processar este evento.

Desta forma, serão realizados alguns eventos internos, mas, além das necessidades da biblioteca, ainda precisaremos enviar alguns eventos para o programa de controle, para que ele possa manuseá-los a partir daí. Portanto, precisamos usar o modelo de evento de qualquer forma, para que não tenhamos que realizar o manuseio dos elementos gráficos através de um temporizador.

No arquivo \MQL5\Include\DoEasy\Defines.mqh, vamos escrever uma enumeração dos possíveis eventos dos objetos WinForms da biblioteca:

//+------------------------------------------------------------------+
//| List of possible WinForms control events                         |
//+------------------------------------------------------------------+
enum ENUM_WF_CONTROL_EVENT
  {
   WF_CONTROL_EVENT_NO_EVENT = GRAPH_OBJ_EVENTS_NEXT_CODE,// No event
   WF_CONTROL_EVENT_CLICK,                            // "Click on the control" event
   WF_CONTROL_EVENT_TAB_SELECT,                       // "TabControl tab selection" event
  };
#define WF_CONTROL_EVENTS_NEXT_CODE (WF_CONTROL_EVENT_TAB_SELECT+1)  // The code of the next event after the last graphical element event code
//+------------------------------------------------------------------+
//| Mode of automatic interface element resizing                     |
//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_AUTO_SIZE_MODE
  {
   CANV_ELEMENT_AUTO_SIZE_MODE_GROW,                  // Increase only
   CANV_ELEMENT_AUTO_SIZE_MODE_GROW_SHRINK,           // Increase and decrease
  };
//+------------------------------------------------------------------+

Até agora, existem apenas dois eventos na enumeração: um clique no controle e um evento de seleção de guia no TabControl, que hoje usaremos para manipular o clique no cabeçalho da guia recortado para gerar a rolagem da fileira de cabeçalhos das guias.

Anteriormente, criamos controles auxiliares que não são objetos WinForms independentes, mas são usados para criar outros controles. Todos eles foram colocados na pasta compartilhada de objetos WinForms. Como há cada vez mais, vamos criar uma pasta "Helpers" separada no caminho \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ e transferir todos os arquivos de objetos WinForms auxiliares, como ArrowButton. mqh, ArrowDownButton.mqh, ArrowLeftButton.mqh, ArrowLeftRightBox.mqh, ArrowRightButton.mqh, ArrowUpButton.mqh, ArrowUpDownBox.mqh, ListBoxItem.mqh, TabField.mqh e TabHeader.mqh.

Como agora os arquivos de objetos auxiliares estão localizados em um novo caminho, precisamos corrigir as linhas dos caminhos dos arquivos de inclusão em alguns arquivos da biblioteca.

No arquivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ListBoxItem.mqh iremos corrigir o caminho:

//+------------------------------------------------------------------+
//|                                                  ListBoxItem.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "..\Common Controls\Button.mqh"
//+------------------------------------------------------------------+
//| Label object class of WForms controls                            |
//+------------------------------------------------------------------+


No arquivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ArrowButton.mqh:

//+------------------------------------------------------------------+
//|                                                  ArrowButton.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "..\Common Controls\Button.mqh"
//+------------------------------------------------------------------+
//| Arrow Button object class of WForms controls                     |
//+------------------------------------------------------------------+


No arquivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ArrowLeftRightBox.mqh:

//+------------------------------------------------------------------+
//|                                            ArrowLeftRightBox.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "..\Containers\Panel.mqh"
//+------------------------------------------------------------------+
//| ArrowLeftRightBox object class of WForms controls                |
//+------------------------------------------------------------------+


No arquivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ArrowUpDownBox.mqh:

//+------------------------------------------------------------------+
//|                                               ArrowUpDownBox.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "..\Containers\Panel.mqh"
//+------------------------------------------------------------------+
//| ArrowUpDownBox object class of the WForms controls               |
//+------------------------------------------------------------------+


No arquivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\ElementsListBox.mqh:

//+------------------------------------------------------------------+
//|                                              ElementsListBox.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4 
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "..\Containers\Container.mqh"
#include "..\Helpers\ListBoxItem.mqh"
//+------------------------------------------------------------------+
//| Class of the base object of the WForms control list              |
//+------------------------------------------------------------------+


No arquivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Panel.mqh, corrigiremos as strings de inclusão dos arquivos agora localizados na nova pasta:

//+------------------------------------------------------------------+
//|                                                        Panel.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "Container.mqh"
#include "..\Helpers\TabField.mqh"
#include "..\Helpers\ArrowButton.mqh"
#include "..\Helpers\ArrowUpButton.mqh"
#include "..\Helpers\ArrowDownButton.mqh"
#include "..\Helpers\ArrowLeftButton.mqh"
#include "..\Helpers\ArrowRightButton.mqh"
#include "..\Helpers\ArrowUpDownBox.mqh"
#include "..\Helpers\ArrowLeftRightBox.mqh"
#include "GroupBox.mqh"
#include "TabControl.mqh"
#include "..\..\WForms\Common Controls\ListBox.mqh"
#include "..\..\WForms\Common Controls\CheckedListBox.mqh"
#include "..\..\WForms\Common Controls\ButtonListBox.mqh"
//+------------------------------------------------------------------+
//| Panel object class of WForms controls                            |
//+------------------------------------------------------------------+

e alteramos o método CreateNewGObject(), basta colocar todas as linhas em seus case em uma linha, o que tornará o método menor e mais legível:

//+------------------------------------------------------------------+
//| Create a new graphical object                                    |
//+------------------------------------------------------------------+
CGCnvElement *CPanel::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                       const int obj_num,
                                       const string descript,
                                       const int x,
                                       const int y,
                                       const int w,
                                       const int h,
                                       const color colour,
                                       const uchar opacity,
                                       const bool movable,
                                       const bool activity)
  {
   CGCnvElement *element=NULL;
   switch(type)
     {
      case GRAPH_ELEMENT_TYPE_ELEMENT                 : element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),descript,x,y,w,h,colour,opacity,movable,activity); break;
      case GRAPH_ELEMENT_TYPE_FORM                    : element=new CForm(this.ChartID(),this.SubWindow(),descript,x,y,w,h);              break;
      case GRAPH_ELEMENT_TYPE_WF_CONTAINER            : element=new CContainer(this.ChartID(),this.SubWindow(),descript,x,y,w,h);         break;
      case GRAPH_ELEMENT_TYPE_WF_GROUPBOX             : element=new CGroupBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);          break;
      case GRAPH_ELEMENT_TYPE_WF_PANEL                : element=new CPanel(this.ChartID(),this.SubWindow(),descript,x,y,w,h);             break;
      case GRAPH_ELEMENT_TYPE_WF_LABEL                : element=new CLabel(this.ChartID(),this.SubWindow(),descript,x,y,w,h);             break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKBOX             : element=new CCheckBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);          break;
      case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON          : element=new CRadioButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);       break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON               : element=new CButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);            break;
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX             : element=new CListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);           break;
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM        : element=new CListBoxItem(this.ChartID(),this.SubWindow(),descript,x,y,w,h);       break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX     : element=new CCheckedListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);    break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX      : element=new CButtonListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);     break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER           : element=new CTabHeader(this.ChartID(),this.SubWindow(),descript,x,y,w,h);         break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD            : element=new CTabField(this.ChartID(),this.SubWindow(),descript,x,y,w,h);          break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL          : element=new CTabControl(this.ChartID(),this.SubWindow(),descript,x,y,w,h);        break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON         : element=new CArrowButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);       break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP      : element=new CArrowUpButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);     break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN    : element=new CArrowDownButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);   break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT    : element=new CArrowLeftButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);   break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT   : element=new CArrowRightButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);  break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX : element=new CArrowUpDownBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);    break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX : element=new CArrowLeftRightBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      default  : break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type));
   return element;
  }
//+------------------------------------------------------------------+

Anteriormente, o método era assim:

//+------------------------------------------------------------------+
//| Create a new graphical object                                    |
//+------------------------------------------------------------------+
CGCnvElement *CPanel::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                       const int obj_num,
                                       const string descript,
                                       const int x,
                                       const int y,
                                       const int w,
                                       const int h,
                                       const color colour,
                                       const uchar opacity,
                                       const bool movable,
                                       const bool activity)
  {
   CGCnvElement *element=NULL;
   switch(type)
     {
      case GRAPH_ELEMENT_TYPE_ELEMENT :
         element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),descript,x,y,w,h,colour,opacity,movable,activity);
        break;
      case GRAPH_ELEMENT_TYPE_FORM :
         element=new CForm(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_CONTAINER            :
         element=new CContainer(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_GROUPBOX             :
         element=new CGroupBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_PANEL                :
         element=new CPanel(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_LABEL                :
         element=new CLabel(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKBOX             :
         element=new CCheckBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON          :
         element=new CRadioButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON               :
         element=new CButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX             :
         element=new CListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM        :
         element=new CListBoxItem(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX     :
         element=new CCheckedListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX      :
         element=new CButtonListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER           :
         element=new CTabHeader(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD            :
         element=new CTabField(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL          :
         element=new CTabControl(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON         :
         element=new CArrowButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP      :
         element=new CArrowUpButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN    :
         element=new CArrowDownButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT    :
         element=new CArrowLeftButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT   :
         element=new CArrowRightButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX :
         element=new CArrowUpDownBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX :
         element=new CArrowLeftRightBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      default:
        break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type));
   return element;
  }
//+------------------------------------------------------------------+

... e tal formatação nos impediu de olhar todo o método de uma só vez, o que é menos útil para comparar os mesmos métodos em outras classes, nas quais iremos mudá-lo exatamente da mesma maneira para tornar a comparação mais clara. Para que serve tudo isso? Vejo que este método é o mesmo em diferentes classes-contêineres e, consequentemente, precisamos elaborá-lo de modo que seja único para todas as classes onde existe e todas as classes de objetos criados nele sejam acessíveis a partir de sua localização. Em artigos futuros, vamos pensar sobre este ponto.


No arquivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\TabField.mqh, corrigiremos a linha de inclusão do arquivo da classe do objeto-painel:

//+------------------------------------------------------------------+
//|                                                     TabField.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "..\Containers\Panel.mqh"
//+------------------------------------------------------------------+
//| TabHeader object class of WForms TabControl                      |
//+------------------------------------------------------------------+

e alteramos a formatação do método CreateNewGObject():

//+------------------------------------------------------------------+
//| Create a new graphical object                                    |
//+------------------------------------------------------------------+
CGCnvElement *CTabField::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                          const int obj_num,
                                          const string descript,
                                          const int x,
                                          const int y,
                                          const int w,
                                          const int h,
                                          const color colour,
                                          const uchar opacity,
                                          const bool movable,
                                          const bool activity)
  {
   CGCnvElement *element=NULL;
   switch(type)
     {
      case GRAPH_ELEMENT_TYPE_ELEMENT                 : element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),descript,x,y,w,h,colour,opacity,movable,activity); break;
      case GRAPH_ELEMENT_TYPE_FORM                    : element=new CForm(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_CONTAINER            : element=new CContainer(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_GROUPBOX             : element=new CGroupBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_PANEL                : element=new CPanel(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_LABEL                : element=new CLabel(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKBOX             : element=new CCheckBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON          : element=new CRadioButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON               : element=new CButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX             : element=new CListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM        : element=new CListBoxItem(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX     : element=new CCheckedListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX      : element=new CButtonListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER           : element=new CTabHeader(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD            : element=new CTabField(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL          : element=new CTabControl(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON         : element=new CArrowButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP      : element=new CArrowUpButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN    : element=new CArrowDownButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT    : element=new CArrowLeftButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT   : element=new CArrowRightButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX : element=new CArrowUpDownBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX : element=new CArrowLeftRightBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      default  : break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type));
   return element;
  }
//+------------------------------------------------------------------+


Vamos também alterar a formatação no arquivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\GroupBox.mqh:

//+------------------------------------------------------------------+
//| Create a new graphical object                                    |
//+------------------------------------------------------------------+
CGCnvElement *CGroupBox::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                          const int obj_num,
                                          const string descript,
                                          const int x,
                                          const int y,
                                          const int w,
                                          const int h,
                                          const color colour,
                                          const uchar opacity,
                                          const bool movable,
                                          const bool activity)
  {
   CGCnvElement *element=NULL;
   switch(type)
     {
      case GRAPH_ELEMENT_TYPE_ELEMENT                 : element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),descript,x,y,w,h,colour,opacity,movable,activity); break;
      case GRAPH_ELEMENT_TYPE_FORM                    : element=new CForm(this.ChartID(),this.SubWindow(),descript,x,y,w,h);              break;
      case GRAPH_ELEMENT_TYPE_WF_CONTAINER            : element=new CContainer(this.ChartID(),this.SubWindow(),descript,x,y,w,h);         break;
      case GRAPH_ELEMENT_TYPE_WF_GROUPBOX             : element=new CGroupBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);          break;
      case GRAPH_ELEMENT_TYPE_WF_PANEL                : element=new CPanel(this.ChartID(),this.SubWindow(),descript,x,y,w,h);             break;
      case GRAPH_ELEMENT_TYPE_WF_LABEL                : element=new CLabel(this.ChartID(),this.SubWindow(),descript,x,y,w,h);             break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKBOX             : element=new CCheckBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);          break;
      case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON          : element=new CRadioButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);       break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON               : element=new CButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);            break;
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX             : element=new CListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);           break;
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM        : element=new CListBoxItem(this.ChartID(),this.SubWindow(),descript,x,y,w,h);       break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX     : element=new CCheckedListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);    break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX      : element=new CButtonListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);     break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER           : element=new CTabHeader(this.ChartID(),this.SubWindow(),descript,x,y,w,h);         break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD            : element=new CTabField(this.ChartID(),this.SubWindow(),descript,x,y,w,h);          break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL          : element=new CTabControl(this.ChartID(),this.SubWindow(),descript,x,y,w,h);        break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON         : element=new CArrowButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);       break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP      : element=new CArrowUpButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);     break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN    : element=new CArrowDownButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);   break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT    : element=new CArrowLeftButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);   break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT   : element=new CArrowRightButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);  break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX : element=new CArrowUpDownBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);    break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX : element=new CArrowLeftRightBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      default  : break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type));
   return element;
  }
//+------------------------------------------------------------------+

Após essas alterações em todos os arquivos de classes-contêineres, os métodos se tornaram mais legíveis e agora todos podem ser comparados entre si rapidamente. Eles são idênticos. Isto significa que um único método ou função comum teria que ser criado e chamado nestes métodos de todas as classes-contêineres.


Quando clicamos em um cabeçalho de guia que não esteja completamente fora do contêiner e parcialmente cortado, precisaremos deslocar a linha de cabeçalhos para que este cabeçalho fique totalmente visível. A linha de cabeçalhos é deslocada na classe de controle TabControl, que não é visível na classe do objeto do cabeçalho da guia, porque as guias estão localizadas dentro do objeto TabControl. Portanto, precisaremos enviar uma mensagem de evento que registre o número do cabeçalho que foi clicado e os nomes dos objetos dentro dos quais o evento ocorreu. Os nomes devem ser tanto o objeto TabControl quanto o objeto ao qual ele está anexado. Estes serão os nomes do objeto principal e do objeto TabControl. No caso do manipulador da biblioteca, conhecendo estes nomes, poderemos identificar de forma única o objeto principal ao qual todos os outros estão anexados, e entre eles está TabControl, cujo nome também saberemos, e selecioná-lo caso haja vários objetos TabControl anexados ao objeto principal.

Todos os objetos do mesmo tipo são agora construídos na biblioteca com nomes diferentes, independentemente do gráfico em que são criados. Portanto, só precisamos saber o nome do objeto para encontrá-lo. Todos os métodos para selecionar um objeto pelo nome, agora disponíveis na biblioteca, também requerem o identificador do gráfico no qual eles são criados. Como o identificador do gráfico não é necessário para a seleção única de um objeto por seu nome (a propósito, isto é verdade apenas para esta biblioteca, pois o terminal do cliente permite criar objetos com o mesmo nome mas em gráficos diferentes), mas não temos tal método, vamos criá-lo.

No arquivo \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh, vamos declarar um método que retorna um elemento gráfico pelo seu nome:

public:
//--- Draw a frame
   virtual void      DrawFrame(void){}
//--- Return by type the (1) list, (2) the number of bound controls, the bound control (3) by index in the list, (4) by name
   CArrayObj        *GetListElementsByType(const ENUM_GRAPH_ELEMENT_TYPE type);
   int               ElementsTotalByType(const ENUM_GRAPH_ELEMENT_TYPE type);
   CGCnvElement     *GetElementByType(const ENUM_GRAPH_ELEMENT_TYPE type,const int index);
   CGCnvElement     *GetElementByName(const string name);
//--- Clear the element filling it with color and opacity

e fora do corpo da classe escreveremos sua implementação:

//+------------------------------------------------------------------+
//| Return the bound element by name                                 |
//+------------------------------------------------------------------+
CGCnvElement *CWinFormBase::GetElementByName(const string name)
  {
   string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
   CArrayObj *list=CSelect::ByGraphCanvElementProperty(this.GetListElements(),CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
   return(list!=NULL ? list.At(0) : NULL);
  }
//+------------------------------------------------------------------+

Aqui, primeiro verificamos que o nome do objeto que estamos procurando contém o nome do programa, e se não estiver no nome passado ao método, adicionamos o nome do programa ao nome do objeto que estamos procurando. Em seguida, obtemos uma lista dos elementos gráficos anexados ao controle que têm o nome que estamos procurando (deve haver apenas um desses objetos, se for encontrado). Como resultado, devolvemos um ponteiro para o único objeto da lista resultante. Se a lista estiver vazia ou não criada, o método retornará NULL.

Na classe do objeto-cabeçalho da guia, precisaremos saber o tamanho dos controles criados para rolagem da linha de cabeçalhos na classe do objeto TabControl. Precisamos disto para aparar corretamente os cabeçalhos, caso os controles sejam visíveis. Novamente, os objetos-cabeçalhos não sabem nada sobre os objetos colocados no objeto de classe a partir do qual eles mesmos foram criados. Mas podemos passar todos os tamanhos necessários desse objeto para os objetos-cabeçalhos, e então os cabeçalhos saberão sobre "alguns" tamanhos, que precisam ser levados em consideração ao cortar a área invisível, além disso, para entender quando esses tamanhos precisam ser considerados e quando não precisam, precisamos enviar aos objetos-cabeçalhos o sinalizador de visibilidade dos controles de rolagem de linhas de cabeçalhos.

Assim, os cabeçalhos não sabem nada sobre os objetos, mas haverá variáveis dentro das classes de objetos-cabeçalhos, que irão armazenar todas as informações necessárias. E escreveremos estas informações no objeto de cabeçalho da classe do objeto TabControl. Desta forma, imitamos a visibilidade dos parâmetros exigidos no objeto, que nada sabe sobre o objeto, mas usa seus parâmetros para seu trabalho.

No arquivo de classe do objeto-cabeçalho da guia \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\TabHeader.mqh, vamos corrigir a string de acesso ao arquivo e declarar variáveis para armazenar as propriedades dos controles de rolagem da linha de cabeçalho:

//+------------------------------------------------------------------+
//|                                                    TabHeader.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "..\Common Controls\Button.mqh"
//+------------------------------------------------------------------+
//| 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
   bool              m_arr_butt_ud_visible_flag;         // Tab header "up-down" control buttons visibility flag 
   bool              m_arr_butt_lr_visible_flag;         // Tab header "left-right" control buttons visibility flag
   int               m_arr_butt_ud_size;                 // Tab header "up-down" control buttons size 
   int               m_arr_butt_lr_size;                 // Tab header "left-right" control buttons size
//--- Adjust the size and location of the element depending on the state


Na seção pública da classe, escreveremos métodos para trabalhar com variáveis privadas declaradas:

public:
//--- Return the visibility of the (1) left-right, (2) up-down buttons
   bool              IsVisibleLeftRightBox(void)                                 const { return this.m_arr_butt_lr_visible_flag; }
   bool              IsVisibleUpDownBox(void)                                    const { return this.m_arr_butt_ud_visible_flag; }
//--- Set the visibility of the (1) left-right, (2) up-down buttons
   void              SetVisibleLeftRightBox(const bool flag)                           { this.m_arr_butt_lr_visible_flag=flag;   }
   void              SetVisibleUpDownBox(const bool flag)                              { this.m_arr_butt_ud_visible_flag=flag;   }
//--- Set the size of the (1) left-right, (2) up-down buttons
   void              SetSizeLeftRightBox(const int value)                              { this.m_arr_butt_lr_size=value;          }
   void              SetSizeUpDownBox(const int value)                                 { this.m_arr_butt_ud_size=value;          }
//--- Find and return a pointer to the field object corresponding to the tab index

Os dados reais para todas essas variáveis serão enviados na classe de objeto TabControl, e aqui será usado para calcular o tamanho da área visível e cortar a parte da imagem que vai além da área visível.

Para que possamos usar os valores das novas variáveis nos cálculos da área visível, precisamos sobrescrever aqui o método virtual Crop() do objeto pai.

Vamos declarar o método no escopo público da classe:

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

//--- Crop the image outlined by the previously specified rectangular visibility scope
   virtual void      Crop(void);

//--- Last mouse event handler
   virtual void      OnMouseEventPostProcessing(void);

Vamos criá-lo fora do corpo da classe.

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

Aqui, ao contrário do método original, são adicionadas dimensões adicionais que devem ser levadas em consideração ao calcular a área de visibilidade. Se estiver escrito nas variáveis-sinalizadores que o objeto de controle de rolagem é visível, o tamanho do controle de rolagem visível deve ser usado para o cálculo. Se a variável disser que o objeto não está visível, usamos zero em vez de seu tamanho. Os botões de seta para a esquerda-direita e os botões de seta para cima e para baixo usam suas próprias variáveis e, consequentemente, cálculos separados de tamanhos adicionais para calcular a área de visibilidade.

Nos construtores de classe, inicializamos novas variáveis com valores padrão:

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


No manipulador de eventos "Cursor dentro da área ativa, o botão do mouse solto (esquerdo)", escreveremos um bloco de código para criar e enviar o evento de seleção de guia do controle TabControl quando o cabeçalho for clicado:

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

Aqui criamos o evento a ser enviado ao manipulador de eventos da biblioteca e nesta mensagem especificamos o número da linha do cabeçalho no parâmetro long da mensagem (para uma linha será sempre zero), o número da coluna do cabeçalho na fileira, isto é, no parâmetro duplo, que indicará exatamente qual cabeçalho foi clicado (número da guia selecionada). Para identificar de forma única o TabControl no qual a guia foi selecionada, precisamos enviar no evento o nome do objeto principal no qual o evento ocorreu e o nome do objeto TabControl no qual a guia foi selecionada. Como temos apenas um parâmetro de string, simplesmente adicionamos os nomes do objeto principal e do objeto TabControl separados por ";". No manipulador de eventos, podemos então usar o separador para cortar a string e obter os dois nomes desses objetos.


Modificamos a classe do objeto WinForms TabControl no arquivo \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\TabControl .mqh.

Corrigimos os caminhos para os arquivos de objetos auxiliares, declaramos novas variáveis e métodos e escrevemos métodos que retornam ponteiros para objetos-botões com setas:

//+------------------------------------------------------------------+
//|                                                   TabControl.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "Container.mqh"
#include "GroupBox.mqh"
#include "..\Helpers\TabHeader.mqh"
#include "..\Helpers\TabField.mqh"
//+------------------------------------------------------------------+
//| TabHeader object class of WForms TabControl                      |
//+------------------------------------------------------------------+
class CTabControl : public CContainer
  {
private:
   int               m_item_width;                    // Fixed width of tab titles
   int               m_item_height;                   // Fixed height of tab titles
   int               m_header_padding_x;              // Additional header width if DrawMode==Fixed
   int               m_header_padding_y;              // Additional header height if DrawMode==Fixed
   int               m_field_padding_top;             // Padding of top tab fields
   int               m_field_padding_bottom;          // Padding of bottom tab fields
   int               m_field_padding_left;            // Padding of left tab fields
   int               m_field_padding_right;           // Padding of right tab fields
   bool              m_arr_butt_ud_visible_flag;      // Tab header "up-down" control buttons visibility flag 
   bool              m_arr_butt_lr_visible_flag;      // Tab header "left-right" control buttons visibility flag
//--- (1) Hide and (2) display right-left and up-down button controls
   void              ShowArrLeftRightBox(void);
   void              ShowArrUpDownBox(void);
   void              HideArrLeftRightBox(void);
   void              HideArrUpDownBox(void);
//--- Move right-left and up-down button controls to the foreground
   void              BringToTopArrLeftRightBox(void);
   void              BringToTopArrUpDownBox(void);
//--- Create a new graphical object
   virtual CGCnvElement *CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                          const int element_num,
                                          const string descript,
                                          const int x,
                                          const int y,
                                          const int w,
                                          const int h,
                                          const color colour,
                                          const uchar opacity,
                                          const bool movable,
                                          const bool activity);

//--- Return the list of (1) headers, (2) tab fields, the pointer to the (3) up-down and (4) left-right button objects
   CArrayObj        *GetListHeaders(void)          { return this.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);        }
   CArrayObj        *GetListFields(void)           { return this.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD);         }
   CArrowUpDownBox  *GetArrUpDownBox(void)         { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX,0); }
   CArrowLeftRightBox *GetArrLeftRightBox(void)    { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX,0); }
//--- Set the tab as selected


Na seção pública da classe, escreveremos métodos para trabalhar com novas variáveis e objetos-botões com setas e declararemos um método para deslocar a linha de cabeçalhos e um manipulador de eventos:

//--- Return a pointer to the (1) tab header, (2) field, (3) the number of tabs, visibility of (4) left-right and (5) up-down buttons
   CTabHeader       *GetTabHeader(const int index)       { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,index);    }
   CWinFormBase     *GetTabField(const int index)        { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,index);     }
   int               TabPages(void)                      { return(this.GetListHeaders()!=NULL ? this.GetListHeaders().Total() : 0); }
   bool              IsVisibleLeftRightBox(void)         { return this.m_arr_butt_lr_visible_flag;                                  }
   bool              IsVisibleUpDownBox(void)            { return this.m_arr_butt_ud_visible_flag;                                  }
//--- Set the visibility of the (1) left-right, (2) up-down buttons
   void              SetVisibleLeftRightBox(const bool flag);
   void              SetVisibleUpDownBox(const bool flag);
//--- Set the size of the (1) left-right, (2) up-down buttons
   void              SetSizeLeftRightBox(const int value);
   void              SetSizeUpDownBox(const int value);
//--- (1) Set and (2) return the location of tabs on the control

...

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

//--- Constructor
                     CTabControl(const long chart_id,
                                 const int subwindow,
                                 const string descript,
                                 const int x,
                                 const int y,
                                 const int w,
                                 const int h);
  };
//+------------------------------------------------------------------+


No construtor da classe, definimos os valores de visibilidade padrão para objetos-setas:

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

Por padrão, estes dois objetos devem ser escondidos e só serão exibidos se a linha de cabeçalhos horizontal ou vertical se estender além do contêiner. Com os mesmos sinalizadores, determinaremos em objetos de cabeçalho de tabulação se valores adicionais devem ser adicionados à área de visibilidade. Se não houver nenhum objeto-seta, os cabeçalhos serão aparados na borda do contêiner e, se houver, na borda deste objeto - para que o objeto-botão de seta não se sobreponha ao cabeçalho aparado da borda do contêiner.


No método que cria o número especificado de guias, ajustamos a coordenada Y da colocação do cabeçalho e a altura dos campos das guias ao colocá-las na parte inferior do contêiner e adicionamos um bloco de código para criar dois objetos-botões esquerdo-direito e para cima e para baixo:

//+------------------------------------------------------------------+
//| 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=2;
      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=2;
           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-2;
           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-2;
           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);
      //--- Get the Y offset of the header position after changing its height and
      //--- 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());
        }
      header.SetVisibleFlag(this.IsVisible(),false);

      //--- 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()-2;
      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()-2;
           break;
         case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
           field_x=0;
           field_y=0;
           field_w=this.Width();
           field_h=this.Height()-header.Height()-2;
           break;
         case CANV_ELEMENT_ALIGNMENT_LEFT    :
           field_x=header.RightEdgeRelative();
           field_y=0;
           field_h=this.Height();
           field_w=this.Width()-header.Width()-2;
           break;
         case CANV_ELEMENT_ALIGNMENT_RIGHT   :
           field_x=0;
           field_y=0;
           field_h=this.Height();
           field_w=this.Width()-header.Width()-2;
           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();
     }
//--- Create left-right and up-down buttons
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX,this.Width()-32,0,15,15,clrNONE,255,this.Active(),false);
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX,0,this.Height()-32,15,15,clrNONE,255,this.Active(),false);
   CArrowUpDownBox *box_ud=this.GetArrUpDownBox();
   if(box_ud!=NULL)
     {
      this.SetVisibleUpDownBox(false);
      this.SetSizeUpDownBox(box_ud.Height());
      box_ud.SetBorderStyle(FRAME_STYLE_NONE);
      box_ud.SetBackgroundColor(CLR_CANV_NULL,true);
      box_ud.SetOpacity(0);
      box_ud.Hide();
     }
   CArrowLeftRightBox *box_lr=this.GetArrLeftRightBox();
   if(box_lr!=NULL)
     {
      this.SetVisibleLeftRightBox(false);
      this.SetSizeLeftRightBox(box_lr.Width());
      box_lr.SetBorderStyle(FRAME_STYLE_NONE);
      box_lr.SetBackgroundColor(CLR_CANV_NULL,true);
      box_lr.SetOpacity(0);
      box_lr.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 coordenada Y do cabeçalho deve ser dois pixels maior que a borda inferior do contêiner quando colocada na parte inferior, pois o cabeçalho da guia selecionada aumenta em tamanho em dois pixels e, se for colocada na borda inferior do contêiner, sua borda inferior será cortada quando selecionada e, portanto, aumenta em altura em dois pixels.

Para evitar o corte, o objeto deve ser posicionado dois pixels mais alto - permitindo sua possível ampliação quando for selecionado. De forma correspondente, o campo da guia deve ser dois pixels menor porque esses dois pixels são agora "comidos" por um deslocamento para cima da coordenada do cabeçalho.

Após criar dois objetos-botões com setas, vamos defini-los como estado "oculto" (estes estados são passados para objetos de cabeçalho de guias) e definir as dimensões dos objetos criados em todos os objetos-cabeçalhos de guias. Tudo isso é feito por meio de dois métodos que descreveremos a seguir. Definimos o tipo de moldura como "nenhuma", definimos uma cor de fundo transparente e uma opacidade completa para os objetos. No final, o objeto está escondido.

Assim, ambos os objetos são criados como dois botões sobre um fundo transparente - para combinar com a aparência dos objetos correspondentes no controle no MS Visual Studio.

Em todos os métodos que colocam cabeçalhos das guias em cima, em baixo, à esquerda e à direita, vamos adicionar números de linhas e colunas de ajuste para o cabeçalho e um bloco de código para lidar com a situação quando tivermos os cabeçalhos de guias em uma linha e no caso de o cabeçalho da guia se estender além do contêiner:

//+------------------------------------------------------------------+
//| 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.Width()-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),
            //--- set the loop index for the n variable, while the column index is set to zero, this is the start of the new row
            row++;
            x_shift=header.CoordXRelative()-2;
            n=i;
            col=0;
           }
         //--- Assign the row and column indices to the tab header and shift it to the calculated coordinates
         header.SetTabLocation(row,col);
         if(header.Move(header.CoordX()-x_shift,header.CoordY()-header.Row()*header.Height()))
           {
            header.SetCoordXRelative(header.CoordX()-this.CoordX());
            header.SetCoordYRelative(header.CoordY()-this.CoordY());
           }
        }
      //--- If only one row of headers is allowed
      else
        {
         header.SetRow(0);
         header.SetColumn(i);
        }
     }

//--- 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;
            //--- shift the tab header by the calculated row coordinates
            if(header.Move(header.CoordX(),header.CoordY()+y_shift))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
            //--- shift the tab field by the calculated shift
            if(field.Move(field.CoordX(),field.CoordY()+y_shift))
              {
               field.SetCoordXRelative(field.CoordX()-this.CoordX());
               field.SetCoordYRelative(field.CoordY()-this.CoordY());
               //--- change the size of the shifted field by the value of its shift
               field.Resize(field.Width(),field.Height()-y_shift,false);
              }
           }
        }
      //--- If this is the first and only string
      else if(!this.Multiline())
        {
         //--- If the right edge of the header goes beyond the right edge of the container area,
         if(last.RightEdge()>this.RightEdgeWorkspace())
           {
            //--- get the button object with left-right arrows
            CArrowLeftRightBox *arr_box=this.GetArrLeftRightBox();
            if(arr_box!=NULL)
              {
               //--- Calculate object location coordinates
               int x=this.RightEdgeWorkspace()-arr_box.Width()+1;
               int y=last.BottomEdge()-arr_box.Height();
               //--- If the object is shifted by the specified coordinates,
               if(arr_box.Move(x,y))
                 {
                  //--- set its relative coordinates
                  arr_box.SetCoordXRelative(arr_box.CoordX()-this.CoordX());
                  arr_box.SetCoordYRelative(arr_box.CoordY()-this.CoordY());
                  //--- set the visibility flag for the object
                  this.SetVisibleLeftRightBox(true);
                  //--- If the control is visible, display the buttons and bring them to the foreground
                  if(this.IsVisible())
                    {
                     arr_box.Show();
                     arr_box.BringToTop();
                    }
                 }
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

O método que coloca os cabeçalhos das guias na parte superior é mostrado aqui em sua totalidade. Os outros três métodos fazem exatamente o mesmo trabalho de definir o número da linha (zero) e o número da coluna (índice de loop) para o cabeçalho da guia, e os blocos de código que definem o cabeçalho que «foge» do contêiner e exibe os botões de controle de rolagem para a linha são ligeiramente diferentes, apenas no cálculo das coordenadas dos cabeçalhos relativos ao contêiner e na exibição dos botões de controle de rolagem nas coordenadas correspondentes.

Vamos considerar estes blocos de código para os diferentes métodos.

Para o método que organiza os cabeçalhos das guias na parte inferior (ArrangeTabHeadersBottom()):

      //--- If this is the first and only string
      else if(!this.Multiline())
        {
         if(last.RightEdge()>this.RightEdgeWorkspace())
           {
            CArrowLeftRightBox *arr_box=this.GetArrLeftRightBox();
            if(arr_box!=NULL)
              {
               int x=this.RightEdgeWorkspace()-arr_box.Width()+1;
               int y=last.CoordY();
               if(arr_box.Move(x,y))
                 {
                  arr_box.SetCoordXRelative(arr_box.CoordX()-this.CoordX());
                  arr_box.SetCoordYRelative(arr_box.CoordY()-this.CoordY());
                  this.SetVisibleLeftRightBox(true);
                  if(this.IsVisible())
                    {
                     arr_box.Show();
                     arr_box.BringToTop();
                    }
                 }
              }
           }
        }


Para o método que organiza os cabeçalhos das guias à esquerda (ArrangeTabHeadersLeft()):

      //--- If this is the first and only string
      else if(!this.Multiline())
        {
         if(last.CoordY()<this.CoordY())
           {
            CArrowUpDownBox *arr_box=this.GetArrUpDownBox();
            if(arr_box!=NULL)
              {
               int x=last.RightEdge()-arr_box.Width();
               int y=this.CoordY()-1;
               if(arr_box.Move(x,y))
                 {
                  arr_box.SetCoordXRelative(arr_box.CoordX()-this.CoordX());
                  arr_box.SetCoordYRelative(arr_box.CoordY()-this.CoordY());
                  this.SetVisibleUpDownBox(true);
                  if(this.IsVisible())
                    {
                     arr_box.Show();
                     arr_box.BringToTop();
                    }
                 }
              }
           }
        }


Para o método que organiza os cabeçalhos das guias à direita (ArrangeTabHeadersRight()):

      //--- If this is the first and only string
      else if(!this.Multiline())
        {
         if(last.BottomEdge()>this.BottomEdge())
           {
            CArrowUpDownBox *arr_box=this.GetArrUpDownBox();
            if(arr_box!=NULL)
              {
               int x=last.CoordX();
               int y=this.BottomEdgeWorkspace()-arr_box.Height()+1;
               if(arr_box.Move(x,y))
                 {
                  arr_box.SetCoordXRelative(arr_box.CoordX()-this.CoordX());
                  arr_box.SetCoordYRelative(arr_box.CoordY()-this.CoordY());
                  this.SetVisibleUpDownBox(true);
                  if(this.IsVisible())
                    {
                     arr_box.Show();
                     arr_box.BringToTop();
                    }
                 }
              }
           }
        }

Comparando todos os quatro blocos de código em todos os quatro métodos, você pode ver como as coordenadas dos botões de controle de rolagem são calculadas e os cabeçalhos que se estendem além do contêiner são determinados.

O método que cria o novo objeto gráfico também sofreu alterações quanto à formatação dos case da instrução switch:

//+------------------------------------------------------------------+
//| Create a new graphical object                                    |
//+------------------------------------------------------------------+
CGCnvElement *CTabControl::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                         const int obj_num,
                                         const string descript,
                                         const int x,
                                         const int y,
                                         const int w,
                                         const int h,
                                         const color colour,
                                         const uchar opacity,
                                         const bool movable,
                                         const bool activity)
  {
   CGCnvElement *element=NULL;
   switch(type)
     {
      case GRAPH_ELEMENT_TYPE_ELEMENT                 : element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),descript,x,y,w,h,colour,opacity,movable,activity); break;
      case GRAPH_ELEMENT_TYPE_FORM                    : element=new CForm(this.ChartID(),this.SubWindow(),descript,x,y,w,h);              break;
      case GRAPH_ELEMENT_TYPE_WF_CONTAINER            : element=new CContainer(this.ChartID(),this.SubWindow(),descript,x,y,w,h);         break;
      case GRAPH_ELEMENT_TYPE_WF_GROUPBOX             : element=new CGroupBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);          break;
      case GRAPH_ELEMENT_TYPE_WF_PANEL                : element=new CPanel(this.ChartID(),this.SubWindow(),descript,x,y,w,h);             break;
      case GRAPH_ELEMENT_TYPE_WF_LABEL                : element=new CLabel(this.ChartID(),this.SubWindow(),descript,x,y,w,h);             break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKBOX             : element=new CCheckBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);          break;
      case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON          : element=new CRadioButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);       break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON               : element=new CButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);            break;
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX             : element=new CListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);           break;
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM        : element=new CListBoxItem(this.ChartID(),this.SubWindow(),descript,x,y,w,h);       break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX     : element=new CCheckedListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);    break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX      : element=new CButtonListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);     break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER           : element=new CTabHeader(this.ChartID(),this.SubWindow(),descript,x,y,w,h);         break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD            : element=new CTabField(this.ChartID(),this.SubWindow(),descript,x,y,w,h);          break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL          : element=new CTabControl(this.ChartID(),this.SubWindow(),descript,x,y,w,h);        break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON         : element=new CArrowButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);       break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP      : element=new CArrowUpButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);     break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN    : element=new CArrowDownButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);   break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT    : element=new CArrowLeftButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);   break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT   : element=new CArrowRightButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);  break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX : element=new CArrowUpDownBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);    break;
      case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX : element=new CArrowLeftRightBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); break;
      default  : break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type));
   return element;
  }
//+------------------------------------------------------------------+

Agora tudo está em uma linha - para uma exibição mais compacta da lógica do método.

No método virtual que define o objeto acima de tudo, substituímos a chamada para o método BringToTop() do objeto-forma

//+------------------------------------------------------------------+
//| Set the object above all the rest                                |
//+------------------------------------------------------------------+
void CTabControl::BringToTop(void)
  {
//--- Move all elements of the object to the foreground
   CForm::BringToTop();
//--- Get the index of the selected tab

pela chamada do método Show() do objeto-gráfico do elemento e, se o próprio objeto estiver visível, exibimos os objetos que controlam a rolagem das linhas do cabeçalho, se esses objetos estiverem visíveis:

//+------------------------------------------------------------------+
//| Set the object above all the rest                                |
//+------------------------------------------------------------------+
void CTabControl::BringToTop(void)
  {
//--- Move all elements of the object to the foreground
   CGCnvElement::Show();
//--- Get the index of the selected tab
   int selected=this.SelectedTabPageNum();
//--- Declare the pointers to tab header objects and tab fields
   CTabHeader *header=NULL;
   CTabField  *field=NULL;
//--- Get the list of all tab headers
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- In a loop by the list of tab headers,
   for(int i=0;i<list.Total();i++)
     {
      //--- get the next header, and if failed to get the object,
      //--- or this is the header of the selected tab, skip it
      header=list.At(i);
      if(header==NULL || header.PageNumber()==selected)
         continue;
      //--- bring the header to the foreground
      header.BringToTop();
      //--- get the tab field corresponding to the current header
      field=header.GetFieldObj();
      if(field==NULL)
         continue;
      //--- Hide the tab field
      field.Hide();
     }
//--- Get the pointer to the title of the selected tab
   header=this.GetTabHeader(selected);
   if(header!=NULL)
     {
      //--- bring the header to the front
      header.BringToTop();
      //--- get the tab field corresponding to the selected tab header
      field=header.GetFieldObj();
      //--- Display the tab field on the foreground
      if(field!=NULL)
         field.BringToTop();
     }
//--- If the object is visible and the "up-down" and "left-right" buttons should be visible, move them to the foreground
   if(this.IsVisible())
     {
      if(this.m_arr_butt_ud_visible_flag)
         this.BringToTopArrUpDownBox();
      if(this.m_arr_butt_lr_visible_flag)
         this.BringToTopArrLeftRightBox();
     }
  }
//+------------------------------------------------------------------+

Se antes chamávamos o método BringToTop da classe CForm pai, este método trazia absolutamente todos os objetos anexados ao controle para o primeiro plano, tornando-os visíveis, agora precisamos controlar a visibilidade dos botões de controle de rolagem. Assim, apenas tornamos o próprio objeto visível, e então dentro do método verificamos se os cabeçalhos e campos das guias e botões de rolagem do controle da linha de cabeçalho precisam ser exibidos.

Método para exibir os controles "Botões para cima-baixo":

//+------------------------------------------------------------------+
//| Display Up-down button controls                                  |
//+------------------------------------------------------------------+
void CTabControl::ShowArrUpDownBox(void)
  {
   CArrowUpDownBox *box=this.GetArrUpDownBox();
   if(box==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ)," ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX));
      return;
     }
   box.Show();
  }
//+------------------------------------------------------------------+

Obtemos um ponteiro para o controle. Se não foi possível obter o ponteiro, exibimos uma mensagem de erro e saímos do método. Após o recebimento bem-sucedido do ponteiro, exibimos o objeto.


Método que exibe os controles "Botões direito-esquerdo":

//+------------------------------------------------------------------+
//| Display the Right-left button controls                           |
//+------------------------------------------------------------------+
void CTabControl::ShowArrLeftRightBox(void)
  {
   CArrowLeftRightBox *box=this.GetArrLeftRightBox();
   if(box==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ)," ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX));
      return;
     }
   box.Show();
  }
//+------------------------------------------------------------------+

A lógica do método é idêntica à anterior.


Métodos que ocultam os controles "Botões para cima-baixo" e "Botões direita-esquerda":

//+------------------------------------------------------------------+
//| Hide the Up-down button controls                                 |
//+------------------------------------------------------------------+
void CTabControl::HideArrUpDownBox(void)
  {
   CArrowUpDownBox *box=this.GetArrUpDownBox();
   if(box==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ)," ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX));
      return;
     }
   box.Hide();
  }
//+------------------------------------------------------------------+
//| Hide the Right-left button controls                              |
//+------------------------------------------------------------------+
void CTabControl::HideArrLeftRightBox(void)
  {
   CArrowLeftRightBox *box=this.GetArrLeftRightBox();
   if(box==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ)," ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX));
      return;
     }
   box.Hide();
  }
//+------------------------------------------------------------------+

A lógica dos métodos é idêntica à lógica dos dois métodos discutidos acima para exibir controles. Mas aqui, em vez de mostrar o elemento, ele está oculto.


Métodos que trazem os controles "Botões para cima/baixo" e "Botões direito/esquerdo" para o primeiro plano:

//+------------------------------------------------------------------+
//| Move Up-down button controls to the foreground                   |
//+------------------------------------------------------------------+
void CTabControl::BringToTopArrUpDownBox(void)
  {
   CArrowUpDownBox *box=this.GetArrUpDownBox();
   if(box==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ)," ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX));
      return;
     }
   box.BringToTop();
  }
//+------------------------------------------------------------------+
//|Move right-left button controls to the foreground                 |
//+------------------------------------------------------------------+
void CTabControl::BringToTopArrLeftRightBox(void)
  {
   CArrowLeftRightBox *box=this.GetArrLeftRightBox();
   if(box==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ)," ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX));
      return;
     }
   box.BringToTop();
  }
//+------------------------------------------------------------------+

Tudo é exatamente o mesmo, mas trazido para o primeiro plano.


Método que define a visibilidade dos botões esquerdo-direito:

//+------------------------------------------------------------------+
//| Set the visibility of the left-right buttons                     |
//+------------------------------------------------------------------+
void CTabControl::SetVisibleLeftRightBox(const bool flag)
  {
   this.m_arr_butt_lr_visible_flag=flag;
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *obj=list.At(i);
      if(obj==NULL)
         continue;
      obj.SetVisibleLeftRightBox(flag);
     }
  }
//+------------------------------------------------------------------+

Primeiro, definimos o sinalizador de visibilidade do controle, em seguida, obtemos a lista de cabeçalhos das guias e, em um loop pela lista, para cada um dos objetos-cabeçalhos, definimos o valor do sinalizador passado para o método.


Método que define a visibilidade dos botões para cima/baixo:

//+------------------------------------------------------------------+
//| Set the visibility of up-down buttons                            |
//+------------------------------------------------------------------+
void CTabControl::SetVisibleUpDownBox(const bool flag)
  {
   this.m_arr_butt_ud_visible_flag=flag;
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *obj=list.At(i);
      if(obj==NULL)
         continue;
      obj.SetVisibleUpDownBox(flag);
     }
  }
//+------------------------------------------------------------------+

A lógica do método é idêntica à anterior. Mas aqui os sinalizadores são definidos para objetos-botões para cima/baixo.


Método que define o tamanho dos botões para esquerda-direita:

//+------------------------------------------------------------------+
//| The method setting the size of left-right buttons                |
//+------------------------------------------------------------------+
void CTabControl::SetSizeLeftRightBox(const int value)
  {
   CArrowLeftRightBox *butt=this.GetArrLeftRightBox();
   if(butt!=NULL)
      butt.Resize(value,butt.Height(),false);
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *obj=list.At(i);
      if(obj==NULL)
         continue;
      obj.SetSizeLeftRightBox(value);
     }
  }
//+------------------------------------------------------------------+

Obtemos o ponteiro para o objeto-botão para esquerda/direita, definimos o valor passado para o método como a largura do objeto (já que aqui só precisamos alterar a largura do objeto) e, em seguida, em um loop pela lista de cabeçalhos da guia em cada objeto subsequente escrevemos o valor da largura definida do objeto-botão de controle de rolagem.


Método que define o tamanho dos botões para cima-baixo:

//+------------------------------------------------------------------+
//| Set the up-down button size                                      |
//+------------------------------------------------------------------+
void CTabControl::SetSizeUpDownBox(const int value)
  {
   CArrowUpDownBox *butt=this.GetArrUpDownBox();
   if(butt!=NULL)
      butt.Resize(butt.Width(),value,false);
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *obj=list.At(i);
      if(obj==NULL)
         continue;
      obj.SetSizeUpDownBox(value);
     }
  }
//+------------------------------------------------------------------+

A lógica do método é idêntica à anterior. Mas aqui obtemos o ponteiro para o objeto-botão para cima-baixo e definimos a altura desse objeto.


Método que desloca a fileira de cabeçalhos:

//+------------------------------------------------------------------+
//| Shift the header row                                             |
//+------------------------------------------------------------------+
void CTabControl::ShiftHeadersRow(const int selected)
  {
//--- If there are multiline headers, leave
   if(this.Multiline())
      return;
//--- Get the list of tab headers
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Get the header of the selected tab
   CTabHeader *header=this.GetTabHeader(selected);
   if(header==NULL)
      return;
//--- Check how much of the selected header is cropped on the right
   int hidden=header.RightEdge()-this.RightEdgeWorkspace();
//--- If the header is not cropped, exit
   if(hidden<0)
      return;
   CTabHeader *obj=NULL;
   int shift=0;
//--- Look for the leftmost one starting from the selected header
   for(int i=selected-1;i>WRONG_VALUE;i--)
     {
      obj=list.At(i);
      if(obj==NULL)
         continue;
      //--- If the leftmost one is found, remember how much to shift all headers to the right
      if(obj.CoordX()-2==this.CoordX())
         shift=obj.Width();
     }
//--- In a loop by the list of headers,
   for(int i=0;i<list.Total();i++)
     {
      //--- get the next header
      obj=list.At(i);
      if(obj==NULL)
         continue;
      //--- and, if the header is shifted to the left by 'shift',
      if(obj.Move(obj.CoordX()-shift,obj.CoordY()))
        {
         //--- save its new relative coordinates
         obj.SetCoordXRelative(obj.CoordX()-this.CoordX());
         obj.SetCoordYRelative(obj.CoordY()-this.CoordY());
         //--- If the title has gone beyond the left edge,
         if(obj.CoordX()-2<this.CoordX())
           {
            //--- crop and hide it
            obj.Crop();
            obj.Hide();
           }
         //--- If the header fits the visible area of the control,
         else
           {
            //--- display and redraw it
            obj.Show();
            obj.Redraw(false);
            //--- Get the tab field corresponding to the header
            CTabField *field=obj.GetFieldObj();
            if(field==NULL)
               continue;
            //--- If this is a selected header,
            if(i==selected)
              {
               //--- Draw the field frame
               field.DrawFrame();
               field.Update();
              }
           }
        }
     }
//--- Redraw the chart to display changes immediately
   ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+

A lógica do método é descrita nos comentários ao código linha por linha. Resumidamente, o método recebe o índice de cabeçalho que foi clicado (seleção de guia do objeto TabControl). Se a guia selecionada (seu cabeçalho) se estender para fora do contêiner, ou seja, se estiver recortada, precisamos deslocar todos os cabeçalhos para a esquerda para que o cabeçalho selecionado se torne totalmente visível, deslocando todos os cabeçalhos para a esquerda. O primeiro cabeçalho à esquerda será movido para a esquerda e o próximo será movido para o seu lugar.

Portanto, primeiro temos que encontrar o primeiro cabeçalho à esquerda que seja visível, e descobrir qual é sua largura (cada cabeçalho pode ter uma largura diferente, se você definir seu tamanho de acordo com o texto do cabeçalho). A largura do primeiro cabeçalho a ser encontrado será o valor pelo qual toda a linha de cabeçalho deve ser deslocada para a esquerda. Após deslocar a linha, para o cabeçalho selecionado, encontramos seu campo e redesenhamos sua moldura, porque a moldura do campo é desenhada em relação à localização do cabeçalho, e agora é deslocada, e a moldura não será desenhada corretamente.

Este método só é adequado para mover uma fileira de cabeçalhos colocados horizontalmente à esquerda. Com base neste método, no próximo artigo criaremos métodos para mover a linha de cabeçalho em todas as direções - esquerda-direita e para cima-abaixo.


Manipulador de eventos:

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

Primeiro chamamos o método de correção de coordenadas Y para a subjanela da classe do objeto-elemento (isto funciona para cada elemento na tela, mas aqui o manipulador de eventos da classe pai é substituído, então precisamos nos lembrar de fazer a chamada do método de correção de coordenadas), e então chamamos o método de deslocamento de linha do cabeçalho da guia discutido acima.


Vamos alterar a classe-coleção de objetos gráficos no arquivo \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh.

Vamos adicionar um método que retorna um elemento gráfico pelo nome:

//--- Return the list of graphical elements by chart ID and object name
   CArrayObj        *GetListCanvElementByName(const long chart_id,const string name)
                       {
                        string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
                        CArrayObj *list=CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),CANV_ELEMENT_PROP_CHART_ID,chart_id,EQUAL);
                        return CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                       }
//--- Return the graphical element by name
   CGCnvElement     *GetCanvElement(const string name)
                       {
                        string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
                        CArrayObj *list=CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                        return(list!=NULL ? list.At(0) : NULL);
                       }
//--- Return the graphical element by chart ID and name
   CGCnvElement     *GetCanvElement(const long chart_id,const string name)
                       {
                        CArrayObj *list=this.GetListCanvElementByName(chart_id,name);
                        return(list!=NULL ? list.At(0) : NULL);
                       }
//--- Return the graphical element by chart and object IDs

Como agora todos os nomes de todos os elementos gráficos na tela são únicos, não há necessidade de especificar o identificador do gráfico sobre o qual o objeto é construído ao procurar por um objeto pelo nome. Portanto, é razoável acrescentar um método que devolva o objeto somente pelo seu nome. Aqui verificamos qual nome é passado ao método e se não há nome do programa na string de nomes, então o nome do programa é adicionado ao início do nome para que o nome do objeto procurado coincida com o nome real do item gráfico - afinal, todos eles têm um substring com o nome do programa - é assim que a biblioteca cria os nomes dos elementos gráficos.


Os objetos WinForms podem agora enviar mensagens sobre seus eventos para o gráfico do programa de controle. Estes eventos são capturados pela biblioteca e devem ser capazes de processá-los e redirecionar a mensagem do evento para a classe correta. Portanto, vamos adicionar o processamento dos códigos de eventos de objetos WinForms ao manipulador de eventos da classe-coleção de elementos gráficos:

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

//--- Processing WinForms control events
   if(idx>WF_CONTROL_EVENT_NO_EVENT && idx<WF_CONTROL_EVENTS_NEXT_CODE)
     {
      //--- Clicking the control
      if(idx==WF_CONTROL_EVENT_CLICK)
        {
         //---
        }
      //--- Selecting the TabControl tab
      if(idx==WF_CONTROL_EVENT_TAB_SELECT)
        {
         string array[];
         if(::StringSplit(sparam,::StringGetCharacter(";",0),array)!=2)
           {
            CMessage::ToLog(MSG_GRAPH_OBJ_FAILED_GET_OBJECT_NAMES);
            return;
           }
         CWinFormBase *main=this.GetCanvElement(array[0]);
         if(main==NULL)
            return;
         CWinFormBase *base=main.GetElementByName(array[1]);
         if(base!=NULL)
            base.OnChartEvent(idx,lparam,dparam,sparam);
        }
     }
//--- Handle the events of renaming and clicking a standard graphical object
   if(id==CHARTEVENT_OBJECT_CHANGE  || id==CHARTEVENT_OBJECT_DRAG    || id==CHARTEVENT_OBJECT_CLICK   ||
      idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG   || idx==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Calculate the chart ID
      //---...
      //---...
     }
//---...
//---...
  }

Até o momento, apenas um código de evento é tratado aqui, a seleção da guia no controle TabControl. Dividimos a mensagem passada no parâmetro string sparam em duas pelo separador ";" usando a função StringSplit(). Como resultado, teremos dois nomes na matriz, o nome do objeto principal (no nosso caso, o objeto painel, main), e o objeto WinForms TabControl, base, que é anexado ao painel. Nos parâmetros lparam e dparam, passamos o número da linha e da coluna do cabeçalho da guia selecionada. Com todos estes dados, podemos agora identificar com precisão o painel ao qual o TabControl está anexado, e a guia do controle cujo cabeçalho foi clicado. Uma vez obtidas as indicações para todos esses objetos, podemos chamar o manipulador de eventos do objeto recebido que, por sua vez, chamará o método de deslocamento da linha de cabeçalho das guias.

Por enquanto, são todas as mudanças e melhorias necessárias.


Teste

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

O que vamos testar? Vamos criar o painel principal, e nele colocaremos um controle TabControl com 11 guias. Em cada uma das guias, criaremos um rótulo de texto com a descrição da aba, para que durante o teste possamos ver qual realmente está sendo exibida.

Depois de iniciar o EA, definimos seus ajustes para esticar os cabeçalhos das guias segundo a largura do controle, para que seja possível ver claramente qual guia não cabe e ultrapassa a borda do contêiner. Vamos verificar a posição das guias em cada lado do controle e como os botões de rolagem da fileira de cabeçalho são posicionados. E quando os cabeçalhos estiverem em cima, vamos clicar nos cabeçalhos das guias aparadas à direita e ver como toda a linha se desloca para a esquerda tornando visível o cabeçalho selecionado, copiando o comportamento do controle TabControl no MS Visual Studio.

No manipulador OnInit() do Expert Advisor, vamos criar um painel e, nele, um controle TabControl de 11 guias com rótulos de texto:

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

//--- Create the required number of WinForms Panel objects
   CPanel *pnl=NULL;
   for(int i=0;i<1;i++)
     {
      pnl=engine.CreateWFPanel("WinForms Panel"+(string)i,(i==0 ? 50 : 70),(i==0 ? 50 : 70),410,200,array_clr,200,true,true,false,-1,FRAME_STYLE_BEVEL,true,false);
      if(pnl!=NULL)
        {
         pnl.Hide();
         Print(DFUN,"Panel description: ",pnl.Description(),", Type and name: ",pnl.TypeElementDescription()," ",pnl.Name());
         //--- Set Padding to 4
         pnl.SetPaddingAll(3);
         //--- Set the flags of relocation, auto resizing and auto changing mode from the inputs
         pnl.SetMovable(InpMovable);
         pnl.SetAutoSize(InpAutoSize,false);
         pnl.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpAutoSizeMode,false);
   
         //--- Create TabControl
         pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,InpTabControlX,InpTabControlY,pnl.Width()-30,pnl.Height()-40,clrNONE,255,true,false);
         CTabControl *tc=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,0);
         if(tc!=NULL)
           {
            tc.SetTabSizeMode((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)InpTabPageSizeMode);
            tc.SetAlignment((ENUM_CANV_ELEMENT_ALIGNMENT)InpHeaderAlignment);
            tc.SetMultiline(InpTabCtrlMultiline);
            tc.SetHeaderPadding(6,0);
            tc.CreateTabPages(11,0,56,20,TextByLanguage("Вкладка","TabPage"));
            //--- Create a text label with a tab description on each tab
            for(int j=0;j<tc.TabPages();j++)
              {
               tc.CreateNewElement(j,GRAPH_ELEMENT_TYPE_WF_LABEL,60,20,80,20,clrDodgerBlue,255,true,false);
               CLabel *label=tc.GetTabElement(j,0);
               if(label==NULL)
                  continue;
               label.SetText("TabPage"+string(j+1));
              }
           }
        }
     }
//--- Display and redraw all created panels
   for(int i=0;i<1;i++)
     {
      pnl=engine.GetWFPanelByName("Panel"+(string)i);
      if(pnl!=NULL)
        {
         pnl.Show();
         pnl.Redraw(true);
        }
     }
        
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Criamos painéis em um loop (por enquanto, apenas um painel), pois descobrimos que, se criarmos vários painéis com controles TabControl, esses elementos não funcionarão corretamente. A fim de corrigir esta falha no futuro, criaremos o número necessário de painéis.

Vamos compilar o Expert Advisor e executá-lo no gráfico, após definir as configurações necessárias:


Como você pode ver, a funcionalidade declarada funciona corretamente.


O que virá a seguir?

No próximo artigo, criaremos métodos para rolar os cabeçalhos das guias em todas as direções usando os botões de controle de rolagem.

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 WinForms ListBox e ButtonListBox
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
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 17): Recorte de seções invisíveis de objetos, objetos-botões WinForms auxiliares com setas

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

Arquivos anexados |
MQL5.zip (4454.87 KB)
Aprendendo a construindo um Expert Advisor que opera de forma automática (Parte 09): Automação (I) Aprendendo a construindo um Expert Advisor que opera de forma automática (Parte 09): Automação (I)
Criar um Expert Advisor automático não é uma tarefa muito complicada. Mas sem o devido conhecimento você pode acabar fazendo muita bobagem. Neste artigo iremos ver como se dá a construção do primeiro nível de automação. Então a questão aqui é aprender a criar o gatilho para promover o Breakeven e o Trailing Stop.
DoEasy. Controles (Parte 17): Recorte de seções invisíveis de objetos, objetos-botões WinForms auxiliares com setas DoEasy. Controles (Parte 17): Recorte de seções invisíveis de objetos, objetos-botões WinForms auxiliares com setas
Neste artigo vamos criar funcionalidade para esconder seções de objetos que ultrapassam as margens de seu contêiner e vamos elaborar objetos-botões auxiliares com setas para usá-los como parte de outros objetos WinForms.
Redes neurais de maneira fácil (Parte 29): Algoritmo ator-crítico de vantagem (Advantage actor-critic) Redes neurais de maneira fácil (Parte 29): Algoritmo ator-crítico de vantagem (Advantage actor-critic)
Nos artigos anteriores desta série, conhecemos 2 algoritmos de aprendizado por reforço. Cada um deles tem suas próprias vantagens e desvantagens. Como costuma acontecer quando nos deparamos com esses casos, surge a ideia de combinar os dois métodos em um algoritmo que incorpore o melhor dos dois. E assim compensar as deficiências de cada um deles. Falaremos sobre tal combinação de métodos neste artigo.
Aprendendo a construindo um EA que opera de forma automática (Parte 08): OnTradeTransaction Aprendendo a construindo um EA que opera de forma automática (Parte 08): OnTradeTransaction
Neste artigo, mostrei como você pode usar o sistema de tratamento de eventos, a fim de conseguir lidar com mais agilidade, e de uma forma melhor com questões envolvendo o sistema de ordens, a fim de deixar o EA mais rápido. Assim ele não precisará, ficar procurando informações a todo o momento.