English 中文 Español Deutsch 日本語 Português
preview
DoEasy. Элементы управления (Часть 18): Готовим функционал для прокрутки вкладок в TabControl

DoEasy. Элементы управления (Часть 18): Готовим функционал для прокрутки вкладок в TabControl

MetaTrader 5Примеры | 16 сентября 2022, 15:01
971 2
Artyom Trishkin
Artyom Trishkin

Содержание


Концепция

Продолжаем работу над WinForms-объектом TabControl. Если поглядеть на работу этого элемента управления в MS Visual Studio, то мы увидим такое поведение: если строка заголовков, расположенных в один ряд, не умещается по, например, ширине элемента управления, то заголовки обрезаются по его краям. Если выбрать заголовок, обрезанный по правому краю элемента управления, то вся строка заголовков смещается левее так, чтобы выбранный заголовок уместился в пределах ширины контейнера — т.е., с левой стороны первый ранее видимый заголовок уходит за левый край элемента управления, и видимым становится следующий за ним заголовок. Точно так же работает прокрутка строки заголовков при щелчке по кнопкам его смещения, которые появляются в случае, если строка заголовков не умещается по размеру элемента управления.

Сегодня сделаем такое поведение и для нашего WinForms-объекта TabControl. Но его прокрутку кнопками сегодня делать не будем, а ограничимся только смещением строки заголовков левее при выборе обрезанного справа заголовка. И только, если заголовки размещаются сверху или снизу элемента управления. Это будет предварительным тестированием такого функционала, а в следующей статье создадим на его основе полноценные методы прокрутки всех заголовков с каждой из сторон элемента управления как при выборе обрезанного заголовка, так и при прокрутке строки заголовков кнопками управления.

К слову, эти кнопки мы сегодня разместим в нужных позициях в случае, если они нужны. А нужны они будут тогда, когда строка заголовков не умещается в пределах элемента управления. При этом, если заголовки размещены сверху или снизу, то кнопки управления прокруткой располагаются справа-вверху или справа-внизу соответственно. При расположении заголовков слева, кнопки управления прокруткой располагаются слева-вверху, а при расположении заголовков справа — кнопки располагаются справа-внизу. Кнопки управления прокруткой строки заголовка будут размещаться с отступом на один пиксель от места размещения поля вкладки, а заголовки вкладок будут обрезаться по границам этих кнопок (а не по границе контейнера) тоже с отступом в один пиксель. Таким образом, эти элементы управления будут размещены в соответствии с их размещением в элементе управления TabControl в MS Visual Studio.


Доработка классов библиотеки


В файле \MQL5\Include\DoEasy\Data.mqh впишем индекс нового сообщения:

//--- CDataPropObj
   MSG_DATA_PROP_OBJ_OUT_OF_PROP_RANGE,               // Переданное свойство находится за пределами диапазона свойств объекта
   MSG_GRAPH_OBJ_FAILED_CREATE_NEW_HIST_OBJ,          // Не удалось создать объект истории изменений графического объекта
   MSG_GRAPH_OBJ_FAILED_ADD_OBJ_TO_HIST_LIST,         // Не удалось добавить объект истории изменений в список
   MSG_GRAPH_OBJ_FAILED_GET_HIST_OBJ,                 // Не удалось получить объект истории изменений
   MSG_GRAPH_OBJ_FAILED_INC_ARRAY_SIZE,               // Не удалось увеличить размер массива

//--- CGraphElementsCollection
   MSG_GRAPH_OBJ_FAILED_GET_ADDED_OBJ_LIST,           // Не удалось получить список вновь добавленных объектов
   MSG_GRAPH_OBJ_FAILED_GET_OBJECT_NAMES,             // Не удалось получить имена объектов
   MSG_GRAPH_OBJ_FAILED_DETACH_OBJ_FROM_LIST,         // Не удалось изъять графический объект из списка
   MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_LIST,         // Не удалось удалить графический объект из списка
   MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART,        // Не удалось удалить графический объект с графика
   MSG_GRAPH_OBJ_FAILED_ADD_OBJ_TO_DEL_LIST,          // Не удалось поместить графический объект в список удалённых объектов
   MSG_GRAPH_OBJ_FAILED_ADD_OBJ_TO_RNM_LIST,          // Не удалось поместить графический объект в список переименованных объектов

и текст сообщения, соответствующий вновь добавленному индексу:

//--- 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"},


При щелчке по элементам управления, мы не всегда можем в том же классе обработать это событие. Могут быть ситуации, когда нам нужно при таком событии вызывать функционал из других классов, недоступных в классе, где написан обработчик события мышки для объекта, по которому был выполнен щелчок. Здесь выход видится таким — при щелчке по элементу управления мы будем отправлять на график управляющей программы событие, а библиотека обработает это событие, отправив его в тот класс, где размещён функционал обработки этого события.

Так будут реализовываться некоторые внутренние обработчики событий, но помимо нужд библиотеки, нам всё равно нужно будет в управляющую программу отсылать некоторые события — чтобы из неё можно было их обрабатывать. Таким образом, нам нужно в любом случае использовать событийную модель, чтобы не организовывать обработку событий графических элементов через таймер.

В файле \MQL5\Include\DoEasy\Defines.mqh напишем перечисление возможных событий WinForms-объектов библиотеки:

//+------------------------------------------------------------------+
//| Список возможных событий элементов управления WinForms           |
//+------------------------------------------------------------------+
enum ENUM_WF_CONTROL_EVENT
  {
   WF_CONTROL_EVENT_NO_EVENT = GRAPH_OBJ_EVENTS_NEXT_CODE,// Нет события
   WF_CONTROL_EVENT_CLICK,                            // Событие "Щелчок по элементу управления"
   WF_CONTROL_EVENT_TAB_SELECT,                       // Событие "Выбор вкладки элемента управления TabControl"
  };
#define WF_CONTROL_EVENTS_NEXT_CODE (WF_CONTROL_EVENT_TAB_SELECT+1)  // Код следующего события после последнего кода события графических элементов
//+------------------------------------------------------------------+
//| Режим автоматического изменения размера элемента интерфейса      |
//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_AUTO_SIZE_MODE
  {
   CANV_ELEMENT_AUTO_SIZE_MODE_GROW,                  // Только увеличение
   CANV_ELEMENT_AUTO_SIZE_MODE_GROW_SHRINK,           // Увеличение и уменьшение
  };
//+------------------------------------------------------------------+

Пока в перечислении есть лишь два события: щелчок по элементу управления и событие выбора вкладки в элементе управления TabControl, которое сегодня будем использовать для обработки щелчка по обрезанному заголовку вкладки для организации прокрутки строки заголовков вкладок.

Ранее мы создавали вспомогательные элементы управления, которые не являются самостоятельными WinForms-объектами, но используются для создания других элементов управления. Все они были размещены в общей папке WinForms-объектов. Так как их начинает появляться всё больше, то создадим для них отдельную папку "Helpers" по пути \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ и перенесём в неё все файлы вспомогательных WinForms-объектов, таких как ArrowButton.mqh, ArrowDownButton.mqh, ArrowLeftButton.mqh, ArrowLeftRightBox.mqh, ArrowRightButton.mqh, ArrowUpButton.mqh, ArrowUpDownBox.mqh, ListBoxItem.mqh, TabField.mqh и TabHeader.mqh.

Так как теперь файлы вспомогательных объектов находятся по новому пути, то нужно исправить строки путей подключаемых файлов в некоторых файлах библиотеки.

В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ListBoxItem.mqh исправим путь:

//+------------------------------------------------------------------+
//|                                                  ListBoxItem.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "..\Common Controls\Button.mqh"
//+------------------------------------------------------------------+
//| Класс объекта Label элементов управления WForms                  |
//+------------------------------------------------------------------+


В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ArrowButton.mqh:

//+------------------------------------------------------------------+
//|                                                  ArrowButton.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "..\Common Controls\Button.mqh"
//+------------------------------------------------------------------+
//| Класс объекта Arrow Button элементов управления WForms           |
//+------------------------------------------------------------------+


В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ArrowLeftRightBox.mqh:

//+------------------------------------------------------------------+
//|                                            ArrowLeftRightBox.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "..\Containers\Panel.mqh"
//+------------------------------------------------------------------+
//| Класс объекта ArrowLeftRightBox элементов управления WForms      |
//+------------------------------------------------------------------+


В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\ArrowUpDownBox.mqh:

//+------------------------------------------------------------------+
//|                                               ArrowUpDownBox.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "..\Containers\Panel.mqh"
//+------------------------------------------------------------------+
//| Класс объекта ArrowUpDownBox элементов управления WForms         |
//+------------------------------------------------------------------+


В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\ElementsListBox.mqh:

//+------------------------------------------------------------------+
//|                                              ElementsListBox.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4 
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "..\Containers\Container.mqh"
#include "..\Helpers\ListBoxItem.mqh"
//+------------------------------------------------------------------+
//| Класс базового объекта списка элементов управления WForms        |
//+------------------------------------------------------------------+


В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Panel.mqh скорректируем строки подключения файлов, теперь находящихся в новой папке:

//+------------------------------------------------------------------+
//|                                                        Panel.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#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 элементов управления WForms                  |
//+------------------------------------------------------------------+

и изменим метод CreateNewGObject() — просто в переключателе switch расположим все строки в его кейсах в одну строку, что сделает метод меньше и удобнее для чтения:

//+------------------------------------------------------------------+
//| Создаёт новый графический объект                                 |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+

Ранее метод выглядел так:

//+------------------------------------------------------------------+
//| Создаёт новый графический объект                                 |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+

... и такое форматирование не давало окинуть взглядом сразу весь метод, что менее удобно для сравнения таких же методов в других классах, в которых мы его точно так же изменим для наглядности сравнения. Для чего всё это? Вижу, что в разных классах-контейнерах этот метод одинаков и, соответственно, нужно его организовать так, чтобы он был единственным для всех классов, где он есть, и при этом все классы создаваемых в нём объектов были доступны из его места расположения. В последующих статьях продумаем этот момент.


В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\TabField.mqh скорректируем строку подключения файла класса объекта-панели:

//+------------------------------------------------------------------+
//|                                                     TabField.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "..\Containers\Panel.mqh"
//+------------------------------------------------------------------+
//| Класс объекта TabHeader элемента управления WForms TabControl    |
//+------------------------------------------------------------------+

и изменим форматирование метода CreateNewGObject():

//+------------------------------------------------------------------+
//| Создаёт новый графический объект                                 |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+


В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\GroupBox.mqh тоже поменяем форматирование:

//+------------------------------------------------------------------+
//| Создаёт новый графический объект                                 |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+

После таких изменений во всех файлах классов-контейнеров, методы стали более удобными для чтения, и теперь их можно все одним взглядом сравнить друг с другом. Они одинаковы. А значит — нужно будет создать один общий метод или функцию и вызывать его в этих методах всех классов-контейнеров.


При щелчке по заголовку вкладки, который не полностью выходит за пределы контейнера и частично обрезан, нам нужно будет сместить строку заголовков так, чтобы этот заголовок стал полностью видимым. Смещение строки заголовков происходит в классе элемента управления TabControl, который не видим в классе объекта заголовка вкладки, так как вкладки расположены внутри объекта TabControl. Поэтому нам нужно будет отправить сообщение о событии, в котором будет записан номер заголовка, по которому был щелчок, и имена объектов, внутри которых произошло событие. Имена должны быть как объекта TabControl, так и того объекта, к которому он прикреплён. Это будут имена главного объекта и объекта TabControl. В обработчике событий библиотеки, зная эти имена, мы сможем однозначно идентифицировать главный объект, к которому прикреплены все остальные, и в их числе и TabControl, имя которого тоже мы будем знать, и выбрать именно его в случае, если к главному объекту прикреплены несколько объектов TabControl.

Все объекты одного типа сейчас строятся в библиотеке с разными именами, независимо от того, на каком графике они создаются. Поэтому для поиска объекта нам достаточно знать только его имя. Все методы для выбора объекта по имени, имеющиеся сейчас в библиотеке, также требуют и идентификатор графика, на котором они созданы. Так как идентификатор графика не требуется для однозначного выбора объекта по его имени (к слову — это справедливо только для данной библиотеки, так как в клиентском терминале допускается создание объектов с одинаковыми именами, но на разных графиках), а такого метода у нас ещё нету, то создадим его.

В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh объявим метод, возвращающий графический элемент по его имени:

public:
//--- Рисует рамку
   virtual void      DrawFrame(void){}
//--- Возвращает по типу (1) список, (2) количество привязанных элементов, привязанный элемент (3) по индексу в списке, (4) по имени
   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);
//--- Очищает элемент с заполнением его цветом и непрозрачностью

и за пределами тела класса напишем его реализацию:

//+------------------------------------------------------------------+
//| Возвращает привязанный элемент по имени                          |
//+------------------------------------------------------------------+
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);
  }
//+------------------------------------------------------------------+

Здесь: сначала проверяем, что в имени искомого объекта присутствует имя программы и, если его нет в переданном в метод имени, то добавляем имя программы к имени искомого объекта. Затем получаем список графических элементов, прикреплённых к элементу управления и имеющих искомое имя (такой объект должен быть всего один, если он найден). В итоге возвращаем указатель на единственный объект в полученном списке. Если же список пустой, или не создан, то метод вернёт NULL.

В классе объекта-заголовка вкладки нам нужно будет знать размеры созданных в классе объекта TabControl элементов управления прокруткой строки заголовков. Нужно это для того, чтобы правильно обрезать заголовки в случае, если элементы управления видимы. Опять же, объекты-заголовки ничего не знают об объектах, размещённых в объекте класса, из которого они были сами созданы. Но мы можем из этого объекта передать в объекты-заголовки все нужные нам размеры, и тогда заголовки будут знать о "неких" размерах, которые необходимо учитывать при обрезании невидимой области, плюс, чтобы понять когда нужно эти размеры учитывать, а когда не нужно, в объекты-заголовки нужно отправлять флаг видимости элементов управления прокруткой строк заголовков.

Такм образом, хоть заголовки ничего не знают про эти объекты, но внутри классов объектов-заголовков будут переменные, хранящие всю нужною информацию. А записывать эту информацию в объекты-заголовки мы будем из класса объекта TabControl. Таким образом мы эмулируем видимость требуемых параметров в объекте, который не знает ничего об объекте, но его параметры использует для своей работы.

В файле класса объекта-заголовка вкладки \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\TabHeader.mqh исправим строку доступа к файлу и объявим переменные для хранения свойств элементов управления прокруткой строки заголовков:

//+------------------------------------------------------------------+
//|                                                    TabHeader.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "..\Common Controls\Button.mqh"
//+------------------------------------------------------------------+
//| Класс объекта TabHeader элемента управления WForms TabControl    |
//+------------------------------------------------------------------+
class CTabHeader : public CButton
  {
private:
   int               m_width_off;                        // Ширина объекта в состоянии "не выбран"
   int               m_height_off;                       // Высота объекта в состоянии "не выбран"
   int               m_width_on;                         // Ширина объекта в состоянии "выбран"
   int               m_height_on;                        // Высота объекта в состоянии "выбран"
   bool              m_arr_butt_ud_visible_flag;         // Флаг видимости кнопок "вверх-вниз" управления заголовками вкладок 
   bool              m_arr_butt_lr_visible_flag;         // Флаг видимости кнопок "влево-вправо" управления заголовками вкладок
   int               m_arr_butt_ud_size;                 // Размер кнопок "вверх-вниз" управления заголовками вкладок 
   int               m_arr_butt_lr_size;                 // Размер кнопок "влево-вправо" управления заголовками вкладок
//--- Подстраивает размер и расположение элемента в зависимости от состояния


В публичной секции класса напишем методы для работы с объявленными приватными переменными:

public:
//--- Возвращает видимость кнопок (1) влево-вправо, (2) вверх-вниз
   bool              IsVisibleLeftRightBox(void)                                 const { return this.m_arr_butt_lr_visible_flag; }
   bool              IsVisibleUpDownBox(void)                                    const { return this.m_arr_butt_ud_visible_flag; }
//--- Устанавливает видимость кнопок (1) влево-вправо, (2) вверх-вниз
   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;   }
//--- Устанавливает размер кнопок (1) влево-вправо, (2) вверх-вниз
   void              SetSizeLeftRightBox(const int value)                              { this.m_arr_butt_lr_size=value;          }
   void              SetSizeUpDownBox(const int value)                                 { this.m_arr_butt_ud_size=value;          }
//--- Находит и возвращает указатель на объект поля, соответствующий номеру вкладки

Актуальные данные во все эти переменные будут отправляться в классе объекта TabControl, а здесь — использоваться для расчёта размера видимой области и обрезания части изображения, выходящей за пределы видимой области.

Чтобы мы могли использовать значения новых переменных в расчётах видимой области, нам нужно переопределить здесь виртуальный метод Crop() родительского объекта.

Объявим метод в публичной области класса:

//--- Перерисовывает объект
   virtual void      Redraw(bool redraw);
//--- Очищает элемент с заполнением его цветом и непрозрачностью
   virtual void      Erase(const color colour,const uchar opacity,const bool redraw=false);
//--- Очищает элемент заливкой градиентом
   virtual void      Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false);

//--- Обрезает изображение, очерченное ранее установленной прямоугольной областью видимости
   virtual void      Crop(void);

//--- Обработчик последнего события мышки
   virtual void      OnMouseEventPostProcessing(void);

За пределами тела класса напишем его реализацию:

//+------------------------------------------------------------------+
//| Обрезает изображение, очерченное рассчитываемой                  |
//| прямоугольной областью видимости                                 |
//+------------------------------------------------------------------+
void CTabHeader::Crop(void)
  {
//--- Получаем указатель на базовый объект
   CGCnvElement *base=this.GetBase();
//--- Если у объекта нет базового, к которому он прикреплён, то и обрезать скрытые области не нужно - уходим
   if(base==NULL)
      return;
//--- Задаём начальные координаты и размеры области видимости во весь объект
   int vis_x=0;
   int vis_y=0;
   int vis_w=this.Width();
   int vis_h=this.Height();
//--- Задаём размеры областей сверху, снизу, слева и справа, которые выходят за пределы контейнера
   int crop_top=0;
   int crop_bottom=0;
   int crop_left=0;
   int crop_right=0;
//--- Получаем дополнительные размеры, на которые нужно обрезать заголовки при видимости кнопок со стрелками
   int add_size_lr=(this.IsVisibleLeftRightBox() ? this.m_arr_butt_lr_size : 0);
   int add_size_ud=(this.IsVisibleUpDownBox()    ? this.m_arr_butt_ud_size : 0);
//--- Рассчитываем границы области контейнера, внутри которой объект полностью виден
   int 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;
//--- Рассчитываем величины областей сверху, снизу, слева и справа, на которые объект выходит
//--- за пределы границ области контейнера, внутри которой объект полностью виден
   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(crop_top<0 || crop_bottom<0 || crop_left<0 || crop_right<0)
      this.Crop(vis_x,vis_y,vis_w,vis_h);
  }
//+------------------------------------------------------------------+

Здесь, в отличии от оригинального метода, добавлены дополнительные размеры, которые необходимо учитывать при расчёте области видимости. Если в переменных-флагах записано, что объект управления прокруткой видим, то для расчёта нужно использовать размер видимого элемента управления прокруткой. Если в переменной записано, что объект не видим, то используем вместо его размера ноль. Для объектов кнопок со стрелками влево-вправо и кнопок со стрелками вверх-вниз используются свои переменные и, соответственно, раздельные расчёты дополнительных размеров для расчёта области видимости.

В конструкторах класса инициализируем новые переменные значениями по умолчанию:

//+------------------------------------------------------------------+
//| Защищённый конструктор с указанием типа объекта,                 |
//| идентификатора чарта и подокна                                   |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+


В обработчике события "Курсор в пределах активной области, отжата кнопка мышки (левая)", напишем блок кода для создания и отправки события выбора вкладки элемента управления TabControl при щелчке по заголовку:

//+------------------------------------------------------------------+
//| Обработчик события Курсор в пределах активной области,           |
//| отжата кнопка мышки (левая)                                      |
//+------------------------------------------------------------------+
void CTabHeader::MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- Если кнопка мышки отпущена за пределами элемента - это отказ от взаимодействия с элементом
   if(lparam<this.CoordX() || lparam>this.RightEdge() || dparam<this.CoordY() || dparam>this.BottomEdge())
     {
      //--- Если это простая кнопка - устанавливаем изначальный цвет фона и текста
      if(!this.Toggle())
        {
         this.SetBackgroundColor(this.BackgroundColorInit(),false);
         this.SetForeColor(this.ForeColorInit(),false);
        }
      //--- Если это кнопка-переключатель - устанавливаем изначальный цвет фона и текста в зависимости от того нажата кнопка или нет
      else
        {
         this.SetBackgroundColor(!this.State() ? this.BackgroundColorInit() : this.BackgroundStateOnColorInit(),false);
         this.SetForeColor(!this.State() ? this.ForeColorInit() : this.ForeStateOnColorInit(),false);
        }
      //--- Устанавливаем изначальный цвет рамки
      this.SetBorderColor(this.BorderColorInit(),false);
      //--- Выводим тестовое сообщение в журнал
      Print(DFUN_ERR_LINE,TextByLanguage("Отмена","Cancel"));
     }
//--- Если кнопка мышки отпущена в пределах элемента - это щелчок по элементу управления
   else
     {
      //--- Если это простая кнопка - устанавливаем цвет фона и текста для состояния "Курсор мышки над активной зоной"
      if(!this.Toggle())
        {
         this.SetBackgroundColor(this.BackgroundColorMouseOver(),false);
         this.SetForeColor(this.ForeColorMouseOver(),false);
         this.Redraw(true);
        }
      //--- Если это кнопка-переключатель -
      else
        {
         //--- если кнопка не работает в группе - устанавливаем её состояние на противоположное,
         if(!this.GroupButtonFlag())
            this.SetState(!this.State());
         //--- иначе - если кнопка ещё не нажата - устанавливаем её в нажатое состояние
         else if(!this.State())
            this.SetState(true);
         //--- устанавливаем цвет фона и текста для состояния "Курсор мышки над активной зоной" в зависимости от того нажата кнопка или нет
         this.SetBackgroundColor(this.State() ? this.BackgroundStateOnColorMouseOver() : this.BackgroundColorMouseOver(),false);
         this.SetForeColor(this.State() ? this.ForeStateOnColorMouseOver() : this.ForeColorMouseOver(),false);
         
         //--- Получаем объект-поле, соответствующий этому заголовку
         CWinFormBase *field=this.GetFieldObj();
         if(field!=NULL)
           {
            //--- Отображаем поле, выводим его на передний план, рисуем рамку и обрезаем лишнее
            field.Show();
            field.BringToTop();
            field.DrawFrame();
            field.Crop();
           }
         //--- Перерисовываем объект и график
         this.Redraw(true);
        }
      //--- Выводим тестовое сообщение в журнал
      Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.State()=",this.State(),", ID=",this.ID(),", Group=",this.Group());
      //--- Создаём событие:
      //--- Получаем базовый и главный объекты
      CWinFormBase *base=this.GetBase();
      CWinFormBase *main=this.GetMain();
      //--- в long-параметре события будем передавать строку, в double-параметре - колонку расположения заголовка вкладки
      long lp=this.Row();
      double dp=this.Column();
      //--- в string-параметре события будем передавать имена главного и базового объектов, разделённые символом ";"
      string name_main=(main!=NULL ? main.Name() : "");
      string name_base=(base!=NULL ? base.Name() : "");
      string sp=name_main+";"+name_base;
      //--- Отправляем на график управляющей программы событие выбора вкладки
      ::EventChartCustom(::ChartID(),WF_CONTROL_EVENT_TAB_SELECT,lp,dp,sp);
      //--- Устанавливаем цвет рамки для состояния "Курсор мышки над активной зоной"
      this.SetBorderColor(this.BorderColorMouseOver(),false);
     }
  }
//+------------------------------------------------------------------+

Здесь мы создаём событие, которое будет отправлено в обработчик событий библиотеки, и в этом сообщении указываем номер строки заголовка в long-параметре сообщения (для одной строки это всегда будет ноль), номер колонки заголовка в ряду — в double-параметре, которое точно будет указывать на то, по какому заголовку был щелчок (номер выбранной вкладки). Для однозначной идентификации элемента управления TabControl, в котором была выбрана вкладка, нам нужно отправить в событии имя главного объекта, в котором произошло событие, и имя объекта TabControl, в котором была выбрана вкладка. Так как string-параметр у нас только один, то просто складываем имена главного объекта и объекта TabControl через разделитель ";". В обработчике событий мы затем по разделителю сможем разрезать строку и получить оба имени этих объектов.


Теперь доработаем класс WinForms-объекта TabControl в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\TabControl.mqh.

Подправим пути к файлам вспомогательных объектов, объявим новые переменные и методы и напишем методы, возвращающие указатели на объекты-кнопки со стрелками:

//+------------------------------------------------------------------+
//|                                                   TabControl.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "Container.mqh"
#include "GroupBox.mqh"
#include "..\Helpers\TabHeader.mqh"
#include "..\Helpers\TabField.mqh"
//+------------------------------------------------------------------+
//| Класс объекта TabHeader элемента управления WForms TabControl    |
//+------------------------------------------------------------------+
class CTabControl : public CContainer
  {
private:
   int               m_item_width;                    // Фиксированная ширина заголовков вкладок
   int               m_item_height;                   // Фиксированная высота заголовков вкладок
   int               m_header_padding_x;              // Дополнительное значение ширины заголовка при DrawMode==Fixed
   int               m_header_padding_y;              // Дополнительное значение высоты заголовка при DrawMode==Fixed
   int               m_field_padding_top;             // Значение Padding полей вкладок сверху
   int               m_field_padding_bottom;          // Значение Padding полей вкладок снизу
   int               m_field_padding_left;            // Значение Padding полей вкладок слева
   int               m_field_padding_right;           // Значение Padding полей вкладок справа
   bool              m_arr_butt_ud_visible_flag;      // Флаг видимости кнопок "вверх-вниз" управления заголовками вкладок 
   bool              m_arr_butt_lr_visible_flag;      // Флаг видимости кнопок "влево-вправо" управления заголовками вкладок
//--- (1) Скрывает, (2) отображает элементы управления Кнопки вправо-влево и вверх-вниз
   void              ShowArrLeftRightBox(void);
   void              ShowArrUpDownBox(void);
   void              HideArrLeftRightBox(void);
   void              HideArrUpDownBox(void);
//--- Переносит на передний план элементы управления Кнопки вправо-влево и вверх-вниз
   void              BringToTopArrLeftRightBox(void);
   void              BringToTopArrUpDownBox(void);
//--- Создаёт новый графический объект
   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);

//--- Возвращает список (1) заголовков, (2) полей вкладок, указатель на объекты кнопки (3) "вверх-вниз", (4) "влево-вправо"
   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); }
//--- Устанавливает вкладку выбранной


В публичной секции класса напишем методы для работы с новыми переменными и объектами-кнопками со стрелками и объявим метод для смещения строки заголовков и обработчик событий:

//--- Возвращает указатель на (1) заголовок, (2) поле вкладки (3) количество вкладок, видимость кнопок (4) влево-вправо, (5) вверх-вниз
   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;                                  }
//--- Устанавливает видимость кнопок (1) влево-вправо, (2) вверх-вниз
   void              SetVisibleLeftRightBox(const bool flag);
   void              SetVisibleUpDownBox(const bool flag);
//--- Устанавливает размер кнопок (1) влево-вправо, (2) вверх-вниз
   void              SetSizeLeftRightBox(const int value);
   void              SetSizeUpDownBox(const int value);
//--- (1) Устанавливает, (2) возвращает местоположение вкладок на элементе управления

...

//--- Устанавливает объект выше всех
   virtual void      BringToTop(void);
//--- Показывает элемент управления
   virtual void      Show(void);
//--- Смещает строку заголовков
   void              ShiftHeadersRow(const int selected);
//--- Обработчик событий
   virtual void      OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam);

//--- Конструктор
                     CTabControl(const long chart_id,
                                 const int subwindow,
                                 const string descript,
                                 const int x,
                                 const int y,
                                 const int w,
                                 const int h);
  };
//+------------------------------------------------------------------+


В конструкторе класса установим значения видимости по умолчанию для объектов-стрелок:

//+------------------------------------------------------------------+
//| Конструктор с указанием идентификатора чарта и подокна           |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+

По умолчанию оба этих объекта должны быть скрыты, и отображаться они будут только в случае, если либо горизонтальная, либо вертикальная строка заголовков выходят за пределы контейнера. По этим же флагам мы будем определять в объектах-заголовках вкладок необходимость добавления дополнительных значений к области видимости. Если объекта-стрелок нет, то заголовки будут обрезаться по краю контейнера, а если есть — то по краю этого объекта — чтобы объект-кнопки со стрелками не накладывался сверху на обрезанный по краю контейнера заголовок.


В методе, создающим указанное количество вкладок, подправим Y-координату размещения заголовков и высоту полей вкладок при их размещении снизу контейнера и добавим блок кода для создания двух объектов-кнопок "влево-вправо" и "вверх-вниз":

//+------------------------------------------------------------------+
//| Создаёт указанное количество вкладок                             |
//+------------------------------------------------------------------+
bool CTabControl::CreateTabPages(const int total,const int selected_page,const int tab_w=0,const int tab_h=0,const string header_text="")
  {
//--- Рассчитываем размеры и начальные координаты заголовка вкладки
   int w=(tab_w==0 ? this.ItemWidth()  : tab_w);
   int h=(tab_h==0 ? this.ItemHeight() : tab_h);

//--- В цикле по количеству вкладок
   CTabHeader *header=NULL;
   CTabField  *field=NULL;
   for(int i=0;i<total;i++)
     {
      //--- В зависимости от расположения заголовков вкладок устанавливаем их начальные координаты
      int header_x=2;
      int header_y=2;
      int header_w=w;
      int header_h=h;
      
      //--- Устанавливаем текущую координату X или Y в зависимости от расположения заголовков вкладок
      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;
        }
      //--- Создаём объект TabHeader
      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());
      
      //--- Сохраняем изначальную высоту заголовка и устанавливаем его размеры в соответствии с режимом установки размеров заголовков
      int h_prev=header_h;
      header.SetSizes(header_w,header_h);
      //--- Получаем смещение по Y расположения заголовка после изменения его высоты и
      //--- сдвигаем его на рассчитанную величину только для расположения заголовков слева
      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);

      //--- В зависимости от расположения заголовков вкладок устанавливаем начальные координаты полей вкладок
      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;
        }
      
      //--- Создаём объект TabField (поле вкладки)
      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();
     }
//--- Создаём кнопки влево-вправо и вверх-вниз
   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();
     }
//--- Выстраиваем все заголовки в соответствии с установленными режимами их отображения и выбираем указанную вкладку
   this.ArrangeTabHeaders();
   this.Select(selected_page,true);
   return true;
  }
//+------------------------------------------------------------------+

Координата Y заголовков при их размещении снизу должна быть выше на два пикселя нижнего края контейнера, так как заголовок выбранной вкладки увеличивается в размерах на два пикселя и, если его разместить по нижнему краю контейнера, то при его выборе и, соответственно, увеличении его высоты на два пикселя, его нижний край выходит за пределы контейнера и там обрезается.

Чтобы обрезание не происходило, объект должен быть размещён выше на два пикселя — с учётом его возможного увеличения при его выборе. Соответственно, поле вкладки должно быть меньше на два пикселя, так как эти два пикселя теперь "съедаются" смещением вверх координаты расположения заголовка.

После создания двух объектов-кнопок со стрелками, устанавливаем их в состояние "скрыт" (при этом эти состояния передаются в объекты-заголовки вкладок) и устанавливаем во все объекты-заголовки вкладок размеры созданных объектов. Всё это делается при помощи двух методов, которые рассмотрим чуть далее. Для объектов устанавливается тип рамки как "отсутствует", устанавливается прозрачный цвет фона и полная непрозрачность. В конце объект скрывается.

Таким образом оба этих объекта создаются как две кнопки на прозрачном фоне — чтобы соответствовать внешнему виду соответствующих объектов в элементе управления в MS Visual Studio.

Во всех методах, располагающих заголовки вкладок сверху, снизу, слева и справа, добавим установку номера строки и столбца для заголовка и блок кода для обработки ситуации, когда у нас включен режим расположения заголовков вкладок в одной строке и в случае, если заголовок вкладки выходит за пределы контейнера:

//+------------------------------------------------------------------+
//| Располагает заголовки вкладок сверху                             |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersTop(void)
  {
//--- Получаем список заголовков вкладок
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Объявляем переменные
   int col=0;                                // Столбец
   int row=0;                                // Строка
   int x1_base=2;                            // Начальная координата X
   int x2_base=this.Width()-2;               // Конечная координата X
   int x_shift=0;                            // Смещение набора вкладок для расчёта их выхода за пределы контейнера
   int n=0;                                  // Переменная для расчёта номера колонки относительно индекса цикла
//--- В цикле по списку заголовков
   for(int i=0;i<list.Total();i++)
     {
      //--- получаем очередной объект-заголовок вкладки
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- Если установлен флаг расположения заголовков в несколько рядов
      if(this.Multiline())
        {
         //--- Рассчитываем значение правого края заголовка с учётом того,
         //--- что начало отсчёта всегда идёт от левого края TabControl + 2 пикселя
         int x2=header.RightEdgeRelative()-x_shift;
         //--- Если рассчитанное значение не выходит за правый край TabControl минус 2 пикселя - 
         //--- устанавливаем номер колонки равным индексу цикла минус значение в переменной n
         if(x2<x2_base)
            col=i-n;
         //--- Если рассчитанное значение выходит за правый край TabControl минус 2 пикселя
         else
           {
            //--- Увеличиваем номер ряда, рассчитываем новое смещение (чтобы очередной объект сравнивать с левый краем TabControl + 2 пикселя),
            //--- переменной n устанавливаем значение индекса цикла, и номер колонки ставим в ноль - это начало новой строки
            row++;
            x_shift=header.CoordXRelative()-2;
            n=i;
            col=0;
           }
         //--- Назначаем заголовку вкладки номер ряда и колонки и смещаем заголовок в рассчитанные координаты
         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());
           }
        }
      //--- Если разрешён только один ряд заголовков
      else
        {
         header.SetRow(0);
         header.SetColumn(i);
        }
     }

//--- Местоположение всех заголовков вкладок задано, теперь разместим их все вместе с полями
//--- в соответствии с номерами рядов и столбцов заголовков.

//--- Получаем последний заголовок в списке
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
//--- Если объект получен
   if(last!=NULL)
     {
      //--- Если установлен режим растягивания заголовков на ширину контейнера - вызываем метод растягивания
      if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL)
         this.StretchHeaders();
      //--- Если это не первый ряд (с индексом 0)
      if(last.Row()>0)
        {
         //--- Рассчитываем смещение координаты Y поля вкладки
         int y_shift=last.Row()*last.Height();
         //--- В цикле по списку заголовков
         for(int i=0;i<list.Total();i++)
           {
            //--- получаем очередной объект
            CTabHeader *header=list.At(i);
            if(header==NULL)
               continue;
            //--- получаем поле вкладки, соответствующее полученному заголовку
            CTabField  *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- смещаем заголовок вкладки на рассчитанные координаты строки
            if(header.Move(header.CoordX(),header.CoordY()+y_shift))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
            //--- смещаем поле вкладки на рассчитанное смещение
            if(field.Move(field.CoordX(),field.CoordY()+y_shift))
              {
               field.SetCoordXRelative(field.CoordX()-this.CoordX());
               field.SetCoordYRelative(field.CoordY()-this.CoordY());
               //--- изменяем размер смещённого поля на величину его смещения
               field.Resize(field.Width(),field.Height()-y_shift,false);
              }
           }
        }
      //--- Если это первая и единственная строка
      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.BottomEdge()-arr_box.Height();
               //--- Если объект смещён на заданные координаты
               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();
                    }
                 }
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Здесь целиком показан метод, располагающий заголовки вкладок сверху. В остальных трёх методах сделана точно так же установка номера строки (ноль) и столбца (индекс цикла) для заголовка вкладки, а блоки кода, определяющие заголовок, выходящий за пределы контейнера и отображающий для строки кнопки управления её прокруткой, немного отличаются — лишь расчётом координат расположения заголовков относительно контейнера и отображением кнопок управления прокруткой в соответствующих координатах.

Рассмотим эти блоки кода для разных методов.

Для метода, располагающего заголовки вкладок снизу (ArrangeTabHeadersBottom()):

      //--- Если это первая и единственная строка
      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();
                    }
                 }
              }
           }
        }


Для метода, располагающего заголовки вкладок слева (ArrangeTabHeadersLeft()):

      //--- Если это первая и единственная строка
      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();
                    }
                 }
              }
           }
        }


Для метода, располагающего заголовки вкладок справа (ArrangeTabHeadersRight()):

      //--- Если это первая и единственная строка
      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();
                    }
                 }
              }
           }
        }

Сравнив все четыре блока кода во всех четырёх методах, можно увидеть, как рассчитываются координаты расположения кнопок управления прокруткой и определяются заголовки, выходящие за пределы контейнера.

Метод, создающий новый графический объект, тоже претерпел изменения в форматировании кейсов оператора switch:

//+------------------------------------------------------------------+
//| Создаёт новый графический объект                                 |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+

Теперь всё в одну строку — для более компактного отображения логики метода.

В виртуальном методе, устанавливающем объект выше всех, заменим вызов метода BringToTop() объекта-формы

//+------------------------------------------------------------------+
//| Устанавливает объект выше всех                                   |
//+------------------------------------------------------------------+
void CTabControl::BringToTop(void)
  {
//--- Переносим все элементы объекта на передний план
   CForm::BringToTop();
//--- Получим номер выбранной вкладки

на вызов метода Show() объекта-графического элемента и, если сам объект видимый, то отобразим объекты управления прокруткой строк заголовков в случае, если эти объекты должны быть видимы:

//+------------------------------------------------------------------+
//| Устанавливает объект выше всех                                   |
//+------------------------------------------------------------------+
void CTabControl::BringToTop(void)
  {
//--- Переносим все элементы объекта на передний план
   CGCnvElement::Show();
//--- Получим номер выбранной вкладки
   int selected=this.SelectedTabPageNum();
//--- Объявим указатели на объекты-заголовки вкладок и поля вкладок
   CTabHeader *header=NULL;
   CTabField  *field=NULL;
//--- Получим список всех заголовков вкладок
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- В цикле по списку заголовков вкладок
   for(int i=0;i<list.Total();i++)
     {
      //--- получаем очередной заголовок и, если получить объект не удалось,
      //--- или это заголовок выбранной вкладки - пропускаем его
      header=list.At(i);
      if(header==NULL || header.PageNumber()==selected)
         continue;
      //--- выводим заголовок на передний план
      header.BringToTop();
      //--- получаем поле вкладки, соответствующее текущему заголовку
      field=header.GetFieldObj();
      if(field==NULL)
         continue;
      //--- Скрываем поле вкладки
      field.Hide();
     }
//--- Получаем указатель на заголовок выбранной вкладки
   header=this.GetTabHeader(selected);
   if(header!=NULL)
     {
      //--- вывоим заголовок на передний план
      header.BringToTop();
      //--- получаем поле вкладки, соответствующее заголовку выбранной вкладки
      field=header.GetFieldObj();
      //--- Выводим поле вкладки на передний план
      if(field!=NULL)
         field.BringToTop();
     }
//--- Если объект видимый и кнопки "вверх-вниз" и "влево-вправо" должны быть видимы - переносим их на передний план
   if(this.IsVisible())
     {
      if(this.m_arr_butt_ud_visible_flag)
         this.BringToTopArrUpDownBox();
      if(this.m_arr_butt_lr_visible_flag)
         this.BringToTopArrLeftRightBox();
     }
  }
//+------------------------------------------------------------------+

Если ранее мы вызывали метод BringToTop родительского класса CForm, то этот метод переносил на передний план абсолютно все объекты, привязанные к элементу управления, делая их видимыми, то теперь нам нужно контролировать видимость объектов-кнопок управления прокруткой. Поэтому мы просто делаем видимым сам объект, а затем уже внутри метода проверяем необходимость отображения заголовков и полей вкладок и кнопок управления прокруткой строк заголовков.

Метод, отображающий элементы управления "Кнопки вверх-вниз":

//+------------------------------------------------------------------+
//| Отображает элементы управления Кнопки вверх-вниз                 |
//+------------------------------------------------------------------+
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();
  }
//+------------------------------------------------------------------+

Получаем указатель на элемент управления. Если указатель получить не удалось — выводим сообщение об ошибке и уходим из метода. При успешном получении указателя отображаем объект.


Метод, отображающий элементы управления "Кнопки вправо-влево":

//+------------------------------------------------------------------+
//| Отображает элементы управления Кнопки вправо-влево               |
//+------------------------------------------------------------------+
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();
  }
//+------------------------------------------------------------------+

Логика метода идентична вышерассмотренному.


Методы, скрывающие элементы управления "Кнопки вверх-вниз" и "Кнопки вправо-влево":

//+------------------------------------------------------------------+
//| Скрывает элементы управления Кнопки вверх-вниз                   |
//+------------------------------------------------------------------+
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();
  }
//+------------------------------------------------------------------+
//| Скрывает элементы управления Кнопки вправо-влево                 |
//+------------------------------------------------------------------+
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();
  }
//+------------------------------------------------------------------+

Логика методов идентична логике двух вышерассмотренных методов для отображения элементов управления. Но здесь вместо отображения элемента он скрывается.


Методы, переносящие на передний план элементы управления "Кнопки вверх-вниз" и "Кнопки вправо-влево":

//+------------------------------------------------------------------+
//| Переносит на передний план элементы управления Кнопки вверх-вниз |
//+------------------------------------------------------------------+
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();
  }
//+------------------------------------------------------------------+
//|Переносит на передний план элементы управления Кнопки вправо-влево|
//+------------------------------------------------------------------+
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();
  }
//+------------------------------------------------------------------+

Всё точно так же, но перенос на передний план.


Метод, устанавливающий видимость кнопок влево-вправо:

//+------------------------------------------------------------------+
//| Устанавливает видимость кнопок влево-вправо                      |
//+------------------------------------------------------------------+
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);
     }
  }
//+------------------------------------------------------------------+

Сначала устанавливаем флаг видимости элемента управления, затем получаем список заголовков вкладок и в цикле по списку для каждого из объектов-заголовков устанавливаем значение флага, переданное в метод.


Метод, устанавливающий видимость кнопок вверх-вниз:

//+------------------------------------------------------------------+
//| Устанавливает видимость кнопок вверх-вниз                        |
//+------------------------------------------------------------------+
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);
     }
  }
//+------------------------------------------------------------------+

Логика метода идентична вышерассмотренному. Но здесь устанавливаются флаги для объектов-кнопок вверх-вниз.


Метод, устанавливающий размер кнопок влево-вправо:

//+------------------------------------------------------------------+
//| Устанавливает размер кнопок влево-вправо                         |
//+------------------------------------------------------------------+
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);
     }
  }
//+------------------------------------------------------------------+

Получаем указатель на объект-кнопки влево-вправо, устанавливаем переданное в метод значение как ширину объекта (так как здесь нам нужно менять только ширину объекта), затем в цикле по списку заголовков вкладок в каждый очередной объект записываем значение установленной ширины объекта-кнопок управления прокруткой.


Метод, устанавливающий размер кнопок вверх-вниз:

//+------------------------------------------------------------------+
//| Устанавливает размер кнопок вверх-вниз                           |
//+------------------------------------------------------------------+
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);
     }
  }
//+------------------------------------------------------------------+

Логика метода идентична вышерассмотренному. Но здесь мы получаем указатель на объект-кнопки вверх-вниз, и устанавливаем высоту этого объекта.


Метод, смещающий строку заголовков:

//+------------------------------------------------------------------+
//| Смещает строку заголовков                                        |
//+------------------------------------------------------------------+
void CTabControl::ShiftHeadersRow(const int selected)
  {
//--- Если многострочные заголовки - уходим
   if(this.Multiline())
      return;
//--- Получаем список заголовков вкладок
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Получаем заголовок выбранной вкладки
   CTabHeader *header=this.GetTabHeader(selected);
   if(header==NULL)
      return;
//--- Проверяем сколько обрезано у выбранного заголовка справа
   int hidden=header.RightEdge()-this.RightEdgeWorkspace();
//--- Если заголовок не обрезан - уходим
   if(hidden<0)
      return;
   CTabHeader *obj=NULL;
   int shift=0;
//--- Начиная от выбранного заголовка ищем крайний слева
   for(int i=selected-1;i>WRONG_VALUE;i--)
     {
      obj=list.At(i);
      if(obj==NULL)
         continue;
      //--- Если крайний слева найден - запоминаем на сколько нужно сместить все заголовки справа
      if(obj.CoordX()-2==this.CoordX())
         shift=obj.Width();
     }
//--- В цикле по списку заголовков
   for(int i=0;i<list.Total();i++)
     {
      //--- получаем очередной заголовок
      obj=list.At(i);
      if(obj==NULL)
         continue;
      //--- и, если заголовок сдвинут влево на величину shift,
      if(obj.Move(obj.CoordX()-shift,obj.CoordY()))
        {
         //--- запоминаем его новые относительные координаты
         obj.SetCoordXRelative(obj.CoordX()-this.CoordX());
         obj.SetCoordYRelative(obj.CoordY()-this.CoordY());
         //--- Если заголовок вышел за левый край
         if(obj.CoordX()-2<this.CoordX())
           {
            //--- обрезаем его и скрываем
            obj.Crop();
            obj.Hide();
           }
         //--- Если заголовок умещается в видимой области элемента управления
         else
           {
            //--- отображаем и перерисовываем его
            obj.Show();
            obj.Redraw(false);
            //--- Получаем поле вкладки, соответствующее заголовку
            CTabField *field=obj.GetFieldObj();
            if(field==NULL)
               continue;
            //--- Если это выбранный заголовок
            if(i==selected)
              {
               //--- Рисуем рамку поля
               field.DrawFrame();
               field.Update();
              }
           }
        }
     }
//--- Перерисовываем график для немедленного отображения изменений
   ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+

Логика метода построчно расписана в комментариях к коду. Вкратце: в метод передаётся индекс заголовка, по которому был щелчок мышки (выбор вкладки объекта TabControl). Если выбранная вкладка, её заголовок, выходит за пределы контейнера, т.е., обрезана, то нам нужно сместить все заголовки влево так, чтобы выбранный заголовок стал полностью видимым за счёт смещения всех заголовков влево. Самый первый видимый слева заголовок при этом уходит за левый край, а на его место встаёт следующий.

Поэтому, сначала нам нужно найти самый первый слева видимый заголовок и узнать его ширину (ширина у каждого из заголовков может быть собственной — если размеры устанавливаются по тексту заголовка). Ширина найденного первого заголовка будет величиной, на которую нужно сместить всю строку заголовков влево. После смещения строки, для выбранного заголовка находим его поле и перерисовываем его рамку, так как рамка поля рисуется относительно расположения заголовка, а он теперь смещён, и рамка будет нарисована не правильно.

Данный метод пригоден только для смещения влево строки горизонтально расположенных заголовков. На основе этого метода в следующей статье создадим методы для смещения строки заголовков во все стороны — влево-вправо и вверх-вниз.


Обработчик событий:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CTabControl::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Корректируем смещение по Y для подокна
   CGCnvElement::OnChartEvent(id,lparam,dparam,sparam);
   if(id==WF_CONTROL_EVENT_TAB_SELECT)
     {
      this.ShiftHeadersRow((int)dparam);
     }
  }
//+------------------------------------------------------------------+

Сначала вызываем метод корректировки смещения координаты Y для подокна класса объекта-графического элемента (это действует для каждого графического элемента на канвасе, но здесь обработчик событий родительского класса переопределён, поэтому нужно не забывать сделать вызов метода корректировки координаты), а затем вызываем метод смещения строки заголовков вкладок, рассмотренный выше.


Доработаем класс-коллекцию графических элементов в файле \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh.

Добавим метод, возвращающий графический элемент по имени:

//--- Возвращает список графических элементов по идентификатору графика и имени объекта
   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);
                       }
//--- Возвращает графический элемент по имени
   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);
                       }
//--- Возвращает графический элемент по идентификатору графика и имени
   CGCnvElement     *GetCanvElement(const long chart_id,const string name)
                       {
                        CArrayObj *list=this.GetListCanvElementByName(chart_id,name);
                        return(list!=NULL ? list.At(0) : NULL);
                       }
//--- Возвращает графический элемент по идентификатору графика и идентификатору объекта

Так как теперь все имена всех графических элементов на канвасе уникальны, то нет необходимости при поиске объекта по имени указывать идентификатор графика, на котором построен объект. Поэтому целесообразно добавить метод, возвращающий объект только по его имени. Здесь проверяется какое имя передано в метод и, если в строке имени отсутствует имя программы, то к этому имени добавляется в начало имя программы, чтобы имя искомого объекта совпадало с реальным именем графического элемента — ведь в них во всех присутствует подстрока с именем программы — так библиотека создаёт имена объектов-графических элементов.


У нас теперь WinForms-объекты могут отправлять сообщения о своих событиях на график управляющей программы. Эти события перехватывает библиотека и должна уметь их обрабатывать и перенаправлять нужное сообщение о событии в нужный класс. Поэтому допишем в обработчик событий класса-коллекции графических элементов обработку кодов событий WinForms-объектов:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CGraphElementsCollection::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   CGStdGraphObj *obj_std=NULL;  // Указатель на стандартный графический объект
   CGCnvElement  *obj_cnv=NULL;  // Указатель на объект графического элемента на канвасе
   ushort idx=ushort(id-CHARTEVENT_CUSTOM);

//--- Обработка событий элементов управления WinForms
   if(idx>WF_CONTROL_EVENT_NO_EVENT && idx<WF_CONTROL_EVENTS_NEXT_CODE)
     {
      //--- Щелчок по элементу управления
      if(idx==WF_CONTROL_EVENT_CLICK)
        {
         //---
        }
      //--- Выбор вкладки элемента управления TabControl
      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);
        }
     }
//--- Обработка событий переименования и щелчка по стандартному графическому объекту
   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)
     {
      //--- Рассчитаем идентификатор графика
      //---...
      //---...
     }
//---...
//---...
  }

Здесь пока обрабатывается только один код события — выбор вкладки в элементе управления TabControl. Разделяем сообщение, переданное в строковом параметре sparam, на два по разделителю ";" при помощи функции StringSplit(). В итоге получаем в массиве два имени — имя главного объекта (в данном случае — объекта-панели — main), и WinForms-объекта TabControl — base, который прикреплён к панели. В параметрах lparam и dparam мы передаём номер строки и столбца выбранного заголовка вкладки. По всем этим данным мы теперь можем точно идентифицировать панель, к которой прикреплён элемент управления TabControl, и вкладку элемента управления, по заголовку которой был выполнен щелчок. После получения указателей на все эти объекты мы можем вызвать обработчик событий полученного объекта, который в свою очередь вызовет метод смещения строки заголовков вкладок.

На сегодня это все требуемые изменения и доработки.


Тестирование

Для теста возьмём советник из прошлой статьи и сохраним его в новой папке \MQL5\Experts\TestDoEasy\Part118\ под новым именем TestDoEasy118.mq5.

Как будем тестировать? Создадим одну главную панель, и на ней разместим элемент управления TabControl с 11-ю вкладками. На каждой из вкладок создадим текстовую метку с описанием этой вкладки — чтобы при тесте мы могли видеть какая вкладка отображается на самом деле.

После запуска советника, установим в его настройках растягивание заголовков вкладок по ширине элемента управления — так будет видно наглядно какая вкладка не помещается и выходит за край контейнера. Проверим расположение вкладок с каждой из сторон элемента управления — как размещаются кнопки управления прокруткой строки заголовков. А при расположении заголовков сверху, пощёлкаем по обрезанным справа заголовкам вкладок и посмотрим, как вся строка смещается влево, делая видимым выбранный заголовок, копируя поведение элемента управления TabControl в MS Visual Studio.

В обработчике OnInit() советника создадим одну панель, а на ней — элемент управления TabControl с 11-ю вкладками с текстовыми метками:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Установка глобальных переменных советника
   ArrayResize(array_clr,2);        // Массив цветов градиентной заливки
   array_clr[0]=C'26,100,128';      // Исходный ≈Тёмно-лазурный цвет
   array_clr[1]=C'35,133,169';      // Осветлённый исходный цвет
//--- Создадим массив с текущим символом и установим его для использования в библиотеке
   string array[1]={Symbol()};
   engine.SetUsedSymbols(array);
   //--- Создадим объект-таймсерию для текущего символа и периода и выведем его описание в журнал
   engine.SeriesCreate(Symbol(),Period());
   engine.GetTimeSeriesCollection().PrintShort(false); // Краткие описания

//--- Создадим требуемое количество объектов WinForms Panel
   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,"Описание панели: ",pnl.Description(),", Тип и имя: ",pnl.TypeElementDescription()," ",pnl.Name());
         //--- Установим значение Padding равным 4
         pnl.SetPaddingAll(3);
         //--- Установим флаги перемещаемости, автоизменения размеров и режим автоизменения из входных параметров
         pnl.SetMovable(InpMovable);
         pnl.SetAutoSize(InpAutoSize,false);
         pnl.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpAutoSizeMode,false);
   
         //--- Создадим элемент управления 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"));
            //--- Создадим на каждой вкладке текстовую метку с описанием вкладки
            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));
              }
           }
        }
     }
//--- Отобразим и перерисуем все созданные панели
   for(int i=0;i<1;i++)
     {
      pnl=engine.GetWFPanelByName("Panel"+(string)i);
      if(pnl!=NULL)
        {
         pnl.Show();
         pnl.Redraw(true);
        }
     }
        
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Панели создаём в цикле (сейчас пока только одна панель), так как выяснилось, что если создать несколько панелей с элементами управления TabControl, то эти элементы работают некорректно. Для будущего исправления этой недоработки мы и сделаем создание нужного количества панелей.

Скомпилируем советник и запустим его на графике, предварительно выставив требуемые настройки:


Как видим, заявленный функционал работает правильно.


Что дальше

В следующей статье создадим методы для прокрутки заголовков вкладок во все стороны при помощи кнопок управления прокруткой.

Ниже прикреплены все файлы текущей версии библиотеки, файлы тестового советника и индикатора контроля событий графиков для MQL5. Их можно скачать и протестировать всё самостоятельно. При возникновении вопросов, замечаний и пожеланий вы можете озвучить их в комментариях к статье.

К содержанию

*Статьи этой серии:

DoEasy. Элементы управления (Часть 10): WinForms-объекты — оживляем интерфейс
DoEasy. Элементы управления (Часть 11): WinForms-объекты — группы, WinForms-объект CheckedListBox
DoEasy. Элементы управления (Часть 12): Базовый объект-список, WinForms-объекты ListBox и ButtonListBox
DoEasy. Элементы управления (Часть 13): Оптимизация взаимодействия WinForms-объектов с мышкой, начало разработки WinForms-объекта TabControl
DoEasy. Элементы управления (Часть 14): Новый алгоритм именования графических элементов. Продолжаем работу над WinForms-объектом TabControl
DoEasy. Элементы управления (Часть 15): WinForms-объект TabControl — несколько рядов заголовков вкладок, методы работы с вкладками
DoEasy. Элементы управления (Часть 16): WinForms-объект TabControl — несколько рядов заголовков вкладок, режим растягивания заголовков под размеры контейнера
DoEasy. Элементы управления (Часть 17): Отсечение невидимых участков объектов, вспомогательные WinForms-объекты кнопки со стрелками

Прикрепленные файлы |
MQL5.zip (4540.22 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
Daniil Kurmyshev
Daniil Kurmyshev | 16 сент. 2022 в 16:14
Добрый день!)

В целом полезная функция, но выглядит визуально не изящно, как сделать иначе, честно не знаю, но хотя бы в размер с кнопками)

Возможно было бы изящнее полосу прокрутки классическую сделать, но это как вариант, а вообще странно так много вкладок делать горизонтальными даже в плане управления это кааапец как неудобно, при большом количестве вкладок уже логичнее меню сделать слева и дать возможность скрывать..

п.с. использую стандартную библиотеку, работает быстрее всех и стабильнее, но не все элементы есть, приходится дополнять.
Artyom Trishkin
Artyom Trishkin | 16 сент. 2022 в 16:25
Daniil Kurmyshev #:
Добрый день!)

В целом полезная функция, но выглядит визуально не изящно, как сделать иначе, честно не знаю, но хотя бы в размер с кнопками)

Возможно было бы изящнее полосу прокрутки классическую сделать, но это как вариант, а вообще странно так много вкладок делать горизонтальными даже в плане управления это кааапец как неудобно, при большом количестве вкладок уже логичнее меню сделать слева и дать возможность скрывать..

Здесь разрабатывается аналог элемента управления TabControl из MS Visual Studio. Естественно, у объекта есть возможность размещать заголовки вкладок в несколько рядов и с разных сторон контейнера. Кнопки управления прокруткой сделаны по аналогии с вышеупомянутым объектом. Размер - высота, заголовков настраивается. Здесь в примере специально сделана больше высоты кнопок - чтобы видно было как идёт обрезание - равно так же, как и в TabControl из MS Visual Studio:

Естественно, кнопки управления прокруткой немного отличаются по внешнему виду. Но это поправимо - нужно заливать их не однородным цветом, а градиентом (это здесь возможно, просто ещё пока рано заниматься дизайнерскими изысками), равно, как и размер стрелок будет подбираться позже.

Классической полосы прокрутки в данном элементе управления для заголовков вкладок не предусмотрено - и её не будет - не для этого она здесь. А вот те элементы, которые возможно разместить на полях вкладок - они будут иметь стандартную полосу прокрутки (настраиваемую по желанию)

Работа с матрицами и векторами в MQL5 Работа с матрицами и векторами в MQL5
Для решения математических задач в MQL5 были добавлены матрицы и векторы. Новые типы имеют встроенные методы для написания краткого и понятного кода, который близок к математической записи. Массивы — это хорошо, но матрицы во многих случаях лучше.
Нейросети — это просто (Часть 29): Алгоритм актор-критик с преимуществом (Advantage actor-critic) Нейросети — это просто (Часть 29): Алгоритм актор-критик с преимуществом (Advantage actor-critic)
В предыдущих статьях данной серии мы познакомились с 2-мя алгоритмами обучения с подкреплением. Каждый из них обладает своими достоинствами и недостатками. Как часто бывает в таких случаях, появляется идея совместить оба метода в некий алгоритм, который бы вобрал в себя лучшее из двух. И тем самым компенсировать недостатки каждого из них. О таком методе мы и поговорим в этой статье.
Нейросети — это просто (Часть 30): Генетические алгоритмы Нейросети — это просто (Часть 30): Генетические алгоритмы
Сегодня я хочу познакомить Вас с немного иным методом обучения. Можно сказать, что он заимствован из теории эволюции Дарвина. Наверное, он менее контролируем в сравнении с рассмотренными ранее методами. Но при этом позволяет обучать и недифференцируемые модели.
Возможности Мастера MQL5, которые вам нужно знать (Часть 02): Карты Кохонена Возможности Мастера MQL5, которые вам нужно знать (Часть 02): Карты Кохонена
Благодаря Мастеру, трейдер экономит время при реализации своих идей. Кроме того, снижается вероятность ошибок, возникающих при дублировании кода. Вместо того чтобы тратить время на оформление кода, трейдеры претворяют в жизнь свою торговую философию.