Русский Deutsch
preview
DoEasy. Controls (Part 18): Functionality for scrolling tabs in TabControl

DoEasy. Controls (Part 18): Functionality for scrolling tabs in TabControl

MetaTrader 5Examples | 3 November 2022, 10:56
3 450 0
Artyom Trishkin
Artyom Trishkin

Contents


Concept

In this article, I will continue my work on TabControl WinForms object. If we have a look at the control operation in MS Visual Studio, we can see the following behavior: if the string of headers arranged in one row does not fit the control width, the headers are cropped by its edges. If we select a header cropped by the right edge of the control, then the entire header bar is shifted to the left so that the selected header fits within the width of the container, i.e. on the left side, the first previously visible header goes beyond the left edge of the control and the header following it becomes visible. The scrolling of the header bar works in the same way when clicking on its offset buttons, which appear if the header bar does not fit the size of the control.

In the current article, I will implement the same behavior for TabControl object. I am not going to add scrolling the object with buttons yet limiting myself only to shifting the header bar to the left when selecting the header cropped to the right only if the headers are located above or below the control. This will be a preliminary test of such functionality. In the next article, I will create full-fledged methods based on it to scroll all the headers on each side of the control both when selecting a cropped title, and when scrolling the header bar with control buttons.

In the current article, I will place these buttons in the necessary positions in case the header bar does not fit within the control. In this case, if the headers are placed at the top or bottom, the scroll control buttons are located at the top right or bottom right, respectively. When the headers are on the left, the scroll control buttons are located at the top left, and when the headers are on the right, the buttons are located at the bottom right. The header bar scroll control buttons will be indented one pixel from where the tab box is placed, and tab headers will be cropped by the borders of these buttons (instead of the container border) with an indent of one pixel as well. Thus, these controls will be placed according to their arrangement in TabControl of MS Visual Studio.


Improving library classes

In \MQL5\Include\DoEasy\Data.mqh, add the new message index:

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

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

and the message text corresponding to the newly added index:

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


When clicking on controls, we cannot always handle this event in the same class. There may be situations when we need to call the functionality from other classes that are not available in the class with the mouse event handler for a clicked object. Here, the proposed solution is as follows - when the control is clicked, we will send an event to the chart of the control program, and the library will process this event by sending it to the class where the functionality for processing this event is located.

This is how some internal event handlers will be implemented. But in addition to the needs of the library, we will still need to send some events to the control program so that we can process them from it. Thus, in any case, we need to use the event model so as not to arrange the processing of events of graphical elements using the timer.

In \MQL5\Include\DoEasy\Defines.mqh, add the enumeration of possible events of the library WinForms objects:

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

So far the enumeration features only two events: clicking on the control and selecting the tab in TabControl. I will use the latter for handling the click on the cropped tab header for arranging scrolling the tab header row.

Previously, I have created auxiliary controls that are not independent WinForms objects but are used to create other controls. All of them have been placed in the shared folder of WinForms objects. As more and more of them start to appear, I will create a separate folder named "Helpers" for them in \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ and move all files of auxiliary WinForms objects there, such as ArrowButton.mqh, ArrowDownButton.mqh, ArrowLeftButton.mqh, ArrowLeftRightBox.mqh, ArrowRightButton.mqh, ArrowUpButton.mqh, ArrowUpDownBox.mqh, ListBoxItem.mqh, TabField.mqh and TabHeader.mqh.

Since now the files of auxiliary objects have a new path, we need to correct the path strings of the included files in some library files.

In \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ListBoxItem.mqh, edit the path:

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


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

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


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

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


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

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


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

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


In \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Panel.mqh, adjust the inclusion strings of the files that are now located in the new folder:

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

and change the CreateNewGObject() method — in 'switch', simply re-arrange all the strings in its cases into one. This will make the method smaller and easier to read:

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

Previously, the method looked like this:

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

Such formatting prevented from evaluating the entire method at once, which is less convenient for comparing the same methods in other classes, in which I am going to change it in the same way for clarity of comparison. What is all this for? I see that this method is the same in different container classes and, accordingly, it needs to be organized so that it is the only one for all classes where it exists. At the same time, all classes of objects created in it remain accessible from its location. I will consider this in subsequent articles.


In \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\TabField.mqh, adjust the inclusion string of the panel object class file:

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

and change the the formatting of the CreateNewGObject() method:

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


In \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\GroupBox.mqh, change formatting as well:

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

After such changes in all container class files, the methods became more readable, and now they can all be compared with each other at a glance. They are the same. This means that we will need to create one common method or function and call it in these methods of all container classes.


When clicking on a tab header which is not completely outside the container and is partially cropped, I will need to move the header bar so that the header is fully visible. The offset of the header bar occurs in the TabControl class, which is not visible in the tab header object class because the tabs are located inside the TabControl object. Therefore, we will need to send an event message that will record the index of the header that was clicked and the names of the objects, within which the event occurred. We need the names of both the TabControl object, and the object it is attached to. These will be the names of the main object and the TabControl object. Knowing these names, we can accurately identify the main object all the others are attached to, including TabControl (whose name will already be known to us), in the library event handler. This will allow us to select the object if several TabControl objects are attached to the main object.

All objects of the same type are now built in the library with different names, regardless of what chart they are created on. Therefore, to search for an object, we only need to know its name. All methods for selecting an object by name, currently available in the library, also require the ID of the chart they were created on. Since the chart ID is not required to select an object by its name (this is only true for this library, since the client terminal allows creating objects with the same names, but on different charts), and we still do not have such a method, we should create it.

In \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh, declare a method that returns a graphical element by its name:

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

and write its implementation beyond the class body:

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

Here we first check that the name of the object being searched for contains the name of the program and, if it is not present in the name passed to the method, then we add the name of the program to the name of the object we are looking for. Next, get a list of graphical elements bound to the control and having the desired name (there should be only one such object, if found). Finally, return the pointer to the only object in the resulting list. If the list is empty or not created, then the method returns NULL.

In the tab header object class, we need to know the size of the header bar scroll controls created in the TabControl object class. This is necessary in order to correctly crop the headers in case the controls are visible. Again, header objects do not know anything about the objects placed in the object of the class they were created from. But we can transfer all the sizes we need from this object to the header objects, and then the headers will know about "certain" sizes that should be taken into account when cropping the invisible area. Besides, in order to understand when these sizes should be taken into account and when not, the visibility flag of the scroll controls for the header rows should be sent to header objects.

Thus, although the headers do not know anything about these objects, there will be variables inside the classes of header objects that store all the necessary information. We will write this information to the header objects from the TabControl object class. Thus, we will emulate the visibility of the required parameters in an object that does not know anything about the object but uses its parameters for work.

In \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\TabHeader.mqh tab header object class file, edit the file access string and declare the variables for storing the properties of the header row scrolling controls:

//+------------------------------------------------------------------+
//|                                                    TabHeader.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "..\Common Controls\Button.mqh"
//+------------------------------------------------------------------+
//| TabHeader object class of WForms TabControl                      |
//+------------------------------------------------------------------+
class CTabHeader : public CButton
  {
private:
   int               m_width_off;                        // Object width in the released state
   int               m_height_off;                       // Object height in the released state
   int               m_width_on;                         // Object width in the selected state
   int               m_height_on;                        // Object height in the selected state
   bool              m_arr_butt_ud_visible_flag;         // Tab header "up-down" control buttons visibility flag 
   bool              m_arr_butt_lr_visible_flag;         // Tab header "left-right" control buttons visibility flag
   int               m_arr_butt_ud_size;                 // Tab header "up-down" control buttons size 
   int               m_arr_butt_lr_size;                 // Tab header "left-right" control buttons size
//--- Adjust the size and location of the element depending on the state


In the public section of the class, write the methods for working with declared private variables:

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

The actual data to all these variables will be sent in the TabControl object class. Here they will be used to calculate the size of the visible area and crop the part of the image that goes beyond the visible area.

In order to use the values of the new variables in the calculations of the visible area, we need to override the Crop() virtual method of the parent object.

Declare the method in the public scope of the class:

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

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

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

Let's write its implementation outside the class body:

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

Here, unlike the original method, we have additional sizes to be considered when calculating the visibility scope. If it is stated in the flag variables that the scroll control object is visible, then the size of the visible scroll control should be used for the calculation. If the variable states that the object is not visible, then we use zero instead of its size. Left-right and up-down arrow buttons objects use their own variables and, accordingly, separate calculations of additional sizes for calculating the visibility area.

Initialize new variables with default values in the class constructors:

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


In the "The cursor is inside the active area, the left mouse button is clicked" event, implement the code block to create and send the TabControl tab selection event when the header is clicked:

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

Here we create an event that will be sent to the library event handler, and in this message we indicate the number of the header line in the 'long' parameter of the message (for one line, it will always be zero), the index of the header column in the row is specified in the 'double' parameter, which will definitely indicate which title was clicked (the index of the selected tab). To uniquely identify the TabControl the tab was selected in, we need to send in an event the name of the main object, in which the event occurred, and the name of the TabControl, in which the tab was selected. Since we have only one string parameter, we simply add the names of the main object and the TabControl object using ";" as a separator. In the event handler, we can then split the string by the separator and get both names of these objects.


Now let's refine the class of the TabControl WinForms object in \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\TabControl.mqh.

Correct the paths to the auxiliary object files, declare the new variables and methods and implement the methods returning the pointers to the arrow button objects:

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

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


In the public section of the class, implement the methods for handling new variables and arrow button objects, and declare the method for shifting the header row, as well as the event handler:

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

...

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

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


In the class constructor, set default visibility values for arrow objects:

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

Both of these objects should be hidden by default, and will only be shown if either the horizontal or vertical header bar extends beyond the container. Using the same flags, we will determine the need to add additional values to the visibility area in the tab header objects. If there is no arrow object, then the headers will be cropped along the edge of the container. If the arrow object is present, then they are cropped along the edge of the object so that the button object with arrows does not overlap on top of the header cropped along the edge of the container.


In the method that creates the specified number of tabs, adjust the Y coordinate of placing the headers and the height of tab fields when placed at the bottom of the container and add the code block for creating two "left-right" and "up-down" button objects:

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

//--- In the loop by the number of tabs
   CTabHeader *header=NULL;
   CTabField  *field=NULL;
   for(int i=0;i<total;i++)
     {
      //--- Depending on the location of tab titles, set their initial coordinates
      int header_x=2;
      int header_y=2;
      int header_w=w;
      int header_h=h;
      
      //--- Set the current X and Y coordinate depending on the location of the tab headers
      switch(this.Alignment())
        {
         case CANV_ELEMENT_ALIGNMENT_TOP     :
           header_w=w;
           header_h=h;
           header_x=(header==NULL ? 2 : header.RightEdgeRelative());
           header_y=2;
           break;
         case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
           header_w=w;
           header_h=h;
           header_x=(header==NULL ? 2 : header.RightEdgeRelative());
           header_y=this.Height()-header_h-2;
           break;
         case CANV_ELEMENT_ALIGNMENT_LEFT    :
           header_w=h;
           header_h=w;
           header_x=2;
           header_y=(header==NULL ? this.Height()-header_h-2 : header.CoordYRelative()-header_h);
           break;
         case CANV_ELEMENT_ALIGNMENT_RIGHT   :
           header_w=h;
           header_h=w;
           header_x=this.Width()-header_w-2;
           header_y=(header==NULL ? 2 : header.BottomEdgeRelative());
           break;
         default:
           break;
        }
      //--- Create the TabHeader object
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,header_x,header_y,header_w,header_h,clrNONE,255,this.Active(),false))
        {
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER),string(i+1));
         return false;
        }
      header=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,i);
      if(header==NULL)
        {
         ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER),string(i+1));
         return false;
        }
      header.SetBase(this.GetObject());
      header.SetPageNumber(i);
      header.SetGroup(this.Group()+1);
      header.SetBackgroundColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR,true);
      header.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_DOWN);
      header.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_OVER);
      header.SetBackgroundStateOnColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR_ON,true);
      header.SetBackgroundStateOnColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BACK_DOWN_ON);
      header.SetBackgroundStateOnColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BACK_OVER_ON);
      header.SetBorderStyle(FRAME_STYLE_SIMPLE);
      header.SetBorderColor(CLR_DEF_CONTROL_TAB_HEAD_BORDER_COLOR,true);
      header.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_DOWN);
      header.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_OVER);
      header.SetAlignment(this.Alignment());
      header.SetPadding(this.HeaderPaddingWidth(),this.HeaderPaddingHeight(),this.HeaderPaddingWidth(),this.HeaderPaddingHeight());
      if(header_text!="" && header_text!=NULL)
         this.SetHeaderText(header,header_text+string(i+1));
      else
         this.SetHeaderText(header,"TabPage"+string(i+1));
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT)
         header.SetFontAngle(90);
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_RIGHT)
         header.SetFontAngle(270);
      header.SetTabSizeMode(this.TabSizeMode());
      
      //--- Save the initial height of the header and set its size in accordance with the header size setting mode
      int h_prev=header_h;
      header.SetSizes(header_w,header_h);
      //--- Get the Y offset of the header position after changing its height and
      //--- shift it by the calculated value only for headers on the left
      int y_shift=header.Height()-h_prev;
      if(header.Move(header.CoordX(),header.CoordY()-(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT ? y_shift : 0)))
        {
         header.SetCoordXRelative(header.CoordX()-this.CoordX());
         header.SetCoordYRelative(header.CoordY()-this.CoordY());
        }
      header.SetVisibleFlag(this.IsVisible(),false);

      //--- Depending on the location of the tab headers, set the initial coordinates of the tab fields
      int field_x=0;
      int field_y=0;
      int field_w=this.Width();
      int field_h=this.Height()-header.Height()-2;
      int header_shift=0;
      
      switch(this.Alignment())
        {
         case CANV_ELEMENT_ALIGNMENT_TOP     :
           field_x=0;
           field_y=header.BottomEdgeRelative();
           field_w=this.Width();
           field_h=this.Height()-header.Height()-2;
           break;
         case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
           field_x=0;
           field_y=0;
           field_w=this.Width();
           field_h=this.Height()-header.Height()-2;
           break;
         case CANV_ELEMENT_ALIGNMENT_LEFT    :
           field_x=header.RightEdgeRelative();
           field_y=0;
           field_h=this.Height();
           field_w=this.Width()-header.Width()-2;
           break;
         case CANV_ELEMENT_ALIGNMENT_RIGHT   :
           field_x=0;
           field_y=0;
           field_h=this.Height();
           field_w=this.Width()-header.Width()-2;
           break;
         default:
           break;
        }
      
      //--- Create the TabField object (tab field)
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,field_x,field_y,field_w,field_h,clrNONE,255,true,false))
        {
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD),string(i+1));
         return false;
        }
      field=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,i);
      if(field==NULL)
        {
         ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD),string(i+1));
         return false;
        }
      field.SetBase(this.GetObject());
      field.SetPageNumber(i);
      field.SetGroup(this.Group()+1);
      field.SetBorderSizeAll(1);
      field.SetBorderStyle(FRAME_STYLE_SIMPLE);
      field.SetOpacity(CLR_DEF_CONTROL_TAB_PAGE_OPACITY,true);
      field.SetBackgroundColor(CLR_DEF_CONTROL_TAB_PAGE_BACK_COLOR,true);
      field.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_DOWN);
      field.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_OVER);
      field.SetBorderColor(CLR_DEF_CONTROL_TAB_PAGE_BORDER_COLOR,true);
      field.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_DOWN);
      field.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_OVER);
      field.SetForeColor(CLR_DEF_FORE_COLOR,true);
      field.SetPadding(this.FieldPaddingLeft(),this.FieldPaddingTop(),this.FieldPaddingRight(),this.FieldPaddingBottom());
      field.Hide();
     }
//--- Create left-right and up-down buttons
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX,this.Width()-32,0,15,15,clrNONE,255,this.Active(),false);
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX,0,this.Height()-32,15,15,clrNONE,255,this.Active(),false);
   CArrowUpDownBox *box_ud=this.GetArrUpDownBox();
   if(box_ud!=NULL)
     {
      this.SetVisibleUpDownBox(false);
      this.SetSizeUpDownBox(box_ud.Height());
      box_ud.SetBorderStyle(FRAME_STYLE_NONE);
      box_ud.SetBackgroundColor(CLR_CANV_NULL,true);
      box_ud.SetOpacity(0);
      box_ud.Hide();
     }
   CArrowLeftRightBox *box_lr=this.GetArrLeftRightBox();
   if(box_lr!=NULL)
     {
      this.SetVisibleLeftRightBox(false);
      this.SetSizeLeftRightBox(box_lr.Width());
      box_lr.SetBorderStyle(FRAME_STYLE_NONE);
      box_lr.SetBackgroundColor(CLR_CANV_NULL,true);
      box_lr.SetOpacity(0);
      box_lr.Hide();
     }
//--- Arrange all titles in accordance with the specified display modes and select the specified tab
   this.ArrangeTabHeaders();
   this.Select(selected_page,true);
   return true;
  }
//+------------------------------------------------------------------+

When headers are placed at the bottom, their Y coordinate should be two pixels higher than the bottom edge of the container, since the title of the selected tab increases in size by two pixels and, if it is placed along the bottom edge of the container, then when it is selected and, accordingly, its height is increased by two pixels, its bottom edge goes outside the container and is cropped there.

To prevent cropping, the object should be placed two pixels higher taking into account its possible increase when it is selected. Accordingly, the tab field should be two pixels smaller, since these two pixels are now "eaten" by the upward shift of the header location coordinate.

After creating two button objects with arrows, set them to the "hidden" state (in this case, these states are transferred to the tab header objects) and set the sizes of the created objects to all tab header objects. All this is done using two methods I will consider a bit later. For objects, the frame type is set to "none". Also, we set the transparent background color and full opacity. At the end, the object is hidden.

Thus, both of these objects are created as two buttons on a transparent background to match the appearance of the corresponding objects in the MS Visual Studio control.

In all methods placing tab headers at the top, bottom, left and right, add a row and column index setting for the header, as well as the code block to handle the situation when we have tab headers in one string enabled, as well as in case the tab header goes outside the container:

//+------------------------------------------------------------------+
//| Arrange tab headers on top                                       |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersTop(void)
  {
//--- Get the list of tab headers
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Declare the variables
   int col=0;                                // Column
   int row=0;                                // Row
   int x1_base=2;                            // Initial X coordinate
   int x2_base=this.Width()-2;               // Final X coordinate
   int x_shift=0;                            // Shift the tab set for calculating their exit beyond the container
   int n=0;                                  // The variable for calculating the column index relative to the loop index
//--- In a loop by the list of headers,
   for(int i=0;i<list.Total();i++)
     {
      //--- get the next tab header object
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- If the flag for positioning headers in several rows is set
      if(this.Multiline())
        {
         //--- Calculate the value of the right edge of the header, taking into account that
         //--- the origin always comes from the left edge of TabControl + 2 pixels
         int x2=header.RightEdgeRelative()-x_shift;
         //--- If the calculated value does not go beyond the right edge of the TabControl minus 2 pixels, 
         //--- set the column number equal to the loop index minus the value in the n variable
         if(x2<x2_base)
            col=i-n;
         //--- If the calculated value goes beyond the right edge of the TabControl minus 2 pixels,
         else
           {
            //--- Increase the row index, calculate the new shift (so that the next object is compared with the TabControl left edge + 2 pixels),
            //--- set the loop index for the n variable, while the column index is set to zero, this is the start of the new row
            row++;
            x_shift=header.CoordXRelative()-2;
            n=i;
            col=0;
           }
         //--- Assign the row and column indices to the tab header and shift it to the calculated coordinates
         header.SetTabLocation(row,col);
         if(header.Move(header.CoordX()-x_shift,header.CoordY()-header.Row()*header.Height()))
           {
            header.SetCoordXRelative(header.CoordX()-this.CoordX());
            header.SetCoordYRelative(header.CoordY()-this.CoordY());
           }
        }
      //--- If only one row of headers is allowed
      else
        {
         header.SetRow(0);
         header.SetColumn(i);
        }
     }

//--- The location of all tab titles is set. Now place them all together with the fields
//--- according to the header row and column indices.

//--- Get the last title in the list
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
//--- If the object is received
   if(last!=NULL)
     {
      //--- If the mode of stretching headers to the width of the container is set, call the stretching method
      if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL)
         this.StretchHeaders();
      //--- If this is not the first row (with index 0)
      if(last.Row()>0)
        {
         //--- Calculate the offset of the tab field Y coordinate
         int y_shift=last.Row()*last.Height();
         //--- In a loop by the list of headers,
         for(int i=0;i<list.Total();i++)
           {
            //--- get the next object
            CTabHeader *header=list.At(i);
            if(header==NULL)
               continue;
            //--- get the tab field corresponding to the received header
            CTabField  *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- shift the tab header by the calculated row coordinates
            if(header.Move(header.CoordX(),header.CoordY()+y_shift))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
            //--- shift the tab field by the calculated shift
            if(field.Move(field.CoordX(),field.CoordY()+y_shift))
              {
               field.SetCoordXRelative(field.CoordX()-this.CoordX());
               field.SetCoordYRelative(field.CoordY()-this.CoordY());
               //--- change the size of the shifted field by the value of its shift
               field.Resize(field.Width(),field.Height()-y_shift,false);
              }
           }
        }
      //--- If this is the first and only string
      else if(!this.Multiline())
        {
         //--- If the right edge of the header goes beyond the right edge of the container area,
         if(last.RightEdge()>this.RightEdgeWorkspace())
           {
            //--- get the button object with left-right arrows
            CArrowLeftRightBox *arr_box=this.GetArrLeftRightBox();
            if(arr_box!=NULL)
              {
               //--- Calculate object location coordinates
               int x=this.RightEdgeWorkspace()-arr_box.Width()+1;
               int y=last.BottomEdge()-arr_box.Height();
               //--- If the object is shifted by the specified coordinates,
               if(arr_box.Move(x,y))
                 {
                  //--- set its relative coordinates
                  arr_box.SetCoordXRelative(arr_box.CoordX()-this.CoordX());
                  arr_box.SetCoordYRelative(arr_box.CoordY()-this.CoordY());
                  //--- set the visibility flag for the object
                  this.SetVisibleLeftRightBox(true);
                  //--- If the control is visible, display the buttons and bring them to the foreground
                  if(this.IsVisible())
                    {
                     arr_box.Show();
                     arr_box.BringToTop();
                    }
                 }
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Here is the full method for placing tab headers on top. Other three methods feature the same setting of the row (zero) and column (loop index) index for the tab header, while the code blocks, defining the header that goes beyond the container and displaying the scroll control buttons for the row, are slightly different in calculating the location of headers relative to the container and displaying the scroll control buttons in the corresponding coordinates.

Let's take a look at these blocks of code for different methods.

For the method that places tab headers at the bottom (ArrangeTabHeadersBottom()):

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


For the method that places tab headers on the left (ArrangeTabHeadersLeft()):

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


For the method that positions tab headers to the right (ArrangeTabHeadersRight()):

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

Comparing all four blocks of code in all four methods, we can see how the coordinates of the scroll control buttons are calculated and the headers that extend beyond the container are defined.

The method that creates a new graphical object has also undergone changes in the formatting of 'switch' statement cases:

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

Now everything is in one line — for a more compact display of the method logic.

In the virtual method that sets the object above all, replace the call to the BringToTop() method of the form object

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

with calling the Show() method of the graphical element object. If the object itself is visible, display the control objects for scrolling the header bars in case these objects should be visible:

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

When I called the BringToTop method of the CForm parent class, it brought absolutely all objects bound to the control to the foreground, making them visible. Now we need to control the visibility of the scroll control button objects. Therefore, we simply make the object itself visible, and then, inside the method, we check the need to display the headers and fields of the tabs and the buttons that control the scrolling of the header rows.

The method that displays "up-down buttons" controls:

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

Get the pointer to the control. If failed to get the pointer, display the error message and exit the method. Upon successful receipt of the pointer, display the object.


The method displaying "Right-left button" controls:

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

The logic of the method is identical to the above.


The methods that hide the "Up-down buttons" and "Right-left buttons" controls:

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

The logic of the methods is identical to the logic of the two methods discussed above for displaying controls. But here, instead of being displayed, the element is hidden.


The methods that bring the "Up-down buttons" and "Right-left buttons" controls to the foreground:

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

Everything is exactly the same except for moving to the foreground.


The method that sets the visibility of the left-right buttons:

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

First, set the control visibility flag, next get the list of tab headers and, in the loop by the list for each of the header objects, set the flag value passed to the method.


The method setting the visibility of up-down butttons:

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

The logic of the method is identical to the above. The flags for up-down button objects are set here.


The method setting the size of left-right buttons:

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

Get the pointer to the left-right button object, set the value passed to the method as an object width (since we only need to change the object width here). Next, in the loop by the list of tab headers, set the value of a specified width of scroll control button objects to each subsequent object.


The method setting the size of up-down buttons:

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

The logic of the method is identical to the above. But here we get the pointer to the up-down button object and set the object height.


The method that shifts the header row:

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

The logic of the method is described line by line in the comments to the code. In short, the index of the clicked header (selecting the tab of the TabControl object) is passed to the method. If the selected tab, namely its header, is outside the container, i.e., is cropped, then we need to shift all headers to the left so that the selected header becomes fully visible. The very first header visible on the left goes beyond the left edge, and the next one takes its place.

Therefore, first we need to find the very first visible header on the left and find out its width (each of the headers can have its own width if the sizes are set according to the header text). The width of the found first header will be the amount, by which the entire header row should be shifted to the left. After shifting the row, we find the field for the selected header and redraw its frame, since the field frame is drawn relative to the location of the header. Now that it has been shifted, the frame will not be drawn correctly.

This method is only suitable for shifting a row of horizontal headers to the left. Based on this method, in the next article, I will create the methods for shifting the header bar in all directions - left-right and up-down.


The event handler:

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

First, we call the Y offset adjustment method for the subwindow of the graphical object class (this is valid for every graphical element on canvas, but here the parent class event handler is overridden, so we should not forget to call the coordinate adjustment method). Then we call the tab header bar offset method discussed above.


Let's improve the collection class of graphical elements in \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh.

Add the method returning the graphical element by name:

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

Since now all the names of all graphical elements on canvas are unique, there is no need to specify the ID of the chart the object is built on when searching for an object by name. Therefore, it makes sense to add a method that returns an object by its name only. Here we check what name is passed to the method and, if the name of the program is missing in the name string, then the program name is added to the beginning of this name so that the name of the searched object matches the real name of the graphical element. After all, they all contain a substring with the name of the program. This is how the library creates the names of graphical element objects.


WinForms objects are now able to send messages about their events to the control program chart. These events are intercepted by the library and should be able to handle them and redirect the required event message to the required class. So let's add handling WinForms object event codes to the event handler of the graphical element collection class:

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

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

Only one event code is handled here so far — selecting a tab in TabControl. Split the message passed in 'sparam' string parameter in two by ";" using the StringSplit() function. As a result, we get two names in the array — the name of the main object (in this case, the panel object — main) and the TabControl WinForms object — base attached to the panel. In the lparam and dparam parameters, we pass the row and column index of the selected tab header. With all this data, we can now accurately identify the panel TabControl is attached to and the control tab whose header was clicked. After receiving pointers to all these objects, we can call the event handler of the received object, which in turn will call the offset method of the tab header bar.

Currently, these are all the required changes and improvements.


Test

To perform the test, I will use the EA from the previous article and save it in \MQL5\Experts\TestDoEasy\Part118\ as TestDoEasy118.mq5.

Let's create one main panel and place TabControl with 11 tabs on it. On each of the tabs, we will create a text label with a description of this tab, so that during the test we can see which tab is actually displayed.

After launching the Expert Advisor, specify its settings to stretch the tab headers to fit the width of the control element. This way it will be clear which tab does not fit and goes beyond the edge of the container. Let's check the location of the tabs on each side of the control — how the scroll control buttons for the header bar are placed. If the headers are on top, click on the right-cut tab headers and see how the entire row shifts to the left making the selected header visible and replicating the behavior of TabControl in MS Visual Studio.

In the EA's OnInit() handler, create a panel featuring TabControl with 11 tabs having text labels:

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

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

The panels are created in a loop (for now, there is only one panel), because it turned out that if you create several panels with TabControl controls, these controls do not work correctly. To fix that, I will create the required number of panels.

Let's compile the EA and run it on the chart after setting the required settings:


As we can see, the declared functionality works correctly.


What's next?

In the next article, I will create methods for scrolling tab headers in all directions using the scroll control buttons.

All files of the current library version, test EA and chart event control indicator for MQL5 are attached below for you to test and download. Leave your questions, comments and suggestions in the comments.

Back to contents

*Previous articles within the series:

DoEasy. Controls (Part 10): WinForms objects — Animating the interface
DoEasy. Controls (Part 11): WinForms objects — groups, CheckedListBox WinForms object
DoEasy. Controls (Part 12): Base list object, ListBox and ButtonListBox WinForms objects
DoEasy. Controls (Part 13): Optimizing interaction of WinForms objects with the mouse, starting the development of the TabControl WinForms object
DoEasy. Controls (Part 14): New algorithm for naming graphical elements. Continuing work on the TabControl WinForms object
DoEasy. Controls (Part 15): TabControl WinForms object — several rows of tab headers, tab handling methods
DoEasy. Controls (Part 16): TabControl WinForms object — several rows of tab headers, stretching headers to fit the container
DoEasy. Controls (Part 17): Cropping invisible object parts, auxiliary arrow buttons WinForms objects

Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/11454

Attached files |
MQL5.zip (4454.87 KB)
DoEasy. Controls (Part 19): Scrolling tabs in TabControl, WinForms object events DoEasy. Controls (Part 19): Scrolling tabs in TabControl, WinForms object events
In this article, I will create the functionality for scrolling tab headers in TabControl using scrolling buttons. The functionality is meant to place tab headers into a single line from either side of the control.
Neural networks made easy (Part 25): Practicing Transfer Learning Neural networks made easy (Part 25): Practicing Transfer Learning
In the last two articles, we developed a tool for creating and editing neural network models. Now it is time to evaluate the potential use of Transfer Learning technology using practical examples.
Neural networks made easy (Part 26): Reinforcement Learning Neural networks made easy (Part 26): Reinforcement Learning
We continue to study machine learning methods. With this article, we begin another big topic, Reinforcement Learning. This approach allows the models to set up certain strategies for solving the problems. We can expect that this property of reinforcement learning will open up new horizons for building trading strategies.
Developing a trading Expert Advisor from scratch (Part 28): Towards the future (III) Developing a trading Expert Advisor from scratch (Part 28): Towards the future (III)
There is still one task which our order system is not up to, but we will FINALLY figure it out. The MetaTrader 5 provides a system of tickets which allows creating and correcting order values. The idea is to have an Expert Advisor that would make the same ticket system faster and more efficient.