English Русский 中文 Español Deutsch 日本語
preview
DoEasy. Controles (Parte 19): Rolagem de guias no elemento TabControl, eventos de objetos WinForms

DoEasy. Controles (Parte 19): Rolagem de guias no elemento TabControl, eventos de objetos WinForms

MetaTrader 5Exemplos | 23 dezembro 2022, 09:32
430 0
Artyom Trishkin
Artyom Trishkin

Conteúdo


Ideia

No último artigo, provamos a rolagem de uma fileira de cabeçalhos de guias enquanto selecionávamos um cabeçalho parcialmente oculto, o que causava que este último se deslocasse para a esquerda, ficando totalmente visível. Hoje, já partindo da funcionalidade criada, elaboraremos métodos para deslocar a fileira de cabeçalhos da esquerda para a direita e de cima para baixo. Devemos considerar que, para rolar os cabeçalhos para cima e para baixo, importa onde a fileira de cabeçalhos está localizada, se à esquerda ou à direita. Todos esses métodos serão chamados ao selecionar um cabeçalho parcialmente oculto para exibi-lo por completo e ao pressionar os botões de rolagem (para cima, para baixo, para a esquerda e para a direita). Para entender qual botão e em qual controle foi pressionado, usaremos o modelo de evento, assim quando o botão for pressionado, será enviado um evento que a biblioteca irá interceptar e processar, enviando, para o manipulador de eventos do elemento ao qual o botão pressionado é vinculado, um evento para processamento adicional dentro do controle. E usaremos esse modelo para trabalhar em outros controles que contêm controles.

No momento, enviamos os nomes dos elementos gráficos — o principal e o base — para o manipulador de eventos no parâmetro sparam. O elemento principal é aquele ao qual está anexado o objeto no qual ocorreu o evento, e ele é aqui considerado o elemento base. Mas, se tal controle base for composto, isto é, mais alguns controles estão anexados a ele (botões, por exemplo) e um evento já ocorreu neles, então não conseguiremos encontrar o elemento base, pois não está vinculado diretamente ao objeto principal. Para contornar essa situação, vamos fazer o seguinte por enquanto: no parâmetro sparam, passaremos os nomes do objeto principal, o base (como antes), mais o nome do objeto em que ocorreu o evento.

Assim, teremos dados de entrada para o objeto principal, para o base, dentro do qual ocorreu um evento em algum dos elementos anexados, e teremos o nome desse elemento no qual o evento ocorreu. Para saber exatamente que tipo de controle foi aquele em que clicamos usando o botão (um caso especial), enviaremos o tipo deste objeto base no parâmetro dparam. Assim, conhecendo o tipo do objeto base, obteremos uma lista de todos os controles com o tipo registrado em dparam no objeto principal e, a seguir, em um loop por todos esses objetos, procuraremos o objeto anexado a ele com o nome passado em sparam, em que houve um evento (clique no controle).

No momento, este esquema não parece muito confiável em termos de versatilidade para lidar com objetos mais complexos. Mas nesta fase, ele é suficiente para continuarmos o desenvolvimento da biblioteca. Já quando criemos controles mais complexos com um esquema mais desenvolvido de aninhamento de objetos, então teremos um exemplo prático claro de como identificar corretamente na biblioteca e universalmente os eventos em tais objetos (lembre-se do princípio de "do simples ao complexo").

A biblioteca agora permite ocultar e exibir controles. Para exibir ou ocultar elementos, basta ocultar o elemento principal ou base e todos os elementos anexados a ele serão ocultados ou exibidos respectivamente. Mas se pensarmos um pouco sobre isso e imaginarmos que deve haver objetos dentro do controle que são mostrados ou exibidos independentemente do objeto principal, ou seja, sua visibilidade é definida dentro do próprio controle, e se tal objeto estiver escondido mas seu pai for mostrado, então este objeto não deve ser mostrado. Em tais situações, precisamos controlar a visibilidade destes objetos a partir de seu controle base, e para a obtenção de tal possibilidade, precisamos introduzir mais uma propriedade de elemento gráfico, isto é, o sinalizador de exibição. Então, se o objeto principal tiver sido escondido e depois exibido, o controle para o qual seu sinalizador de exibição for reiniciado permanecerá oculto até que seja explicitamente exibido desde seu controle básico.

Mas chega de teoria, vamos ao que interessa...


Modificando as classes da biblioteca

Como estamos fazendo tudo pensando no futuro, para os botões de controle de rolagem não criaremos nossos próprios identificadores de eventos, uma vez que iremos criá-los enquanto focamos em outros novos controles que também usarão botões (barra de rolagem, listas suspensas, etc.). Isso quer dizer que criaremos alguns eventos de clique genéricos no controle de rolagem ou de janelas suspensas.

No arquivo \MQL5\Include\DoEasy\Defines.mqh, adicionei novas constantes de enumeração na lista de possíveis eventos para controles WinForms:

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

Aqui, por precaução, criamos um identificador de evento para cancelar o clique no controle (em alguns casos é possível processar tal evento) e adicionamos eventos gerais para controles que possuem botões que gerem a aparência do controle ou servem para interagir com ele.

Adicionamos uma nova propriedade à enumeração de propriedades inteiras do elemento gráfico na tela e aumentamos o número total de propriedades de 96 para 97:

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

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

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

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

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

O sinalizador de exibição de um controle não oculto indica que, se o controle não estiver oculto, mas esse sinalizador estiver desmarcado (false), esse controle não será exibido. O que significa que se o controle principal for exibido, então seus descendentes, para os quais este sinalizador está desmarcado, ainda permanecerão ocultos até que sejam forçados a serem exibidos definindo este sinalizador como true para eles e chamando o método Show().


E adicionamos estas novas propriedades à lista de possíveis critérios para ordenar elementos gráficos na tela:

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

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

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

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

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

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

Agora poderemos selecionar, classificar e filtrar listas de objetos-elementos gráficos por esta nova propriedade.


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

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

...

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

e os textos das mensagens correspondentes aos índices recém-adicionados:

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

...

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


Vamos modificar a classe do objeto-elemento gráfico no arquivo \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh.

Vamos adicionar a nova propriedade recém-adicionada à estrutura do objeto :

private:
   int               m_shift_coord_x;                          // Offset of the X coordinate relative to the base object
   int               m_shift_coord_y;                          // Offset of the Y coordinate relative to the base object
   struct SData
     {         
      //--- Object integer properties
      int            id;                                       // Element ID
      int            type;                                     // Graphical element type
               
      //---... 
      //---... 
               
      int            visible_area_w;                           // Visibility scope width
      int            visible_area_h;                           // Visibility scope height
      bool           displayed;                                // Non-hidden control display flag
      //--- Object real properties
               
      //--- Object string properties
      uchar          name_obj[64];                             // Graphical element object name
      uchar          name_res[64];                             // Graphical resource name
      uchar          text[256];                                // Graphical element text
      uchar          descript[256];                            // Graphical element description
     };        
   SData             m_struct_obj;                             // Object structure
   uchar             m_uchar_array[];                          // uchar array of the object structure

Todas as propriedades do objeto são gravadas na estrutura do objeto para salvá-lo em um arquivo e, em seguida, restaurar o objeto a partir do arquivo.

No bloco com métodos para acesso simplificado às propriedades do objeto, vamos adicionar dois novos métodos para configurar e obter a propriedade de exibição do objeto:

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

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

Os métodos simplesmente gravam o sinalizador passado na propriedade do objeto e retornam o valor escrito na propriedade do objeto.


Em ambos os construtores de classe, escrevemos o valor padrão na nova propriedade:

//+------------------------------------------------------------------+
//| Parametric constructor                                           |
//+------------------------------------------------------------------+
CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                           const int      element_id,
                           const int      element_num,
                           const long     chart_id,
                           const int      wnd_num,
                           const string   descript,
                           const int      x,
                           const int      y,
                           const int      w,
                           const int      h,
                           const color    colour,
                           const uchar    opacity,
                           const bool     movable=true,
                           const bool     activity=true,
                           const bool     redraw=false) : m_shadow(false)
  {
   this.SetTypeElement(element_type);
   this.m_type=OBJECT_DE_TYPE_GELEMENT; 
   this.m_element_main=NULL;
   this.m_element_base=NULL;
   this.m_chart_color_bg=(color)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_COLOR_BACKGROUND);
   this.m_name=this.CreateNameGraphElement(element_type);
   this.m_chart_id=(chart_id==NULL || chart_id==0 ? ::ChartID() : chart_id);
   this.m_subwindow=wnd_num;
   this.SetFont(DEF_FONT,DEF_FONT_SIZE);
   this.m_text_anchor=0;
   this.m_text_x=0;
   this.m_text_y=0;
   this.SetBackgroundColor(colour,true);
   this.SetOpacity(opacity);
   this.m_shift_coord_x=0;
   this.m_shift_coord_y=0;
   if(::ArrayResize(this.m_array_colors_bg,1)==1)
      this.m_array_colors_bg[0]=this.BackgroundColor();
   if(::ArrayResize(this.m_array_colors_bg_dwn,1)==1)
      this.m_array_colors_bg_dwn[0]=this.BackgroundColor();
   if(::ArrayResize(this.m_array_colors_bg_ovr,1)==1)
      this.m_array_colors_bg_ovr[0]=this.BackgroundColor();
   if(this.Create(chart_id,wnd_num,x,y,w,h,redraw))
     {
      this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,this.m_canvas.ResourceName()); // Graphical resource name
      this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,CGBaseObj::ChartID());         // Chart ID

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

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

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

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

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

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

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

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

Por padrão, um objeto é definido como visível e exibido quando a visibilidade do objeto principal ou do base é ativada. Para definir a visibilidade do objeto manualmente, o valor do sinalizador deve ser definido como false. Neste caso, se o objeto base ou principal foi oculto e depois exibido, então este objeto atual permanecerá em um estado oculto e, para exibi-lo, nós precisaremos definir a propriedade Displayed como true e chamar o método Show() deste objeto.


No método que cria a estrutura do objeto, escrevemos o valor da propriedade do objeto no campo correspondente da estrutura:

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

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

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


No método que cria um objeto a partir de uma estrutura, definimos o valor da nova propriedade do objeto a partir do campo correspondente da estrutura:

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

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

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


Na classe do objeto WinForms base no arquivo \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh, no método que retorna a descrição da propriedade inteira do elemento, escreveremos um bloco de código para retornar a descrição da nova propriedade do objeto:

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

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

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

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

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

Se esta propriedade for passada para o método, então, dependendo do sinalizador only_prop, uma mensagem de texto será criada — apenas o nome da propriedade (only_prop = true) ou junto com o valor definido para a propriedade (only_prop = false) .


Todos os controles, de uma forma ou de outra, usarão a funcionalidade de evento, tanto para uso interno quanto para notificar o programa sobre eventos que ocorrem em sua GUI. A classe principal para interação com o usuário é a classe de objeto-forma, ela implementa a funcionalidade de interação do mouse, e a classe base de todos os objetos da biblioteca WinForms é herdada dela. Nela, criaremos um método de envio de mensagens de elementos gráficos.

No arquivo de classe do objeto-forma \MQL5\Include\DoEasy\Objects\Graph\Form.mqh, em sua seção protegida, declararemos um método para envio de mensagens.
O método será virtual caso algum objeto filho precise sobrescrevê-lo:

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

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

public:


No método que mostra a forma, escreveremos a verificação do sinalizador de exibição do objeto (verificação da nova propriedade do elemento gráfico):

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

Ao chamar este método para qualquer objeto do tipo CForm ou superior, o sinalizador de exibição do objeto será verificado primeiro e, se o sinalizador não estiver definido (o controle de visibilidade manual ativado), abandonamos imediatamente o método.


Fora do corpo da classe, vamos escrever a implementação do método que envia a mensagem sobre o evento:

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

Aqui obtemos ponteiros para os objetos principal e base e obtemos seus nomes. Nesse caso, se o ponteiro para o objeto principal for NULL, provavelmente esse é o objeto principal. Para fazer isso, verificamos se é assim e, em caso afirmativo, usamos o nome do objeto atual. Se, por algum motivo, o ponteiro não for recebido, para o nome usamos a string "Lost name of object", para que possamos controlá-lo.

Em seguida, precisamos descobrir o tipo do objeto base ao qual o objeto base deste objeto está anexado (ou seja, temos que obter seu objeto base a partir do objeto base, bem como seu tipo a partir dele próprio) e gravar todos os dados recebidos nas variáveis enviadas na mensagem do evento. Em lparam enviaremos o identificador do objeto atual, em dparam enviaresmos o tipo do objeto base ao qual está anexado o objeto base do objeto atual, e em sparam enviaremos uma string que contém os nomes de três objetos separados pelo delimitador ";" - esses objetos são o principal, o base e o atual. Ao receber um evento, podemos usar esses dados para determinar exatamente de qual objeto veio a mensagem do evento.

No momento, essa lógica é suficiente para determinar o objeto que gerou o evento, mas vamos alterá-la mais tarde, pois não permite rastrear todo o aninhamento de objetos ao criar controles mais complexos com uma hierarquia mais profunda de aninhamento.

Agora vamos adicionar o envio de mensagens de eventos aos manipuladores de eventos dos objetos WinForms.

No arquivo de classe do objeto-botão \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Button.mqh, no manipulador do evento "Cursor dentro da área ativa, botão (esquerdo) do mouse liberado" escrevemos o envio de mensagens sobre o evento:

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


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

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


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

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


Já após a publicação do último artigo, notei que dois arquivos de classe dos objetos-botões que têm as setas esquerda-direita e para cima-para baixo pararam ser compilados sozinhos. Eles só podem ser compilados como parte da biblioteca, isto é, ao compilar o arquivo principal da biblioteca Engine.mqh, mas já sozinhos não é possível. Isso traz confusão. E para corrigir essa situação, temos que alterar a lista de arquivos de inclusão nos arquivos desses objetos.
Anteriormente, tínhamos anexado o arquivo do objeto-painel:

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

Agora vamos anexar apenas os arquivos que devem ser usados por essas classes.

Para o arquivo do objeto-botão com as setas para cima-baixo \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ArrowUpDownBox.mqh:

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


Para o arquivo do objeto-botão com as setas para esquerda-direita \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ArrowLeftRightBox.mqh:

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

Agora ambos os arquivos serão compilados normalmente de forma independente e como parte da biblioteca.


Modificamos a classe do objeto-cabeçalho da guia do controle TabControl no arquivo \MQL5\Include\DoEasy\Objects\Graph\WForms\TabHeader.mqh.

No manipulador de eventos "Cursor dentro da área ativa, botão (esquerdo) do mouse liberado" removemos o bloco de código para criação do evento:

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

Agora temos um método para criar e enviar um evento, vamos usá-lo:

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


Quando rolamos a fileira de cabeçalhos, por exemplo, para a esquerda, o cabeçalho mais à esquerda se move para fora do controle e o que está à direita dele toma seu lugar. Como a coordenada inicial dos cabeçalhos é deslocada dois pixels para a direita a partir da borda esquerda do contêiner, dois pixels do cabeçalho que vai além da borda esquerda permanecem visíveis nesta mesma área, por isso ele é recortado seguindo as bordas da área do contêiner dentro da qual os elementos devem estar visíveis.

Para ocultar essa fina parte visível do cabeçalho que saiu da borda esquerda, precisamos ajustar ligeiramente as dimensões da área do contêiner dentro da qual os objetos anexados são exibidos. Nesse caso, também precisamos levar em consideração o fato de o cabeçalho estar selecionado ou não, pois o cabeçalho selecionado aumenta de tamanho em dois pixels de cada lado. E para que, ao ser selecionado, dois dos seus pixels não sejam cortados, devemos levar isso em consideração. Ou seja, precisamos redimensionar dinamicamente a área do contêiner onde os objetos estão visíveis, dependendo de qual objeto está na borda. Se selecionado, não alteramos o tamanho e, se não selecionado, reduzimos dois pixels do mesmo.

No método que recorta a imagem, delimitada pela área retangular visível a ser calculada, adicionamos um ajuste do tamanho da área visível do contêiner e adicionamos o valor resultante às bordas da área visível:

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

Agora, quando os cabeçalhos forem além do contêiner, sua seção estreita, com dois pixels de tamanho, não será exibida.


No arquivo de classe do objeto WinForms TabControl \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\TabControl.mqh, em sua seção privada, declararemos novos métodos:

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

e removemos o método público

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

//--- Constructor

Tornamos esse método público no último artigo, e lembremos que ele deslocava a fileira de cabeçalhos para a esquerda quando um cabeçalho parcialmente oculto era clicado. Agora os métodos declarados acima irão encarregar-se disso. Além disso, eles processarão o clique em um cabeçalho parcialmente oculto e o clique no botão que controla a rolagem da fileira de cabeçalhos.

Cada um dos métodos declarados é projetado para rolar a fileira de cabeçalhos em sua própria direção:

  • Quando os cabeçalhos estão na parte superior ou inferior, existem dois métodos de rolagem para a esquerda e rolagem para a direita.
  • Quando os cabeçalhos estão à esquerda, há dois métodos para rolagem à esquerda e rolagem à direita. 
  • Quando os cabeçalhos estão à direita, tem dois métodos de rolagem para a esquerda e rolagem para a direita.

No método que cria o número especificado de guias, ao criar objetos-botões com setas esquerda-direita e cima-baixo, precisamos especificar os objetos principal e base para cada uma desses objetos e para cada objeto-botão com seta que faz parte desses objetos, caso contrário, não conseguiremos encontrar esses objetos ao criar a mensagem sobre o evento ao clicar o respectivo botão:

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

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

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

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

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

Depois de criar os objetos-botões com as setas esquerda-direita e cima-baixo, obtemos um ponteiro para o objeto criado e definimos os objetos principal e base para ele. Então, do objeto resultante, obtemos seus objetos-botões com setas, e para cada um deles especificamos os objetos tanto principal como nosso básico.

No método que mostra o controle, vamos adicionar uma verificação de sinalizador de exibição de objeto:

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

Se a exibição manual do objeto estiver habilitada, abandonamos o método.


Método que retorna um ponteiro para o primeiro cabeçalho visível:

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

O primeiro cabeçalho visível é quer seja o cabeçalho à esquerda quando os cabeçalhos estão na parte superior/inferior ou o cabeçalho abaixo quando os cabeçalhos estão à esquerda ou o cabeçalho na parte superior quando os cabeçalhos estão à direita do controle. Para encontrar o cabeçalho extremo, precisamos fazer um loop sobre todos os cabeçalhos do objeto para verificar se suas coordenadas correspondem à localização da fileira de cabeçalhos. Para ser posicionado no topo, o cabeçalho deve estar localizado na coordenada X inicial do contêiner. Nesse caso, se o cabeçalho não for selecionado, sua coordenada inicial será deslocada dois pixels para a direita. E será feito da mesma forma quando a fileira de cabeçalhos tiver uma localização diferente.
O método no loop procura uma correspondência entre as coordenadas do objeto e as coordenadas do contêiner, dependendo da localização da fileira de cabeçalhos, e retorna um ponteiro para o objeto encontrado. Se nenhum dos cabeçalhos for encontrado, o método retornará NULL.


Método que rola a fileira de cabeçalhos para a esquerda:

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


Método que rola a fileira de cabeçalhos para a direita:

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


Método que rola para cima a fileira de cabeçalhos quando eles estão à esquerda:

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


Método que rola para baixo a fileira de cabeçalhos quando eles estão à esquerda:

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


Método que rola para cima a fileira de cabeçalhos quando eles estão à direita:

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


Método que rola para baixo a fileira de cabeçalhos quando eles estão à direita:

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

A lógica de todos os métodos para rolar as linhas de cabeçalho está totalmente descrita no código dos métodos. Todos eles são idênticos entre si e diferem apenas ligeiramente em relação aos cálculos de deslocamento e área visível. Espero que os métodos não precisem de explicações adicionais. Em qualquer caso, todas as perguntas podem ser colocadas na discussão do artigo.

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

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

Agora processamos cada evento, quer seja ele a seleção um cabeçalho de guia parcialmente oculto ou o clique no botão para rolar a fileira de cabeçalhos, usando os métodos mencionados acima. Em geral, dependendo da localização da fileira de cabeçalhos e do evento de clique no botão/cabeçalho, chamamos o método apropriado para rolar a fileira de cabeçalhos.

Na classe-coleção de elementos gráficos no arquivo \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh, em seu manipulador de eventos, agora precisamos processar corretamente os eventos recebidos dos objetos WinForms. Em outras palavras, devemos obter três nomes a partir do parâmetro de string sparam e encontrar o objeto base, e temos que encontrar a partir deste último aquele que gerou o evento. Depois de encontrá-lo, e se este objeto pertencer ao controle TabControl, então chamamos o manipulador de eventos do controle TabControl, enviando o identificador do evento para ele.

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

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

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

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

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

Aqui também toda a lógica está completamente descrita no código, e espero que não precise de explicações adicionais.

Agora está tudo pronto para testar o que fizemos aqui hoje.


Teste

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

A única diferença entre este Expert Advisor e a versão anterior é que iremos criar 15 guias no TabControl:

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

Dava para deixar como estava, quer dizer, 11 guias, mas para testar a performance e procurar alguns "bugs" aumentei o número de abas. É por isso que esse número é apenas o resultado da depuração e solução de problemas ao mover o cabeçalho selecionado para fora do contêiner em ambos os lados.

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


Como podemos ver, tudo o que queríamos fazer hoje funciona como planejado.

Existem duas falhas: se passarmos o mouse sobre a área do cabeçalho da guia que está oculta, o cabeçalho reage mudando de cor, como se estivesse visível neste local. Esta é a razão pela qual a área ativa de um elemento não muda de tamanho quando a área visível é redimensionada. Para corrigi-lo, precisaremos calcular e redimensionar a área ativa de acordo com a visível.

O segundo defeito é que se movermos o cabeçalho selecionado para fora do contêiner e mover o painel, dois pixels do cabeçalho oculto serão exibidos. Isso tem a ver com o dimensionamento da guia para o cálculo da área visível, pois o tamanho de cada lado - do cabeçalho selecionado - aumenta dois pixels. Para corrigir isso, precisamos pensar em como, dentro do objeto-cabeçalho da guia, obter o tamanho do cabeçalho adjacente de acordo com o qual o tamanho da área de visibilidade é calculado.

Trataremos disso em artigos posteriores junto com o desenvolvimento de um novo objeto WinForms.


O que virá a seguir?

No próximo artigo, começaremos a desenvolver o objeto WinForms SplitContainer.

Todos os arquivos da versão atual da biblioteca, os arquivos do EA de teste e o indicador do controle de eventos de 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 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
DoEasy. Controles (Parte 18): Preparando a funcionalidade para rolagem de guias no TabControl

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

Arquivos anexados |
MQL5.zip (4458.6 KB)
Redes neurais de maneira fácil (Parte 30): Algoritmos genéticos Redes neurais de maneira fácil (Parte 30): Algoritmos genéticos
Hoje quero apresentar-lhes um método de aprendizado um pouco diferente. Pode-se dizer que é emprestado da teoria da evolução de Darwin. É provavelmente menos controlável do que os métodos discutidos anteriormente. Mas, mesmo assim, permite também treinar modelos indiferenciados.
Aprendendo a construindo um Expert Advisor que opera de forma automática (Parte 10): Automação (II) Aprendendo a construindo um Expert Advisor que opera de forma automática (Parte 10): Automação (II)
Automação não é nada sem que você consiga controlar o horário. Nenhum trabalhador consegue ser eficiente trabalhando 24 horas. No entanto, muitos acreditam que um sistema automático deva trabalhar 24 horas. Mas é sempre bom que você tenha meios de configurar um range de horário para o Expert Advisor. Neste artigo iremos tratar disto. Como adicionar adequadamente uma faixa de horário.
DoEasy. Controles (Parte 20): Objeto WinForms SplitContainer DoEasy. Controles (Parte 20): Objeto WinForms SplitContainer
Hoje começaremos a desenvolver o controle SplitContainer a partir da caixa de ferramentas do MS Visual Studio. Este elemento consiste em dois painéis separados por um separador móvel vertical ou horizontal.
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.