English 中文 Español Deutsch 日本語 Português
preview
DoEasy. Элементы управления (Часть 15): WinForms-объект TabControl — несколько рядов заголовков вкладок, методы работы с вкладками

DoEasy. Элементы управления (Часть 15): WinForms-объект TabControl — несколько рядов заголовков вкладок, методы работы с вкладками

MetaTrader 5Примеры | 12 августа 2022, 11:38
751 2
Artyom Trishkin
Artyom Trishkin

Содержание


Концепция

Продолжаем тему WinForms-объекта TabControl. Если у элемента управления вкладок больше, чем их может разместиться по ширине объекта (имеем в виду их расположение сверху), то те заголовки, которые не умещаются в пределах элемента, могут либо быть обрезанными по краю с наличием кнопок их прокрутки, либо, если у объекта установлен флаг режима Multiline, то заголовки размещаются по несколько штук (сколько входит в размер элемента) в несколько рядов. Для режима их расположения в несколько рядов есть три способа задания размера вкладок (SizeMode):

  • Normal — ширина вкладок устанавливается по ширине текста заголовка, по краям заголовка добавляется пространство, указанное в значениях PaddingWidth и PaddingHeight заголовка;
  • Fixed — фиксированный размер, указываемый в настройках элемента управления. Текст заголовков обрезается, если не входит в его размеры;
  • FillToRight — вкладки, умещающиеся в пределах ширины элемента управления, растягиваются на всю его ширину.

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

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

Для взаимодействия с полем вкладки, ранее реализованным как объект-контейнер из класса CContainer, создадим новый объект TabField — наследник объекта-контейнера со своими свойствами и методами для полноценной работы с полем вкладки.


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

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

//+------------------------------------------------------------------+
//| Список типов графических элементов                               |
//+------------------------------------------------------------------+
enum ENUM_GRAPH_ELEMENT_TYPE
  {
   GRAPH_ELEMENT_TYPE_STANDARD,                       // Стандартный графический объект
   GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED,              // Расширенный стандартный графический объект
   GRAPH_ELEMENT_TYPE_SHADOW_OBJ,                     // Объект тени
   GRAPH_ELEMENT_TYPE_ELEMENT,                        // Элемент
   GRAPH_ELEMENT_TYPE_FORM,                           // Форма
   GRAPH_ELEMENT_TYPE_WINDOW,                         // Окно
   //--- WinForms
   GRAPH_ELEMENT_TYPE_WF_UNDERLAY,                    // Подложка объекта-панели
   GRAPH_ELEMENT_TYPE_WF_BASE,                        // Windows Forms Base
   //--- Ниже нужно вписывать типы объектов "контейнер"
   GRAPH_ELEMENT_TYPE_WF_CONTAINER,                   // Windows Forms базовый объект-контейнер
   GRAPH_ELEMENT_TYPE_WF_PANEL,                       // Windows Forms Panel
   GRAPH_ELEMENT_TYPE_WF_GROUPBOX,                    // Windows Forms GroupBox
   GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,                 // Windows Forms TabControl
   //--- Ниже нужно вписывать типы объектов "стандартный элемент управления"
   GRAPH_ELEMENT_TYPE_WF_COMMON_BASE,                 // Windows Forms базовый стандартный элемент управления
   GRAPH_ELEMENT_TYPE_WF_LABEL,                       // Windows Forms Label
   GRAPH_ELEMENT_TYPE_WF_BUTTON,                      // Windows Forms Button
   GRAPH_ELEMENT_TYPE_WF_CHECKBOX,                    // Windows Forms CheckBox
   GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,                 // Windows Forms RadioButton
   GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX,           // Базовый объект-список Windows Forms элементов
   GRAPH_ELEMENT_TYPE_WF_LIST_BOX,                    // Windows Forms ListBox
   GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,            // Windows Forms CheckedListBox
   GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX,             // Windows Forms ButtonListBox
   //--- Вспомогательные элементы WinForms-объектов
   GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM,               // Windows Forms ListBoxItem
   GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,                  // Windows Forms TabHeader
   GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,                   // Windows Forms TabField
  };
//+------------------------------------------------------------------+

Так как этот объект как самостоятельная единица работать не будет, то он является вспомогательным, и работает в составе элемента управления TabControl, наряду с таким же вспомогательным объектом TabHeader, созданным нами в прошлой статье.

Добавим новое перечисление режимов установки размеров вкладок:

//+------------------------------------------------------------------+
//| Местоположение объекта внутри элемента управления                |
//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_ALIGNMENT
  {
   CANV_ELEMENT_ALIGNMENT_TOP,                        // Сверху
   CANV_ELEMENT_ALIGNMENT_BOTTOM,                     // Снизу
   CANV_ELEMENT_ALIGNMENT_LEFT,                       // Слева
   CANV_ELEMENT_ALIGNMENT_RIGHT,                      // Справа
  };
//+------------------------------------------------------------------+
//| Режим установки размера вкладок                                  |
//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_TAB_SIZE_MODE
  {
   CANV_ELEMENT_TAB_SIZE_MODE_NORMAL,                 // По ширине текста заголовка вкладки
   CANV_ELEMENT_TAB_SIZE_MODE_FIXED,                  // Фиксированный размер
   CANV_ELEMENT_TAB_SIZE_MODE_FILL,                   // По ширине элемента управления TabControl
  };
//+------------------------------------------------------------------+
//| Целочисленные свойства графического элемента на канвасе          |
//+------------------------------------------------------------------+


В конце списка целочисленных свойств графического элемента на канвасе впишем два новых свойства и увеличим значение общего количества целочисленных свойств с 88 до 90:

//+------------------------------------------------------------------+
//| Целочисленные свойства графического элемента на канвасе          |
//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_PROP_INTEGER
  {
   CANV_ELEMENT_PROP_ID = 0,                          // Идентификатор элемента
   CANV_ELEMENT_PROP_TYPE,                            // Тип графического элемента
   //---...
   //---...
   CANV_ELEMENT_PROP_TAB_MULTILINE,                   // Несколько рядов вкладок в элементе управления TabControl
   CANV_ELEMENT_PROP_TAB_ALIGNMENT,                   // Местоположение вкладок внутри элемента управления
   CANV_ELEMENT_PROP_TAB_SIZE_MODE,                   // Режим установки размера вкладок
   CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,                 // Порядковый номер вкладки
   CANV_ELEMENT_PROP_ALIGNMENT,                       // Местоположение объекта внутри элемента управления
   
  };
#define CANV_ELEMENT_PROP_INTEGER_TOTAL (90)          // Общее количество целочисленных свойств
#define CANV_ELEMENT_PROP_INTEGER_SKIP  (0)           // Количество неиспользуемых в сортировке целочисленных свойств
//+------------------------------------------------------------------+


Впишем два новых свойства в список возможных критериев сортировки графических элементов на канвасе:

//+------------------------------------------------------------------+
//| Возможные критерии сортировки графических элементов на канвасе   |
//+------------------------------------------------------------------+
#define FIRST_CANV_ELEMENT_DBL_PROP  (CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_INTEGER_SKIP)
#define FIRST_CANV_ELEMENT_STR_PROP  (CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_INTEGER_SKIP+CANV_ELEMENT_PROP_DOUBLE_TOTAL-CANV_ELEMENT_PROP_DOUBLE_SKIP)
enum ENUM_SORT_CANV_ELEMENT_MODE
  {
//--- Сортировка по целочисленным свойствам
   SORT_BY_CANV_ELEMENT_ID = 0,                       // Сортировать по идентификатору элемента
   SORT_BY_CANV_ELEMENT_TYPE,                         // Сортировать по типу графического элемента
   //---...
   //---...
   SORT_BY_CANV_ELEMENT_TAB_MULTILINE,                // Сортировать по флагу нескольких рядов вкладок в элементе управления TabControl
   SORT_BY_CANV_ELEMENT_TAB_ALIGNMENT,                // Сортировать по местоположению вкладок внутри элемента управления
   SORT_BY_CANV_ELEMENT_TAB_SIZE_MODE,                // Сортировать по режиму установки размера вкладок
   SORT_BY_CANV_ELEMENT_TAB_PAGE_NUMBER,              // Сортировать по порядковому номеру вкладки
   SORT_BY_CANV_ELEMENT_ALIGNMENT,                    // Сортировать по местоположению объекта внутри элемента управления
//--- Сортировка по вещественным свойствам

//--- Сортировка по строковым свойствам
   SORT_BY_CANV_ELEMENT_NAME_OBJ = FIRST_CANV_ELEMENT_STR_PROP,// Сортировать по имени объекта-элемента
   SORT_BY_CANV_ELEMENT_NAME_RES,                     // Сортировать по имени графического ресурса
   SORT_BY_CANV_ELEMENT_TEXT,                         // Сортировать по тексту графического элемента
   SORT_BY_CANV_ELEMENT_DESCRIPTION,                  // Сортировать по описанию графического элемента
  };
//+------------------------------------------------------------------+

Теперь мы сможем сортировать, фильтровать, выбирать и искать объекты по этим новым свойствам.

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


Новые свойства и перечисления добавили. Теперь добавим тексты для вывода их описания.

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

   MSG_LIB_TEXT_TOP,                                  // Сверху
   MSG_LIB_TEXT_BOTTOM,                               // Снизу
   MSG_LIB_TEXT_LEFT,                                 // Слева
   MSG_LIB_TEXT_RIGHT,                                // Справа
   
   MSG_LIB_TEXT_TAB_SIZE_MODE_NORMAL,                 // По ширине текста заголовка вкладки
   MSG_LIB_TEXT_TAB_SIZE_MODE_FILL,                   // По ширине элемента управления TabControl
   MSG_LIB_TEXT_TAB_SIZE_MODE_FIXED,                  // Фиксированный размер
   
   MSG_LIB_TEXT_CORNER_LEFT_UPPER,                    // Центр координат в левом верхнем углу графика
   MSG_LIB_TEXT_CORNER_LEFT_LOWER,                    // Центр координат в левом нижнем углу графика
   MSG_LIB_TEXT_CORNER_RIGHT_LOWER,                   // Центр координат в правом нижнем углу графика
   MSG_LIB_TEXT_CORNER_RIGHT_UPPER,                   // Центр координат в правом верхнем углу графика

...

   MSG_GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX,         // Элемент управления ButtonListBox
   MSG_GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,              // Заголовок вкладки
   MSG_GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,               // Поле вкладки
   MSG_GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,             // Элемент управления TabControl
   MSG_GRAPH_OBJ_BELONG_PROGRAM,                      // Графический объект принадлежит программе

...

//--- CButtonListBox
   MSG_BUTT_LIST_ERR_FAILED_SET_GROUP_BUTTON,         // Не удалось установить группу кнопке с индексом 
   MSG_BUTT_LIST_ERR_FAILED_SET_TOGGLE_BUTTON,        // Не удалось установить флаг "Переключатель" кнопке с индексом 

//--- CTabControl
   MSG_ELM_LIST_ERR_FAILED_GET_TAB_OBJ,               // Не удалось получить вкладку элемента управления TabControl

//--- Целочисленные свойства графических элементов

...

   MSG_CANV_ELEMENT_PROP_TAB_MULTILINE,               // Несколько рядов вкладок в элементе управления
   MSG_CANV_ELEMENT_PROP_TAB_ALIGNMENT,               // Местоположение вкладок внутри элемента управления
   MSG_CANV_ELEMENT_PROP_TAB_SIZE_MODE,               // Режим установки размера вкладок
   MSG_CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,             // Порядковый номер вкладки
   MSG_CANV_ELEMENT_PROP_ALIGNMENT,                   // Местоположение объекта внутри элемента управления
//--- Вещественные свойства графических элементов

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

   {"Сверху","Top"},
   {"Снизу","Bottom"},
   {"Слева","Left"},
   {"Справа","Right"},
   
   {"По ширине текста заголовка вкладки","Fit to tab title text width"},
   {"По ширине элемента управления TabControl","Fit TabControl Width"},
   {"Фиксированный размер","Fixed size"},
   
   {"Центр координат в левом верхнем углу графика","Center of coordinates is in the upper left corner of the chart"},
   {"Центр координат в левом нижнем углу графика","Center of coordinates is in the lower left corner of the chart"},
   {"Центр координат в правом нижнем углу графика","Center of coordinates is in the lower right corner of the chart"},
   {"Центр координат в правом верхнем углу графика","Center of coordinates is in the upper right corner of the chart"},

...

   {"Элемент управления \"ButtonListBox\"","Control element \"ButtonListBox\""},
   {"Заголовок вкладки","Tab header"},
   {"Поле вкладки","Tab field"},
   {"Элемент управления \"TabControl\"","Control element \"TabControl\""},
   {"Графический объект принадлежит программе","The graphic object belongs to the program"},

...

//--- CButtonListBox
   {"Не удалось установить группу кнопке с индексом ","Failed to set group for button with index "},
   {"Не удалось установить флаг \"Переключатель\" кнопке с индексом ","Failed to set the \"Toggle\" flag on the button with index "},
   
//--- CTabControl
   {"Не удалось получить вкладку элемента управления TabControl","Failed to get tab of TabControl"},
   
//--- Целочисленные свойства графических элементов

...

   {"Несколько рядов вкладок в элементе управления","Multiple rows of tabs in a control"},
   {"Местоположение вкладок внутри элемента управления","Location of tabs inside the control"},
   {"Режим установки размера вкладок","Tab Size Mode"},
   {"Порядковый номер вкладки","Tab ordinal number"},
   {"Местоположение объекта внутри элемента управления","Location of the object inside the control"},
   
//--- Строковые свойства графических элементов


Так как у нас появился новый режим рисования заголовков вкладок, то нам нужно сделать возврат описания выбранного режима. В файле сервисных функций библиотеки \MQL5\Include\DoEasy\Services\DELib.mqh напишем функцию, возвращающую описание режима установки размера вкладок:

//+------------------------------------------------------------------+
//| Возвращает описание режима установки размера вкладок             |
//+------------------------------------------------------------------+
string TabSizeModeDescription(ENUM_CANV_ELEMENT_TAB_SIZE_MODE mode)
  {
   switch(mode)
     {
      case CANV_ELEMENT_TAB_SIZE_MODE_NORMAL :  return CMessage::Text(MSG_LIB_TEXT_TAB_SIZE_MODE_NORMAL);   break;
      case CANV_ELEMENT_TAB_SIZE_MODE_FIXED  :  return CMessage::Text(MSG_LIB_TEXT_TAB_SIZE_MODE_FIXED);    break;
      case CANV_ELEMENT_TAB_SIZE_MODE_FILL   :  return CMessage::Text(MSG_LIB_TEXT_TAB_SIZE_MODE_FILL);     break;
      default                                :  return "Unknown"; break;
     }
  }
//+------------------------------------------------------------------+

В функцию передаётся режим установки размера вкладок и, в зависимости от него, возвращается соответствующее текстовое сообщение.


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

//+------------------------------------------------------------------+
//| Возвращает описание типа графического элемента                   |
//+------------------------------------------------------------------+
string CGBaseObj::TypeElementDescription(const ENUM_GRAPH_ELEMENT_TYPE type)
  {
   return
     (
      type==GRAPH_ELEMENT_TYPE_STANDARD               ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_STANDARD)              :
      type==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED      ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)     :
      type==GRAPH_ELEMENT_TYPE_ELEMENT                ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_ELEMENT)               :
      type==GRAPH_ELEMENT_TYPE_SHADOW_OBJ             ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_SHADOW_OBJ)            :
      type==GRAPH_ELEMENT_TYPE_FORM                   ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_FORM)                  :
      type==GRAPH_ELEMENT_TYPE_WINDOW                 ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WINDOW)                :
      //--- WinForms
      type==GRAPH_ELEMENT_TYPE_WF_UNDERLAY            ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_UNDERLAY)           :
      type==GRAPH_ELEMENT_TYPE_WF_BASE                ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BASE)               :
      //--- Контейнеры
      type==GRAPH_ELEMENT_TYPE_WF_CONTAINER           ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CONTAINER)          :
      type==GRAPH_ELEMENT_TYPE_WF_GROUPBOX            ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_GROUPBOX)           :
      type==GRAPH_ELEMENT_TYPE_WF_PANEL               ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_PANEL)              :
      type==GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL         ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL)        :
      //--- Стандартные элементы управления
      type==GRAPH_ELEMENT_TYPE_WF_COMMON_BASE         ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_COMMON_BASE)        :
      type==GRAPH_ELEMENT_TYPE_WF_LABEL               ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LABEL)              :
      type==GRAPH_ELEMENT_TYPE_WF_CHECKBOX            ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CHECKBOX)           :
      type==GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON         ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON)        :
      type==GRAPH_ELEMENT_TYPE_WF_BUTTON              ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BUTTON)             :
      type==GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX   ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX)  :
      type==GRAPH_ELEMENT_TYPE_WF_LIST_BOX            ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LIST_BOX)           :
      type==GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM       ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM)      :
      type==GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX    ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX)   :
      type==GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX     ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX)    :
      //--- Вспомогательные объекты элементов управления
      type==GRAPH_ELEMENT_TYPE_WF_TAB_HEADER          ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_HEADER)         :
      type==GRAPH_ELEMENT_TYPE_WF_TAB_FIELD           ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_FIELD)          :
      "Unknown"
     );
  }  
//+------------------------------------------------------------------+


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

Чтобы избежать двойного вызова метода, в файле \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh, где расположены методы для создания имён графических элементов, в защищённой секции класса, добавим ещё один — перегруженный метод, в который будем передавать уже ранее созданное, и теперь известное имя объекта для его поиска среди других графических объектов на графике:

//--- Возвращает количество графических элементов (1) по типу, (2) по имени и типу
   int               GetNumGraphElements(const ENUM_GRAPH_ELEMENT_TYPE type) const;
   int               GetNumGraphElements(const string name,const ENUM_GRAPH_ELEMENT_TYPE type) const;
//--- Создаёт и возвращает имя графического элемента по его типу
   string            CreateNameGraphElement(const ENUM_GRAPH_ELEMENT_TYPE type);
   
private:


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

//+------------------------------------------------------------------+
//| Возвращает количество графических элементов по типу              |
//+------------------------------------------------------------------+
int CGCnvElement::GetNumGraphElements(const ENUM_GRAPH_ELEMENT_TYPE type) const
  {
//--- Объявляем переменную с количеством графических объектов и
//--- получаем общее количество графических объектов на графике и подокне, где создаётся графический элемент
   int n=0, total=::ObjectsTotal(this.ChartID(),this.SubWindow());
//--- Создаём имя графического объекта по его типу
   string name=TypeGraphElementAsString(type);
//--- В цикле по всем объектам графика и подокна
   for(int i=0;i<total;i++)
     {
      //--- получаем имя очередного объекта
      string name_obj=::ObjectName(this.ChartID(),i,this.SubWindow());
      //--- если в имени объекта нет установленного префикса имён графических объектов библиотеки, идём далее - этот объект не наш
      if(::StringFind(name_obj,this.NamePrefix())==WRONG_VALUE)
         continue;
      //--- Если в имени выбранного в цикле графического объекта есть подстрока с созданным именем объекта по его типу -
      //--- значит есть графический объект такого типа - увеличиваем счётчик объектов этого типа
      if(::StringFind(name_obj,name)>0)
         n++;
     }
//--- Возвращаем количество найденных объектов по их типу
   return n;
  }
//+------------------------------------------------------------------+
//| Возвращает количество графических элементов по имени и типу      |
//+------------------------------------------------------------------+
int CGCnvElement::GetNumGraphElements(const string name,const ENUM_GRAPH_ELEMENT_TYPE type) const
  {
//--- Объявляем переменную с количеством графических объектов и
//--- получаем общее количество графических объектов на графике и подокне, где создаётся графический элемент
   int n=0, total=::ObjectsTotal(this.ChartID(),this.SubWindow());
//--- В цикле по всем объектам графика и подокна
   for(int i=0;i<total;i++)
     {
      //--- получаем имя очередного объекта
      string name_obj=::ObjectName(this.ChartID(),i,this.SubWindow());
      //--- если в имени объекта нет установленного префикса имён графических объектов библиотеки, идём далее - этот объект не наш
      if(::StringFind(name_obj,this.NamePrefix())==WRONG_VALUE)
         continue;
      //--- Если в имени выбранного в цикле графического объекта есть подстрока с созданным именем объекта по его типу -
      //--- значит есть графический объект такого типа - увеличиваем счётчик объектов этого типа
      if(::StringFind(name_obj,name)>0)
         n++;
     }
//--- Возвращаем количество найденных объектов по их типу
   return n;
  }
//+------------------------------------------------------------------+

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

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

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

//+------------------------------------------------------------------+
//| Создаёт и возвращает имя графического элемента по его типу       |
//+------------------------------------------------------------------+
string CGCnvElement::CreateNameGraphElement(const ENUM_GRAPH_ELEMENT_TYPE type)
  {
   return this.NamePrefix()+TypeGraphElementAsString(type)+(string)this.GetNumGraphElements(type);

  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Создаёт и возвращает имя графического элемента по его типу       |
//+------------------------------------------------------------------+
string CGCnvElement::CreateNameGraphElement(const ENUM_GRAPH_ELEMENT_TYPE type)
  {
   string name=TypeGraphElementAsString(type);
   return this.NamePrefix()+name+(string)this.GetNumGraphElements(name,type);
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Создаёт графический объект-элемент                               |
//+------------------------------------------------------------------+
bool CGCnvElement::Create(const long chart_id,     // Идентификатор графика
                          const int wnd_num,       // Подокно графика
                          const int x,             // Координата X
                          const int y,             // Координата Y
                          const int w,             // Ширина
                          const int h,             // Высота
                          const bool redraw=false) // Флаг необходимости перерисовки
                         
  {
   ::ResetLastError();
   if(this.m_canvas.CreateBitmapLabel((chart_id==NULL ? ::ChartID() : chart_id),wnd_num,this.m_name,x,y,w,h,COLOR_FORMAT_ARGB_NORMALIZE))
     {
      this.Erase(CLR_CANV_NULL);
      this.m_canvas.Update(redraw);
      this.m_shift_y=(int)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_WINDOW_YDISTANCE,wnd_num);
      return true;
     }
   int err=::GetLastError();
   int code=(err==0 ? (w<1 ? MSG_CANV_ELEMENT_ERR_FAILED_SET_WIDTH : h<1 ? MSG_CANV_ELEMENT_ERR_FAILED_SET_HEIGHT : ERR_OBJECT_ERROR) : err);
   string subj=(w<1 ? "Width="+(string)w+". " : h<1 ? "Height="+(string)h+". " : "");
   CMessage::ToLog(DFUN_ERR_LINE+subj,code,true);
   
   return false;
  }
//+------------------------------------------------------------------+

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


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

В файле \MQL5\Include\DoEasy\Objects\Graph\Form.mqh класса объекта-панели, в его методе создания нового присоединённого элемента и добавления его в список присоединённых объектов, пропишем для всех подчинённых объектов (объектов, создаваемых из своих родителей) наследование этих свойств:

//+------------------------------------------------------------------+
//| Создаёт новый присоединённый элемент                             |
//| и добавляет его в список присоединённых объектов                 |
//+------------------------------------------------------------------+
CGCnvElement *CForm::CreateAndAddNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                            const int x,
                                            const int y,
                                            const int w,
                                            const int h,
                                            const color colour,
                                            const uchar opacity,
                                            const bool activity)
  {
//--- Если тип создаваемого графического элемента меньше, чем "элемент" - сообщаем об этом и возвращаем false
   if(element_type<GRAPH_ELEMENT_TYPE_ELEMENT)
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_NOT_INTENDED),::StringSubstr(::EnumToString(element_type),19));
      return NULL;
     }
//--- Задаём номер элемента в списке
   int num=this.m_list_elements.Total();
//--- Создаём описание графического элемента по умолчанию
   string descript=TypeGraphElementAsString(element_type);
//--- Получаем экранные координаты объекта относительно системы координат базового объекта
   int elm_x=x;
   int elm_y=y;
   this.GetCoords(elm_x,elm_y);
//--- Создаём новый графический элемент
   CGCnvElement *obj=this.CreateNewGObject(element_type,num,descript,elm_x,elm_y,w,h,colour,opacity,false,activity);
   if(obj==NULL)
      return NULL;
//--- и добавляем его в список привязанных графических элементов
   if(!this.AddNewElement(obj,elm_x,elm_y))
     {
      delete obj;
      return NULL;
     }
//--- Устанавливаем минимальные свойства привязанному графическому элементу
   obj.SetBackgroundColor(colour,true);
   obj.SetOpacity(opacity);
   obj.SetActive(activity);
   obj.SetMain(this.GetMain()==NULL ? this.GetObject() : this.GetMain());
   obj.SetBase(this.GetObject());
   obj.SetID(this.GetMaxIDAll()+1);
   obj.SetNumber(num);
   obj.SetCoordXRelative(obj.CoordX()-this.CoordX());
   obj.SetCoordYRelative(obj.CoordY()-this.CoordY());
   obj.SetZorder(this.Zorder(),false);
   obj.SetCoordXRelativeInit(obj.CoordXRelative());
   obj.SetCoordYRelativeInit(obj.CoordYRelative());
   obj.SetVisible(this.IsVisible(),false);
   obj.SetActive(this.Active());
   obj.SetEnabled(this.Enabled());
   return obj;
  }
//+------------------------------------------------------------------+

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

Таким же образом и обработчики событий мышки должны пропускать неактивные или скрытые объекты.

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

//+------------------------------------------------------------------+
//| Обработчик последнего события мышки                              |
//+------------------------------------------------------------------+
void CForm::OnMouseEventPostProcessing(void)
  {
   if(!this.IsVisible() || !this.Enabled())
      return;
   ENUM_MOUSE_FORM_STATE state=this.GetMouseState();
   switch(state)
     {

Если объект является таковым — просто выходим из метода.


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

if(this.BorderStyle()!=FRAME_STYLE_NONE && redraw)

В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh напишем пустой виртуальный метод для рисования рамки объекта:

//+------------------------------------------------------------------+
//| Класс объекта "форма"                                            |
//+------------------------------------------------------------------+
class CWinFormBase : public CForm
  {
protected:
   color             m_fore_color_init;                        // Первоначальный цвет текста элемента
   color             m_fore_state_on_color_init;               // Первоначальный цвет текста элемента в состоянии "ON"
private:
//--- Возвращает флаги шрифта
   uint              GetFontFlags(void);
public:
//--- Рисует рамку
   virtual void      DrawFrame(void){}
//--- Возвращает по типу (1) список, (2) количество привязанных элементов, (3) привязанный элемент по индексу в списке
   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);

Метод нужен, чтобы можно было в классах-наследниках, где требуется рисование рамки, переопределить и использовать именно этот метод, а не ранее написанные и используемые методы DrawFrameBevel(), DrawFrameFlat(), DrawFrameSimple() и DrawFrameStamp() класса объекта-формы — всё же эти методы предназначены для другого — для рисования именно определённой рамки объекта-формы. Если же для какого-либо графического элемента нужно нарисовать свою уникальную рамку, то нужно в том классе переопределить объявленный здесь метод и нарисовать с его помощью нужную рамку.


В методах Erase() теперь удалена проверка флагов обновления для рисования рамки:

//+------------------------------------------------------------------+
//| Очищает элемент с заполнением его цветом и непрозрачностью       |
//+------------------------------------------------------------------+
void CWinFormBase::Erase(const color colour,const uchar opacity,const bool redraw=false)
  {
//--- Закрашиваем элемент с указанным цветом и флагом необходимости перерисовки
   CGCnvElement::Erase(colour,opacity,redraw);
//--- Если у объекта есть рамка - рисуем её
   if(this.BorderStyle()!=FRAME_STYLE_NONE)
      this.DrawFormFrame(this.BorderSizeTop(),this.BorderSizeBottom(),this.BorderSizeLeft(),this.BorderSizeRight(),this.BorderColor(),this.Opacity(),this.BorderStyle());
//--- Обновляем элемент с указанным флагом необходимости перерисовки
   this.Update(redraw);
  }
//+------------------------------------------------------------------+
//| Очищает элемент заливкой градиентом                              |
//+------------------------------------------------------------------+
void CWinFormBase::Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false)
  {
//--- Закрашиваем элемент с указанным массивом цветов и флагом необходимости перерисовки
   CGCnvElement::Erase(colors,opacity,vgradient,cycle,redraw);
//--- Если у объекта есть рамка - рисуем её
   if(this.BorderStyle()!=FRAME_STYLE_NONE)
      this.DrawFormFrame(this.BorderSizeTop(),this.BorderSizeBottom(),this.BorderSizeLeft(),this.BorderSizeRight(),this.BorderColor(),this.Opacity(),this.BorderStyle());
//--- Обновляем элемент с указанным флагом необходимости перерисовки
   this.Update(redraw);
  }
//+------------------------------------------------------------------+

Теперь рамка рисуется всегда, если для неё задан её тип. Так сделано во всех файлах всех классов, в которых есть методы Erase.


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

      property==CANV_ELEMENT_PROP_TAB_ALIGNMENT                ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_ALIGNMENT)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+AlignmentDescription((ENUM_CANV_ELEMENT_ALIGNMENT)this.GetProperty(property))
         )  :
      property==CANV_ELEMENT_PROP_TAB_SIZE_MODE                ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_SIZE_MODE)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+TabSizeModeDescription((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)this.GetProperty(property))
         )  :
      property==CANV_ELEMENT_PROP_TAB_PAGE_NUMBER              ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_PAGE_NUMBER)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==CANV_ELEMENT_PROP_ALIGNMENT                    ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_ALIGNMENT)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+AlignmentDescription((ENUM_CANV_ELEMENT_ALIGNMENT)this.GetProperty(property))
         )  :
      ""
     );
  }
//+------------------------------------------------------------------+
//| Возвращает описание вещественного свойства элемента              |
//+------------------------------------------------------------------+

Теперь любой графический элемент сможет вернуть строку с описанием указанного нового свойства и его значения.


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

this.Redraw(false);

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

Вот если представить цепочку иерархии объекта: Obj0 --> Obj1 --> Obj2 --> Obj3 --> Obj4 --> ... ... --> ObjN, где Obj0 — это самый первый объект в иерархии, а ObjN — самый последний, который и создаётся, то при его создании будут поочерёдно вызваны все конструкторы всей цепочки наследования. И если в каждом из них будет стоять указанная строка с обновлением, то каждый раз объект будет перерисован.

Уберём эти строки из всех защищённых конструкторов всех классов.

Как пример, в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\CommonBase.mqh:

//+------------------------------------------------------------------+
//| Защищённый конструктор с указанием типа объекта,                 |
//| идентификатора чарта и подокна                                   |
//+------------------------------------------------------------------+
CCommonBase::CCommonBase(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) : CWinFormBase(type,chart_id,subwindow,descript,x,y,w,h)
  {
//--- Установим объекту указанный тип графического элемента, а тип объекта библиотеки - как тип этого объекта
   this.SetTypeElement(type);
   this.m_type=OBJECT_DE_TYPE_GWF_COMMON;
   this.SetCoordX(x);
   this.SetCoordY(y);
   this.SetWidth(w);
   this.SetHeight(h);
   this.Initialize();
   if(this.AutoSize())
      this.AutoSetWH();
   this.SetWidthInit(this.Width());
   this.SetHeightInit(this.Height());
   this.SetCoordXInit(x);
   this.SetCoordYInit(y);
   this.Redraw(false);
  }
//+------------------------------------------------------------------+

Такие же изменения уже сделаны в классах:
CLabel
в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Label.mqh и CButton в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Button.mqh.

Здесь же, в этом же файле класса CCommonBase сделаем вышеозвученные изменения в методах Erase() по удалению проверки флага перерисовки:

//+------------------------------------------------------------------+
//| Очищает элемент с заполнением его цветом и непрозрачностью       |
//+------------------------------------------------------------------+
void CCommonBase::Erase(const color colour,const uchar opacity,const bool redraw=false)
  {
//--- Закрашиваем элемент с указанным цветом и флагом необходимости перерисовки
   CGCnvElement::Erase(colour,opacity,redraw);
//--- Если у объекта есть рамка - рисуем её
   if(this.BorderStyle()!=FRAME_STYLE_NONE)
      this.DrawFormFrame(this.BorderSizeTop(),this.BorderSizeBottom(),this.BorderSizeLeft(),this.BorderSizeRight(),this.BorderColor(),255,this.BorderStyle());
//--- Обновляем элемент с указанным флагом необходимости перерисовки
   this.Update(redraw);
  }
//+------------------------------------------------------------------+
//| Очищает элемент заливкой градиентом                              |
//+------------------------------------------------------------------+
void CCommonBase::Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false)
  {
//--- Закрашиваем элемент с указанным массивом цветов и флагом необходимости перерисовки
   CGCnvElement::Erase(colors,opacity,vgradient,cycle,redraw);
//--- Если у объекта есть рамка - рисуем её
   if(this.BorderStyle()!=FRAME_STYLE_NONE)
      this.DrawFormFrame(this.BorderSizeTop(),this.BorderSizeBottom(),this.BorderSizeLeft(),this.BorderSizeRight(),this.BorderColor(),255,this.BorderStyle());
//--- Обновляем элемент с указанным флагом необходимости перерисовки
   this.Update(redraw);
  }
//+------------------------------------------------------------------+

Далее в других файлах других классов мы эти изменения описывать не будем.


Так как теперь для отрисовки рамки объекта не нужно принудительно отправлять в метод установленный флаг перерисовки, то в файле класса объекта-кнопки \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Button.mqh, в методе перерисовки объекта укажем не принудительно true, как это было раньше и что вызывало перерисовку всего графика, а передадим флаг redraw, в свою очередь передаваемый в метод извне и от которого будет зависеть необходимость перерисовки:

//+------------------------------------------------------------------+
//| Перерисовывает объект                                            |
//+------------------------------------------------------------------+
void CButton::Redraw(bool redraw)
  {
//--- Заполняем объект цветом фона
   this.Erase(this.BackgroundColor(),this.Opacity(),redraw);
//--- Объявляем переменные для координат X и Y и устанавливаем их значения в зависимости от выравнивания текста
   int x=0,y=0;
   CLabel::SetTextParamsByAlign(x,y);
//--- Рисуем текст в установленных координатах объекта и точкой привязки текста и обновляем объект 
   this.Text(x,y,this.Text(),this.ForeColor(),this.ForeColorOpacity(),this.TextAnchor());
   this.Update(redraw);
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Устанавливает состояние кнопки как "отжато"                      |
//| для всех Button одной группы в контейнере                        |
//+------------------------------------------------------------------+
void CButton::UnpressOtherAll(void)
  {
//--- Получаем указатель на базовый объект
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
//--- Из базового объекта получаем список всех объектов с типом этого объекта (Button или его наследник)
   CArrayObj *list=base.GetListElementsByType(this.TypeGraphElement());
//--- Из полученного списка выбираем все объекты, кроме данного (имена выбранных объектов не равны имени этого)
   list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,this.Name(),NO_EQUAL);
//--- Из полученного списка выбираем только те объекты, у которых номер группы совпадает с группой этого
   list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_GROUP,this.Group(),EQUAL);
//--- Если список объектов получен,
   if(list!=NULL)
     {
      //--- в цикле по всем объектам в списке
      for(int i=0;i<list.Total();i++)
        {
         //--- получаем очередной объект,
         CButton *obj=list.At(i);
         if(obj==NULL)
            continue;
         //--- устанавливаем кнопке состояние "отжата",
         obj.SetState(false);
         //--- устанавливаем цвет фона изначальным (курсор находится на другой кнопке за пределами этой)
         obj.SetBackgroundColor(obj.BackgroundColorInit(),false);
         obj.SetForeColor(obj.ForeColorInit(),false);
         obj.SetBorderColor(obj.BorderColorInit(),false);
         //--- Перерисовываем объект для отображения изменений
         obj.Redraw(false);
        }
     }
   ::ChartRedraw(this.ChartID());
  }
//+------------------------------------------------------------------+


В обработчике последнего события мышки, в самом его начале, добавим проверку невидимости или недоступности элемента, как это уже делали выше для объекта-формы:

//+------------------------------------------------------------------+
//| Обработчик последнего события мышки                              |
//+------------------------------------------------------------------+
void CButton::OnMouseEventPostProcessing(void)
  {
   if(!this.IsVisible() || !this.Enabled())
      return;
   ENUM_MOUSE_FORM_STATE state=GetMouseState();
   switch(state)
     {

Такие же доработки уже сделаны в классах:
CCheckBox в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\CheckBox.mqh и CTabHeader в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\TabHeader.mqh.

Далее такие изменения уже рассматривать не будем.


Объект TabField — поле вкладки элемента управления TabControl

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

В прошлой статье мы для реализации поля вкладки использовали объект-контейнер. Это базовый объект для всех объектов-контейнеров в библиотеке. На поле вкладки должны располагаться подчинённые объекты, создаваемые на этом поле, и, соответственно, подчинённые ему. Функционала базового объекта-контейнера для реализации работы с полем, естественно, недостаточно. Поэтому на основе базового объекта-контейнера создадим новый класс объекта-поля вкладки.

В папке библиотеки \MQL5\Include\DoEasy\Objects\Graph\WForms\ создадим новый файл TabField.mqh класса CTabField. Класс должен быть унаследован от базового класса объекта-контейнера. Файл объекта-панели должен быть подключен к файлу создаваемого класса, что даст к нему доступ всем файлам графических объектов библиотеки:

//+------------------------------------------------------------------+
//|                                                     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    |
//+------------------------------------------------------------------+
class CTabField : public CContainer
  {
  }


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

//+------------------------------------------------------------------+
//| Класс объекта TabHeader элемента управления WForms TabControl    |
//+------------------------------------------------------------------+
class CTabField : public CContainer
  {
private:
//--- Находит и возвращает указатель на объект заголовка, соответствующий номеру этой вкладки
   CWinFormBase     *GetHeaderObj(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);
protected:

Так как если в этом файле указать тип объекта-заголовка именно его типом (CTabHeader), который виден в этом классе, и попытаться скомпилировать всю библиотеку компиляцией главного класса библиотеки CEngine, то получим большое количество ошибок и предупреждений о неизвестности типа CTabHeader. Голову ломать не будем в поисках, где там что "застряло" во всех хитросплетениях всех файлов библиотеки, а просто объявим возвращаемый тип как базовый объект всех WinForms-объектов библиотеки. Этого будет достаточно для работы с ним здесь. А за пределами этого класса мы его уже можем получить отсюда с его правильным типом.

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

В защищённой секции класса объявим защищённый конструктор:

protected:
//--- Защищённый конструктор с указанием типа объекта, идентификатора чарта и подокна
                     CTabField(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);
public:


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

public:
//--- Рисует рамку поля в зависимости от расположения заголовка
   virtual void      DrawFrame(void);
//--- (1) Устанавливает, (2) возвращает номер вкладки
   void              SetPageNumber(const int value)         { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,value);         }
   int               PageNumber(void)                 const { return (int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER);   }
//--- Очищает элемент с заполнением его цветом и непрозрачностью
   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);
//--- Конструкторы
                     CTabField(const long chart_id,
                               const int subwindow,
                               const string descript,
                               const int x,
                               const int y,
                               const int w,
                               const int h);
  };
//+------------------------------------------------------------------+

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


Защищённый и параметрический конструкторы:

//+------------------------------------------------------------------+
//| Защищённый конструктор с указанием типа объекта,                 |
//| идентификатора чарта и подокна                                   |
//+------------------------------------------------------------------+
CTabField::CTabField(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) : CContainer(type,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD);
   this.m_type=OBJECT_DE_TYPE_GWF_CONTAINER;
   this.SetBorderSizeAll(1);
   this.SetBorderStyle(FRAME_STYLE_SIMPLE);
   this.SetOpacity(CLR_DEF_CONTROL_TAB_PAGE_OPACITY,true);
   this.SetBackgroundColor(CLR_DEF_CONTROL_TAB_PAGE_BACK_COLOR,true);
   this.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_DOWN);
   this.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_OVER);
   this.SetBorderColor(CLR_DEF_CONTROL_TAB_PAGE_BORDER_COLOR,true);
   this.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_DOWN);
   this.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_OVER);
   this.SetForeColor(CLR_DEF_FORE_COLOR,true);
   this.SetPaddingAll(3);
  }
//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CTabField::CTabField(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_FIELD,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD);
   this.m_type=OBJECT_DE_TYPE_GWF_CONTAINER;
   this.SetBorderSizeAll(1);
   this.SetBorderStyle(FRAME_STYLE_SIMPLE);
   this.SetOpacity(CLR_DEF_CONTROL_TAB_PAGE_OPACITY,true);
   this.SetBackgroundColor(CLR_DEF_CONTROL_TAB_PAGE_BACK_COLOR,true);
   this.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_DOWN);
   this.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_OVER);
   this.SetBorderColor(CLR_DEF_CONTROL_TAB_PAGE_BORDER_COLOR,true);
   this.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_DOWN);
   this.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_OVER);
   this.SetForeColor(CLR_DEF_FORE_COLOR,true);
   this.SetPaddingAll(3);
  }
//+------------------------------------------------------------------+

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

В теле конструктора созданному объекту устанавливаются нужные значения некоторых свойств по умолчанию. Остальные свойства объекта устанавливаются в родительских классах.

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

//+------------------------------------------------------------------+
//| Находит и возвращает указатель на заголовок,                     |
//| соответствующий номеру вкладки                                   |
//+------------------------------------------------------------------+
CWinFormBase *CTabField::GetHeaderObj(void)
  {
//--- Получаем указатель на базовый объект
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return NULL;
//--- Из базового объекта получаем список объектов-заголовков вкладок
   CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
//--- Оставляем в полученном списке только объект, номер вкладки которого совпадает с этим
   list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,this.PageNumber(),EQUAL);
//--- Возвращаем из списка указатель на найденный объект
   return(list!=NULL ? list.At(0) : NULL);
  }
//+------------------------------------------------------------------+

Метод подробно расписан в комментариях к коду. Вкратце: для сопоставления этого поля со своим заголовком, хранящимся в классе, к которому привязан этот объект и объекты-заголовки, нам нужно получить доступ к базовому объекту. Такой метод давно у нас существует. Получаем указатель на базовый объект, и из него получаем список всех привязанных к нему объектов с типом объекта-заголовка вкладки. Фильтруем полученный список так, чтобы в нём остался один объект с номером вкладки, записанном в этом объекте. Таким образом мы нашли соответствующий этому полю заголовок, и указатель на него хранится в полученном списке. Его и возвращаем. Если объект не найден, то метод вернёт NULL.


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

//+------------------------------------------------------------------+
//| Рисует рамку элемента в зависимости от расположения заголовка    |
//+------------------------------------------------------------------+
void CTabField::DrawFrame(void)
  {
//--- Устанавливаем начальные координаты
   int x1=0;
   int y1=0;
   int x2=this.Width()-1;
   int y2=this.Height()-1;
//--- Получаем заголовок вкладки, соответствующий данному полю
   CTabHeader *header=this.GetHeaderObj();
   if(header==NULL)
      return;
//--- Рисуем прямоугольник, полностью очерчивающий поле
   this.DrawRectangle(x1,y1,x2,y2,this.BorderColor(),this.Opacity());
//--- В зависимости от расположения заголовка рисуем линию на прилегающей к заголовку грани грани.
//--- Размер линии рассчитывается от размеров заголовка и соответствует им с отступом в один пиксель с каждой стороны
//--- таким образом визуально грань поля не будет нарисована на прилегающей стороне заголовка
   switch(header.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
        this.DrawLine(header.CoordXRelative()+1,0,header.RightEdgeRelative()-2,0,this.BackgroundColor(),this.Opacity());
        break;
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
        this.DrawLine(header.CoordXRelative()+1,this.Height()-1,header.RightEdgeRelative()-2,this.Height()-1,this.BackgroundColor(),this.Opacity());
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
        // { }
        break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
        // { }
        break;
      default:
        break;
     }
  }
//+------------------------------------------------------------------+

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

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

Виртуальные методы, очищающие элемент с заполнением его цветом и непрозрачностью:

//+------------------------------------------------------------------+
//| Очищает элемент с заполнением его цветом и непрозрачностью       |
//+------------------------------------------------------------------+
void CTabField::Erase(const color colour,const uchar opacity,const bool redraw=false)
  {
//--- Закрашиваем элемент с указанным цветом и флагом необходимости перерисовки
   CGCnvElement::Erase(colour,opacity,redraw);
//--- Если у объекта есть рамка - рисуем её
   if(this.BorderStyle()!=FRAME_STYLE_NONE)
      this.DrawFrame();
//--- Обновляем элемент с указанным флагом необходимости перерисовки
   this.Update(redraw);
  }
//+------------------------------------------------------------------+
//| Очищает элемент заливкой градиентом                              |
//+------------------------------------------------------------------+
void CTabField::Erase(color &colors[],const uchar opacity,const bool vgradient,const bool cycle,const bool redraw=false)
  {
//--- Закрашиваем элемент с указанным массивом цветов и флагом необходимости перерисовки
   CGCnvElement::Erase(colors,opacity,vgradient,cycle,redraw);
//--- Если у объекта есть рамка - рисуем её
   if(this.BorderStyle()!=FRAME_STYLE_NONE)
      this.DrawFrame();
//--- Обновляем элемент с указанным флагом необходимости перерисовки
   this.Update(redraw);
  }
//+------------------------------------------------------------------+

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

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

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

Из приватной секции удалим метод для установки размеров и смещения объекта:

//--- Устанавливает ширину, высоту и смещение элемента в зависимости от состояния
   void              SetWH(void);


Там же объявим методы для установки строки выделенного заголовка вкладки в корректное положение:

private:
   int               m_width_off;                        // Ширина объекта в состоянии "не выбран"
   int               m_height_off;                       // Высота объекта в состоянии "не выбран"
   int               m_width_on;                         // Ширина объекта в состоянии "выбран"
   int               m_height_on;                        // Высота объекта в состоянии "выбран"
   int               m_col;                              // Номер колонки заголовка
   int               m_row;                              // Номер строки заголовка
//--- Подстраивает размер и расположение элемента в зависимости от состояния
   bool              WHProcessStateOn(void);
   bool              WHProcessStateOff(void);
//--- Рисует рамку заголовка в зависимости от его расположения
   virtual void      DrawFrame(void);
//--- Устанавливает строку выделенного заголовка вкладки в корректное положение, (1) сверху, (2) снизу, (3) слева, (4) справа
   void              CorrectSelectedRowTop(void);
   void              CorrectSelectedRowBottom(void);
   void              CorrectSelectedRowLeft(void);
   void              CorrectSelectedRowRight(void);
   
protected:


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

public:
//--- Находит и возвращает указатель на объект поля, соответствующий номеру вкладки
   CWinFormBase     *GetFieldObj(void);
//---Устанавливает размеры элемента управления в состоянии (1) "не выбран", (2) "выбран"
   bool              SetSizeOff(void)  { return(CGCnvElement::SetWidth(this.m_width_off) && CGCnvElement::SetHeight(this.m_height_off) ? true : false);  }
   bool              SetSizeOn(void)   { return(CGCnvElement::SetWidth(this.m_width_on) && CGCnvElement::SetHeight(this.m_height_on)   ? true : false);  }
//--- Устанавливает размеры элемента управления
   void              SetWidthOff(const int value)                                            { this.m_width_off=value;  }
   void              SetHeightOff(const int value)                                           { this.m_height_off=value; }
   void              SetWidthOn(const int value)                                             { this.m_width_on=value;   }
   void              SetHeightOn(const int value)                                            { this.m_height_on=value;  }
//--- Устанавливает все размеры элемента управления
   bool              SetSizes(const int w,const int h);
//--- Возвращает размеры элемента управления
   int               WidthOff(void)                                                    const { return this.m_width_off; }
   int               HeightOff(void)                                                   const { return this.m_height_off;}
   int               WidthOn(void)                                                     const { return this.m_width_on;  }
   int               HeightOn(void)                                                    const { return this.m_height_on; }
//--- (1) Устанавливает, (2) возвращает номер строки заголовка вкладки
   void              SetRow(const int value)                                                 { this.m_row=value;        }
   int               Row(void)                                                         const { return this.m_row;       }
//--- (1) Устанавливает, (2) возвращает номер колонки заголовка вкладки
   void              SetColumn(const int value)                                              { this.m_col=value;        }
   int               Column(void)                                                      const { return this.m_col;       }
//--- Устанавливает местоположение вкладки
   void              SetTabLocation(const int row,const int col)
                       {
                        this.SetRow(row);
                        this.SetColumn(col);
                       }
//--- (1) Устанавливает, (2) возвращает местоположение объекта на элементе управления
   void              SetAlignment(const ENUM_CANV_ELEMENT_ALIGNMENT alignment)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT,alignment);
                       }
   ENUM_CANV_ELEMENT_ALIGNMENT Alignment(void)  const { return (ENUM_CANV_ELEMENT_ALIGNMENT)this.GetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT);  }
   
//--- (1) Устанавливает, (2) возвращает режим установки размера вкладок
   void              SetTabSizeMode(const ENUM_CANV_ELEMENT_TAB_SIZE_MODE mode)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_TAB_SIZE_MODE,mode);
                       }
   ENUM_CANV_ELEMENT_TAB_SIZE_MODE TabSizeMode(void)const{ return (ENUM_CANV_ELEMENT_TAB_SIZE_MODE)this.GetProperty(CANV_ELEMENT_PROP_TAB_SIZE_MODE);}

//--- (1) Устанавливает, (2) возвращает номер вкладки
   void              SetPageNumber(const int value)         { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,value);         }
   int               PageNumber(void)                 const { return (int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER);   }

//--- Устанавливает состояние элемента управления
   virtual void      SetState(const bool flag);

//--- Очищает элемент с заполнением его цветом и непрозрачностью
   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      OnMouseEventPostProcessing(void);
   
//--- Устанавливает промежуток (1) слева, (2) сверху, (3) справа, (4) снизу, (5) со всех сторон внутри элемента управления
   virtual void      SetPaddingLeft(const uint value)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_PADDING_LEFT,(value<1 ? 0 : value));
                       }
   virtual void      SetPaddingTop(const uint value)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_PADDING_TOP,(value<1 ? 0 : value));
                       }
   virtual void      SetPaddingRight(const uint value)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_PADDING_RIGHT,(value<1 ? 0 : value));
                       }
   virtual void      SetPaddingBottom(const uint value)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_PADDING_BOTTOM,(value<1 ? 0 : value));
                       }
   virtual void      SetPaddingAll(const uint value)
                       {
                        this.SetPaddingLeft(value); this.SetPaddingTop(value); this.SetPaddingRight(value); this.SetPaddingBottom(value);
                       }
   virtual void      SetPadding(const int left,const int top,const int right,const int bottom)
                       {
                        this.SetPaddingLeft(left); this.SetPaddingTop(top); this.SetPaddingRight(right); this.SetPaddingBottom(bottom);
                       }
protected:
//--- Защищённый конструктор с указанием типа объекта, идентификатора чарта и подокна

Ранее метод SetAlignment() помимо установки свойства, устанавливал и размеры рамки. Рамка здесь всегда одного размера — 1 пиксель, и поэтому менять ничего не нужно — удалим это всё:

//--- (1) Устанавливает, (2) возвращает местоположение объекта на элементе управления
   void              SetAlignment(const ENUM_CANV_ELEMENT_ALIGNMENT alignment)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT,alignment);
                        if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_TOP)
                           this.SetBorderSize(1,1,1,0);
                        if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_BOTTOM)
                           this.SetBorderSize(1,0,1,1);
                        if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT)
                           this.SetBorderSize(1,1,0,1);
                        if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_RIGHT)
                           this.SetBorderSize(0,1,1,1);
                       }


Конструкторы класса, защищённый и параметрический:

//+------------------------------------------------------------------+
//| Защищённый конструктор с указанием типа объекта,                 |
//| идентификатора чарта и подокна                                   |
//+------------------------------------------------------------------+
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_COMMON;
   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);
  }
//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
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_COMMON;
   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.SetWidthOff(this.Width());
   this.SetHeightOff(this.Height());
   this.SetWidthOn(this.Width()+4);
   this.SetHeightOn(this.Height()+2);

вызывается метод установки размеров заголовка вкладки, где устанавливаются размеры в зависимости от режима задания размера заголовка:

//+------------------------------------------------------------------+
//| Устанавливает все размеры заголовка                              |
//+------------------------------------------------------------------+
bool CTabHeader::SetSizes(const int w,const int h)
  {
//--- Если переданные ширина или высота меньше 4 пикселей, 
//--- делаем их равными четырём пикселям
   int width=(w<4 ? 4 : w);
   int height=(h<4 ? 4 : h);
//--- В зависимости от режима задания размера заголовка
   switch(this.TabSizeMode())
     {
      //--- устанавливаем ширину и высоту для режима Normal
      case CANV_ELEMENT_TAB_SIZE_MODE_NORMAL :
        this.TextSize(this.Text(),width,height);
        width+=this.PaddingLeft()+this.PaddingRight();
        height=h+this.PaddingTop()+this.PaddingBottom();
        break;
      //--- Для режимов Fixed и Fill размеры пока остаются
      //--- переданными в метод и скорректированными по размерам
      case CANV_ELEMENT_TAB_SIZE_MODE_FIXED  : break;
      //---CANV_ELEMENT_TAB_SIZE_MODE_FILL
      default: break;
     }
//--- Записываем в res результаты изменения ширины и высоты
   bool res=true;
   res &=this.SetWidth(width);
   res &=this.SetHeight(height);
//--- Если ошибка изменения ширины или высоты - возвращаем false
   if(!res)
      return false;
//--- Устанавливаем изменённые размеры для разных состояний кнопки
   this.SetWidthOn(this.Width()+4);
   this.SetHeightOn(this.Height()+2);
   this.SetWidthOff(this.Width());
   this.SetHeightOff(this.Height());
   return true;
  }
//+------------------------------------------------------------------+

Логика метода расписана в комментариях к коду. Размеры подстраиваются только для режима, где ширина заголовка соответствует ширине выводимого на нём текста. Для режима Fixed размер заголовка должен быть фиксированным, поэтому он остаётся тем, который был передан в метод в переменных w и h, но скорректированным, если переданы размеры меньше четырёх пикселей (в переменных width и height). Режим растягивания ширины по размеру контейнера будем делать в последующих статьях.

Метод, устанавливающий состояние элемента управления, претерпел сильные изменения:

//+------------------------------------------------------------------+
//| Устанавливает состояние элемента управления                      |
//+------------------------------------------------------------------+
void CTabHeader::SetState(const bool flag)
  {
//--- Получаем состояние кнопки и устанавливаем новое, переданное в метод
   bool state=this.State();
   CButton::SetState(flag);
//--- Если прошлое состояние кнопки не соответствует установленному
   if(state!=this.State())
     {
      //--- Если кнопка нажата
      if(this.State())
        {
         //--- Вызывем метод изменения размера кнопки и выводим её на передний план
         this.WHProcessStateOn();
         this.BringToTop();
         //--- Получаем базовый объект, к которому прикреплён заголовок вкладки (TabControl)
         CWinFormBase *base=this.GetBase();
         if(base==NULL)
            return;
         //--- Устанавливаем в объект TabControl номер выбранной вкладки
         base.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,this.PageNumber());
         //--- Получаем из базового объекта список объектов-полей вкладок
         CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD);
         if(list==NULL)
            return;
         //--- В цикле по полученному списку скрываем все поля, не соответствующие заголовку
         for(int i=0;i<list.Total();i++)
           {
            //--- получаем очередной объект-поле вкладки
            CWinFormBase *obj=list.At(i);
            //--- Если объект не получен, либо соответствует выбранному заголовку - идём дальше
            if(obj==NULL || obj.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER)==this.PageNumber())
               continue;
            //--- Устанавливаем полю вкладки ZOrder как у базового объекта и скрываем поле
            obj.SetZorder(base.Zorder(),false);
            obj.Hide();
           }
         //--- Получаем объект-поле, соответствующий заголовку поля (этому объекту)
         CWinFormBase *field=this.GetFieldObj();
         if(field==NULL)
            return;
         //--- Отображаем поле и устанавливаем ему ZOrder выше, чем у остальных полей объекта TabControl,
         //--- рисуем рамку объекта-поля и переносим его на передний план
         field.Show();
         field.SetZorder(base.Zorder()+1,false);
         field.DrawFrame();
         field.BringToTop();
        }
      //--- Если кнопка не нажата - вызываем метод восстановления размера заголовка
      else
         this.WHProcessStateOff();
     }
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Подстраивает размер и расположение элемента                      |
//| в состоянии "выбран" в зависимости от его расположения           |
//+------------------------------------------------------------------+
bool CTabHeader::WHProcessStateOn(void)
  {
//--- Если не удалось установить новый размер - уходим
   if(!this.SetSizeOn())
      return false;
//--- Получаем базовый объект
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return false;
//--- В зависимости от расположения заголовка
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
        //--- Корректируем расположение строки с выбранным заголовком
        this.CorrectSelectedRowTop();
        //--- смещаем заголовок на два пикселя в новые координаты пасположения и
        //--- устанавливаем новые относительные координаты
        if(this.Move(this.CoordX()-2,this.CoordY()-2))
          {
           this.SetCoordXRelative(this.CoordXRelative()-2);
           this.SetCoordYRelative(this.CoordYRelative()-2);
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
        //--- Корректируем расположение строки с выбранным заголовком
        this.CorrectSelectedRowBottom();
        //--- смещаем заголовок на два пикселя в новые координаты пасположения и
        //--- устанавливаем новые относительные координаты
        if(this.Move(this.CoordX()-2,this.CoordY()))
          {
           this.SetCoordXRelative(this.CoordXRelative()-2);
           this.SetCoordYRelative(this.CoordYRelative());
          }
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
        //--- Корректируем расположение строки с выбранным заголовком
        this.CorrectSelectedRowLeft();
        //--- смещаем заголовок на два пикселя в новые координаты пасположения и
        //--- устанавливаем новые относительные координаты
        // {   }
        break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
        //--- Корректируем расположение строки с выбранным заголовком
        this.CorrectSelectedRowRight();
        //--- смещаем заголовок на два пикселя в новые координаты пасположения и
        //--- устанавливаем новые относительные координаты
        // {   }
        break;
      default:
        break;
     }
   return true;
  }
//+------------------------------------------------------------------+

Расположение заголовков вкладок слева и справа мы здесь пока не обрабатываем — будем делать в последующих статьях.


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

//+------------------------------------------------------------------+
//| Подстраивает размер и расположение элемента                      |
//| в состоянии "не выбран" в зависимости от его расположения        |
//+------------------------------------------------------------------+
bool CTabHeader::WHProcessStateOff(void)
  {
//--- Если не удалось установить новый размер - уходим
   if(!this.SetSizeOff())
      return false;
//--- В зависимости от расположения заголовка
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
        //--- смещаем заголовок на прежнее место и устанавливаем прежние относительные координаты
        if(this.Move(this.CoordX()+2,this.CoordY()+2))
          {
           this.SetCoordXRelative(this.CoordXRelative()+2);
           this.SetCoordYRelative(this.CoordYRelative()+2);
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
        //--- смещаем заголовок на прежнее место и устанавливаем прежние относительные координаты
        if(this.Move(this.CoordX()+2,this.CoordY()))
          {
           this.SetCoordXRelative(this.CoordXRelative()+2);
           this.SetCoordYRelative(this.CoordYRelative());
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
        //--- смещаем заголовок на прежнее место и устанавливаем прежние относительные координаты
        // {   }
        break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
        //--- смещаем заголовок на прежнее место и устанавливаем прежние относительные координаты
        // {   }
        break;
      default:
        break;
     }
   return true;
  }
//+------------------------------------------------------------------+

Это задел на последующие доработки.


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

//+------------------------------------------------------------------+
//| Устанавливает строку выделенного заголовка вкладки               |
//| в корректное положение сверху                                    |
//+------------------------------------------------------------------+
void CTabHeader::CorrectSelectedRowTop(void)
  {
   int row_pressed=this.Row();      // Строка выбранного заголовка
   int y_pressed=this.CoordY();     // Координата куда перенести все заголовки с Row(), равным нулю
   int y0=0;                        // Координата нулевой строки (Row == 0)
//--- Если выбрана нулевая строка - ничего делать не нужно - уходим
   if(row_pressed==0)
      return;
      
//--- Получаем объект-поле вкладки, соответствующий этому заголовку, и устанавливаем Y-координату нулевой строки
   CWinFormBase *obj=this.GetFieldObj();
   if(obj==NULL)
      return;
   y0=obj.CoordY()-this.Height()+2;
   
//--- Получаем базовый объект (TabControl)
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
//--- Из базового объекта получаем список всех заголовков вкладок
   CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
   if(list==NULL)
      return;
//--- В цикле по всем заголовкам меняем местами строки -
//--- строку выбранного заголовка устанавливаем на место нулевой, нулевую - на место строки выбранного заголовка
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- Если это нулевая строка
      if(header.Row()==0)
        {
         //--- перемещаем заголовок на место строки выбранного
         if(header.Move(header.CoordX(),y_pressed))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            //--- В качестве метки перемещённой нулевой строки на место выбранной, зададим значение Row как -1
            header.SetRow(-1);
           }
        }
      //--- Если это строка нажатого заголовка
      if(header.Row()==row_pressed)
        {
         //--- перемещаем заголовок на место нулевой строки
         if(header.Move(header.CoordX(),y0))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            //--- В качестве метки перемещённой выбранной строки на место нулевой, зададим значение Row как -2
            header.SetRow(-2);
           }
        }
     }
//--- Устанавливаем правильные Row и Col
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- Если это бывшая нулевая строка, перемещённая на место выбранной, ставим ей Row выбранной строки
      if(header.Row()==-1)
         header.SetRow(row_pressed);
      //--- Если это выбранная строка, перемещённая на место нулевой - ставим Row нулевой строки
      if(header.Row()==-2)
         header.SetRow(0);
     }
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Устанавливает строку выделенного заголовка вкладки               |
//| в корректное положение снизу                                     |
//+------------------------------------------------------------------+
void CTabHeader::CorrectSelectedRowBottom(void)
  {
  
  }
//+------------------------------------------------------------------+
//| Устанавливает строку выделенного заголовка вкладки               |
//| в корректное положение слева                                     |
//+------------------------------------------------------------------+
void CTabHeader::CorrectSelectedRowLeft(void)
  {
  
  }
//+------------------------------------------------------------------+
//| Устанавливает строку выделенного заголовка вкладки               |
//| в корректное положение справа                                    |
//+------------------------------------------------------------------+
void CTabHeader::CorrectSelectedRowRight(void)
  {
  
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Находит и возвращает указатель на объект поля,                   |
//| соответствующий номеру вкладки                                   |
//+------------------------------------------------------------------+
CWinFormBase *CTabHeader::GetFieldObj(void)
  {
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return NULL;
   CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD);
   list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,this.PageNumber(),EQUAL);
   return(list!=NULL ? list.At(0) : NULL);
  }
//+------------------------------------------------------------------+

Метод идентичен методу GetHeaderObj(), ищущему и возвращающему указатель на заголовок вкладки, рассмотренному нами выше в классе объекта-поля вкладки. Этот метод ищет поле вкладки, соответствующее этому заголовку.


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

//+------------------------------------------------------------------+
//| Обработчик события Курсор в пределах активной области,           |
//| отжата кнопка мышки (левая)                                      |
//+------------------------------------------------------------------+
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();
           }
         //--- Перерисовываем объект и график
         this.Redraw(true);
        }
      //--- Выводим тестовое сообщение в журнал
      Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.State()=",this.State(),", ID=",this.ID(),", Group=",this.Group());
      //--- Устанавливаем цвет рамки для состояния "Курсор мышки над активной зоной"
      this.SetBorderColor(this.BorderColorMouseOver(),false);
     }
  }
//+------------------------------------------------------------------+

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


Всё управление заголовками и полями вкладок должно осуществляться из класса элемента управления 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 "..\TabHeader.mqh"
#include "..\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 полей вкладок справа
//--- Создаёт новый графический объект
   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) полей вкладок
   CArrayObj        *GetListHeaders(void)                { return this.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);  }
   CArrayObj        *GetListFields(void)                 { return this.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD);   }
//--- Устанавливает вкладку выбранной
   void              SetSelected(const int index);
//--- Устанавливает вкладку не выбранной
   void              SetUnselected(const int index);
//--- Устанавливает номер выбранной вкладки
   void              SetSelectedTabPageNum(const int value) { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,value);         }
//--- Располагает заголовки вкладок в соответствии с установленными режимами
   void              ArrangeTabHeaders(void);
//--- Располагает заголовки вкладок (1) сверху, (2) снизу, (3) слева, (4) справа
   void              ArrangeTabHeadersTop(void);
   void              ArrangeTabHeadersBottom(void);
   void              ArrangeTabHeadersLeft(void);
   void              ArrangeTabHeadersRight(void);
public:


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

public:
//--- Создаёт указанное количество вкладок
   bool              CreateTabPages(const int total,const int selected_page,const int tab_w=0,const int tab_h=0,const string header_text="");
   
//--- Создаёт новый присоединённый элемент
   bool              CreateNewElement(const int tab_page,
                                      const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                      const int x,
                                      const int y,
                                      const int w,
                                      const int h,
                                      const color colour,
                                      const uchar opacity,
                                      const bool activity,
                                      const bool redraw);
 
//--- Возвращает количество (1) привязанных элементов, (2) привязанный элемент по индексу в списке в указанной вкладке
   int               TabElementsTotal(const int tab_page);
   CGCnvElement     *GetTabElement(const int tab_page,const int index);
   
//--- Возвращает по типу (1) список, (2) количество привязанных элементов, (3) привязанный элемент по индексу в списке в указанной вкладке
   CArrayObj        *GetListTabElementsByType(const int tab_page,const ENUM_GRAPH_ELEMENT_TYPE type);
   int               TabElementsTotalByType(const int tab_page,const ENUM_GRAPH_ELEMENT_TYPE type);
   CGCnvElement     *GetTabElementByType(const int tab_page,const ENUM_GRAPH_ELEMENT_TYPE type,const int index);
   
//--- Возвращает указатель на (1) заголовок, (2) поле вкладки (3) количество вкладок
   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); }
//--- (1) Устанавливает, (2) возвращает местоположение вкладок на элементе управления
   void              SetAlignment(const ENUM_CANV_ELEMENT_ALIGNMENT alignment)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT,alignment);
                        CArrayObj *list=this.GetListHeaders();
                        if(list==NULL)
                           return;
                        for(int i=0;i<list.Total();i++)
                          {
                           CTabHeader *header=list.At(i);
                           if(header==NULL)
                              continue;
                           header.SetAlignment(alignment);
                          }
                       }
   ENUM_CANV_ELEMENT_ALIGNMENT Alignment(void)  const { return (ENUM_CANV_ELEMENT_ALIGNMENT)this.GetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT);  }

//--- (1) Устанавливает, (2) возвращает режим установки размера вкладок
   void              SetTabSizeMode(const ENUM_CANV_ELEMENT_TAB_SIZE_MODE mode)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_TAB_SIZE_MODE,mode);
                        CArrayObj *list=this.GetListHeaders();
                        if(list==NULL)
                           return;
                        for(int i=0;i<list.Total();i++)
                          {
                           CTabHeader *header=list.At(i);
                           if(header==NULL)
                              continue;
                           header.SetTabSizeMode(mode);
                          }
                       }
   ENUM_CANV_ELEMENT_TAB_SIZE_MODE TabSizeMode(void)const{ return (ENUM_CANV_ELEMENT_TAB_SIZE_MODE)this.GetProperty(CANV_ELEMENT_PROP_TAB_SIZE_MODE);}
   
//--- Устанавливает всем заголовкам вкладок значения PaddingWidth и PaddingHeight
   void              SetHeaderPadding(const int w,const int h);
//--- Устанавливает всем полям вкладок значения Padding
   void              SetFieldPadding(const int top,const int bottom,const int left,const int right);
//--- Возвращает значения PaddingWidth и PaddingHeight заголовков вкладок
   int               HeaderPaddingWidth(void)      const { return this.m_header_padding_x;      }
   int               HeaderPaddingHeight(void)     const { return this.m_header_padding_y;      }
//--- Возвращает значения Padding полей вкладок
   int               FieldPaddingTop(void)         const { return this.m_field_padding_top;     }
   int               FieldPaddingBottom(void)      const { return this.m_field_padding_bottom;  }
   int               FieldPaddingLeft(void)        const { return this.m_field_padding_left;    }
   int               FieldPaddingRight(void)       const { return this.m_field_padding_right;   }
   
//--- (1) Устанавливает, (2) возвращает флаг разрешения нескольких рядов заголовков вкладок на элементе управления
   void              SetMultiline(const bool flag)       { this.SetProperty(CANV_ELEMENT_PROP_TAB_MULTILINE,flag);         }
   bool              Multiline(void)               const { return (bool)this.GetProperty(CANV_ELEMENT_PROP_TAB_MULTILINE); }
//--- (1) Устанавливает, (2) возвращает фиксированную ширину заголовков вкладок
   void              SetItemWidth(const int value)       { this.m_item_width=value;             }
   int               ItemWidth(void)               const { return this.m_item_width;            }
//--- (1) Устанавливает, (2) возвращает фиксированную высоту заголовков вкладок
   void              SetItemHeight(const int value)      { this.m_item_height=value;            }
   int               ItemHeight(void)              const { return this.m_item_height;           }
//--- Устанавливает фиксированный размер вкладки
   void              SetItemSize(const int w,const int h)
                       {
                        if(this.ItemWidth()!=w)
                           this.SetItemWidth(w);
                        if(this.ItemHeight()!=h)
                           this.SetItemHeight(h);
                       }
//--- Устанавливает текст заголовка (1) указанной вкладки, (2) по индексу
   void              SetHeaderText(CTabHeader *header,const string text);
   void              SetHeaderText(const int index,const string text);
   
//--- Устанавливает вкладку, указанную по индексу, выбранной/не выбранной
   void              Select(const int index,const bool flag);
//--- Возвращает (1) номер, (2) указатель на выбранную вкладку
   int               SelectedTabPageNum(void)      const { return (int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_NUMBER);}
   CWinFormBase     *SelectedTabPage(void)               { return this.GetTabField(this.SelectedTabPageNum());             }
   
//--- Конструктор
                     CTabControl(const long chart_id,
                                 const int subwindow,
                                 const string descript,
                                 const int x,
                                 const int y,
                                 const int w,
                                 const int h);
  };
//+------------------------------------------------------------------+


В конструкторе класса установим значения по умолчанию для режима задания размера вкладок и установим значения Padding для заголовков и полей:

//+------------------------------------------------------------------+
//| Конструктор с указанием идентификатора чарта и подокна           |
//+------------------------------------------------------------------+
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.SetHeaderPadding(6,3);
   this.SetFieldPadding(3,3,3,3);
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Создаёт указанное количество вкладок                             |
//+------------------------------------------------------------------+
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=0;
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_TOP)
         header_y=0;
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_BOTTOM)
         header_y=this.Height()-h;
      //--- Устанавливаем текущую координату X
      header_x=(header==NULL ? header_x : header.RightEdgeRelative());
      //--- Создаём объект TabHeader
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,header_x,header_y,w,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));
      header.SetTabSizeMode(this.TabSizeMode());
      header.SetSizes(w,h);
      
      //--- В зависимости от расположения заголовков вкладок устанавливаем начальные координаты полей вкладок
      int field_x=0;
      int field_y=0;
      int field_h=this.Height()-header.Height();
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_TOP)
         field_y=header.BottomEdgeRelative();
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_BOTTOM)
         field_y=0;
      //--- Создаём объект TabField (поле вкладки)
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,field_x,field_y,this.Width(),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.ArrangeTabHeaders();
   this.Select(selected_page,true);
   return true;
  }
//+------------------------------------------------------------------+

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


Метод, создающий новый присоединённый элемент:

//+------------------------------------------------------------------+
//| Создаёт новый присоединённый элемент                             |
//+------------------------------------------------------------------+
bool CTabControl::CreateNewElement(const int tab_page,
                                   const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                   const int x,
                                   const int y,
                                   const int w,
                                   const int h,
                                   const color colour,
                                   const uchar opacity,
                                   const bool activity,
                                   const bool redraw)
  {
   CTabField *field=this.GetTabField(tab_page);
   if(field==NULL)
     {
      CMessage::ToLog(DFUN,MSG_ELM_LIST_ERR_FAILED_GET_TAB_OBJ);
      ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_TAB_OBJ)," (Tab",(string)tab_page,")");
      return false;
     }
   return field.CreateNewElement(element_type,x,y,w,h,colour,opacity,activity,redraw);
  }
//+------------------------------------------------------------------+

Получаем объект-поле вкладки по указанному номеру вкладки и возвращаем результат вызова его метода создания нового присоединённого элемента.


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

//+------------------------------------------------------------------+
//| Располагает заголовки вкладок                                    |
//| в соответствии с установленными режимами                         |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeaders(void)
  {
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :  this.ArrangeTabHeadersTop();     break;
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :  this.ArrangeTabHeadersBottom();  break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :  this.ArrangeTabHeadersLeft();    break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :  this.ArrangeTabHeadersRight();   break;
      default: break;
     }
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Располагает заголовки вкладок сверху                             |
//+------------------------------------------------------------------+
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.RightEdgeRelative()-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())
        {
         //--- CANV_ELEMENT_TAB_SIZE_MODE_FIXED и CANV_ELEMENT_TAB_SIZE_MODE_NORMAL
         if(this.TabSizeMode()<CANV_ELEMENT_TAB_SIZE_MODE_FILL)
           {
            //--- Рассчитываем значение правого края заголовка с учётом того,
            //---  что начало отсчёта всегда идёт от левого края 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());
              }
           }
         //--- Растягивание заголовков на ширину контейнера
         //--- CANV_ELEMENT_TAB_SIZE_MODE_FILL
         else
           {
            
           }
        }
      //--- Если разрешён только один ряд заголовков
      else
        {
         
        }
     }
//--- Местоположение всех заголовков вкладок задано, теперь разместим их все вместе с полями
//--- в соответствии с номерами рядов и столбцов заголовков
//--- Получаем последний заголовок в списке
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
//--- Если объект получен и его значение строки больше нуля
   if(last!=NULL && 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);
           }
        }
     }
  }
//+------------------------------------------------------------------+

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

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

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

Остальные подобные методы реализованы пока в качестве методов-заглушек:

//+------------------------------------------------------------------+
//| Располагает заголовки вкладок снизу                              |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersBottom(void)
  {
  
  }
//+------------------------------------------------------------------+
//| Располагает заголовки вкладок слева                              |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersLeft(void)
  {
  
  }
//+------------------------------------------------------------------+
//| Располагает заголовки вкладок справа                             |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersRight(void)
  {
  
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Устанавливает всем заголовкам вкладок значения Padding           |
//+------------------------------------------------------------------+
void CTabControl::SetHeaderPadding(const int w,const int h)
  {
   this.m_header_padding_x=w;
   this.m_header_padding_y=h;
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      header.SetPadding(this.m_header_padding_x,this.m_header_padding_y,this.m_header_padding_x,this.m_header_padding_y);
     }
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Устанавливает всем полям вкладок значения Padding                |
//+------------------------------------------------------------------+
void CTabControl::SetFieldPadding(const int top,const int bottom,const int left,const int right)
  {
   this.m_field_padding_top=top;
   this.m_field_padding_bottom=bottom;
   this.m_field_padding_left=left;
   this.m_field_padding_right=right;
   CArrayObj *list=this.GetListFields();
   if(list==NULL)
      return;
   for(int i=0;i<list.Total();i++)
     {
      CTabField *field=list.At(i);
      if(field==NULL)
         continue;
      field.SetPadding(left,top,right,bottom);
     }
  }
//+------------------------------------------------------------------+

Метод аналогичен вышерассмотренному. Но здесь в него передаются значения Padding сверху, снизу, справа и слева поля вкладки. Эти значения устанавливаются в соответствующие переменные, а затем в цикле — и в каждый объект-поле вкладки.


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

//+------------------------------------------------------------------+
//| Устанавливает вкладку выбранной                                  |
//+------------------------------------------------------------------+
void CTabControl::SetSelected(const int index)
  {
//--- Получаем заголовок по индексу и
   CTabHeader *header=this.GetTabHeader(index);
   if(header==NULL)
      return;
//--- устанавливаем его в состояние "выбран"
   if(!header.State())
      header.SetState(true);
//--- сохраним номер выбранной вкладки
   this.SetSelectedTabPageNum(index);
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Устанавливает вкладку не выбранной                               |
//+------------------------------------------------------------------+
void CTabControl::SetUnselected(const int index)
  {
//--- Получаем заголовок по индексу и
   CTabHeader *header=this.GetTabHeader(index);
   if(header==NULL)
      return;
//--- устанавливаем его в состояние "не выбран"
   if(header.State())
      header.SetState(false);
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Возвращает количество привязанных элементов в указанной вкладке  |
//+------------------------------------------------------------------+
int CTabControl::TabElementsTotal(const int tab_page)
  {
   CTabField *field=this.GetTabField(tab_page);
   return(field!=NULL ? field.ElementsTotal() : 0);
  }
//+------------------------------------------------------------------+

Получаем объект-поле вкладки по указанному номеру и возвращаем количество привязанных к нему объектов.
Метод позволяет узнать сколько объектов прикреплено ко вкладке с указанным номером.


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

//+------------------------------------------------------------------+
//| Возвращает привязанный элемент по индексу в списке               |
//| в указанной вкладке                                              |
//+------------------------------------------------------------------+
CGCnvElement *CTabControl::GetTabElement(const int tab_page,const int index)
  {
   CTabField *field=this.GetTabField(tab_page);
   return(field!=NULL ? field.GetElement(index) : NULL);
  }
//+------------------------------------------------------------------+

Получаем объект-поле по указанному номеру и возвращаем указатель на прикреплённый элемент из списка по указанному индексу.
Метод позволяет получить указатель на нужный элемент по его индексу на указанной вкладке.


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

//+------------------------------------------------------------------+
//| Возвращает по типу список привязанных элементов                  |
//| в указанной вкладке                                              |
//+------------------------------------------------------------------+
CArrayObj *CTabControl::GetListTabElementsByType(const int tab_page,const ENUM_GRAPH_ELEMENT_TYPE type)
  {
   CTabField *field=this.GetTabField(tab_page);
   return(field!=NULL ? field.GetListElementsByType(type) : NULL);
  }
//+------------------------------------------------------------------+

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


Метод, возвращающий по типу объекта количество привязанных элементов в указанной вкладке:

//+------------------------------------------------------------------+
//| Возвращает по типу количество привязанных элементов              |
//| в указанной вкладке                                              |
//+------------------------------------------------------------------+
int CTabControl::TabElementsTotalByType(const int tab_page,const ENUM_GRAPH_ELEMENT_TYPE type)
  {
   CTabField *field=this.GetTabField(tab_page);
   return(field!=NULL ? field.ElementsTotalByType(type) : 0);
  }
//+------------------------------------------------------------------+

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


Метод, возвращающий по типу объекта привязанный элемент по индексу в списке в указанной вкладке:

//+------------------------------------------------------------------+
//| Возвращает по типу привязанный элемент по индексу в списке       |
//| в указанной вкладке                                              |
//+------------------------------------------------------------------+
CGCnvElement *CTabControl::GetTabElementByType(const int tab_page,const ENUM_GRAPH_ELEMENT_TYPE type,const int index)
  {
   CTabField *field=this.GetTabField(tab_page);
   return(field!=NULL ? field.GetElementByType(type,index) : NULL);
  }
//+------------------------------------------------------------------+

Получаем объект-поле по указанному номеру и возвращаем элемент нужного типа по указанному индексу в списке.
Метод позволяет получить элемент нужного типа по его номеру с указанной вкладки.


В методе, создающем новый графический объект, в самом его конце, добавим блок кода для создания объекта-поля вкладки (отрывок кода):

      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;
      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\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 "..\TabField.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"
//+------------------------------------------------------------------+


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

      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;
      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\Container.mqh класса базового объекта-контейнера, в методе, устанавливающем параметры присоединённому объекту, впишем установку параметров объекта-поля вкладки в блок кода установки параметров объектов Button, TabHeader и ListBoxItem (отрывок кода):

      //--- Для WinForms-объектов "Label", "CheckBox", "RadioButton"
      case GRAPH_ELEMENT_TYPE_WF_LABEL             :
      case GRAPH_ELEMENT_TYPE_WF_CHECKBOX          :
      case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON       :
        //--- устанавливаем цвет текста объекта в зависимости от переданного в метод:
        //--- либо цвет текста контейнера, либо переданный в метод.
        //--- Цвет рамки устанавливаем равным цвету текста
        //--- Цвет фона устанавливаем прозрачным
        obj.SetForeColor(colour==clrNONE ? this.ForeColor() : colour,true);
        obj.SetBorderColor(obj.ForeColor(),true);
        obj.SetBackgroundColor(CLR_CANV_NULL,true);
        obj.SetOpacity(0,false);
        break;
      //--- Для WinForms-объекта "Button", "TabHeader", TabField, "ListBoxItem"
      case GRAPH_ELEMENT_TYPE_WF_BUTTON            :
      case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER        :
      case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD         :
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM     :
        //--- устанавливаем цвет текста объекта как цвет текста контейнера в зависимости от переданного в метод:
        //--- цвет фона устанавливаем  в зависимости от переданного в метод:
        //--- либо цвет фона стандартных элементов управления по умолчанию, либо переданный в метод.
        //--- Цвет рамки устанавливаем равным цвету текста
        obj.SetForeColor(this.ForeColor(),true);
        obj.SetBackgroundColor(colour==clrNONE ? CLR_DEF_CONTROL_STD_BACK_COLOR : colour,true);
        obj.SetBorderColor(obj.ForeColor(),true);
        obj.SetBorderStyle(FRAME_STYLE_SIMPLE);
        break;
      //--- Для WinForms-объекта "ListBox", "CheckedListBox", "ButtonListBox"
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX          :
      case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX  :
      case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX   :

После создания объекта в этом методе, ему будут назначены указанные здесь свойства. В последующем их можно будет поменять.


В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\GroupBox.mqh класса объекта GroupBox сделаем метод рисования рамки виртуальным:

//+------------------------------------------------------------------+
//| Класс объекта GroupBox элементов управления WForms               |
//+------------------------------------------------------------------+
class CGroupBox : public CContainer
  {
private:
//--- Рисует рамку
   virtual void      DrawFrame(void);
//--- Создаёт новый графический объект

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


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

      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;
      default:
        break;
     }
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type));
   return element;
  }
//+------------------------------------------------------------------+


В классе-коллекции графических элементов в файле \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh в методах создания графических элементов на канвасе были заменены имена переменных с "name" на "descript" — это отголоски изменения алгоритма именования графических элементов в прошлой статье. Оно и так работало без ошибок, так как тип переменной string, но для правильности наименований формальных параметров методов, они были изменены на "описание" вместо "имя", что правильно. В качестве примера:

//--- Создаёт графический объект-форму на канвасе на указанном графике и подокне с заливкой циклическим горизонтальным градиентом
   int               CreateFormHGradientCicle(const long chart_id,
                                              const int subwindow,
                                              const string descript,
                                              const int x,
                                              const int y,
                                              const int w,
                                              const int h,
                                              color &clr[],
                                              const uchar opacity,
                                              const bool movable,
                                              const bool activity,
                                              const bool shadow=false,
                                              const bool redraw=false)
                       {
                        int id=this.GetMaxID()+1;
                        CForm *obj=new CForm(chart_id,subwindow,descript,x,y,w,h);
                        ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id);
                        if(res==ADD_OBJ_RET_CODE_ERROR)
                           return WRONG_VALUE;
                        obj.SetID(id);
                        obj.SetActive(activity);
                        obj.SetMovable(movable);
                        obj.SetBackgroundColors(clr,true);
                        obj.SetBorderColor(clr[0],true);
                        obj.SetOpacity(opacity,false);
                        obj.SetShadow(shadow);
                        obj.DrawRectangle(0,0,obj.Width()-1,obj.Height()-1,obj.BorderColor(),obj.Opacity());
                        obj.Done();
                        obj.Erase(clr,opacity,false,true,redraw);
                        return obj.ID();
                       }
 
//--- Создаёт графический объект WinForms GroupBox на канвасе на указанном графике и подокне

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

В методе, ищущем объекты взаимодействия, добавим проверку на видимость и доступность объекта. Если он невидим или недоступен — такой объект обрабатывать нельзя — он должен быть недоступным для взаимодействия с мышкой:

//+------------------------------------------------------------------+
//| Ищет объекты взаимодействия                                      |
//+------------------------------------------------------------------+
CForm *CGraphElementsCollection::SearchInteractObj(CForm *form,const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Если передан не пустой указатель
   if(form!=NULL)
     {
      //--- Создадим список объектов взаимодействия
      int total=form.CreateListInteractObj();
      //--- В цикле по созданному списку
      for(int i=total-1;i>WRONG_VALUE;i--)
        {
         //--- получаем очередной объект-форму
         CForm *obj=form.GetInteractForm(i);
         //--- Если объёкт получен и курсор мышки находится над объектом - возвращаем указатель на найденный объект
         if(obj==NULL)
            continue;
         if(!obj.IsVisible())
           {
            continue;
           }
         if(!obj.Enabled())
           {
            continue;
           }
         if(obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL)
           {
            CTabControl *tab_ctrl=obj;
            CForm *elm=tab_ctrl.SelectedTabPage();
            if(elm!=NULL && elm.MouseFormState(id,lparam,dparam,sparam)>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL)
               return elm;
           }
         if(obj.MouseFormState(id,lparam,dparam,sparam)>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL)
            return obj;
        }
     }
//--- Возвращаем обратно тот же указатель
   return form;
  }
//+------------------------------------------------------------------+

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


В методе постобработки бывшей активной формы под курсором пропустим все скрытые и недоступные объекты — их обрабатывать не нужно:

//+------------------------------------------------------------------+
//| Постобработка бывшей активной формы под курсором                 |
//+------------------------------------------------------------------+
void CGraphElementsCollection::FormPostProcessing(CForm *form,const int id, const long &lparam, const double &dparam, const string &sparam)
  {
//--- Получаем главный объект, к которому прикреплена форма
   CForm *main=form.GetMain();
   if(main==NULL)
      main=form;
//--- Получаем все элементы, прикреплённые к форме
   CArrayObj *list=main.GetListElements();
   if(list==NULL)
      return;
   //--- В цикле по списку полученных элементов
   int total=list.Total();
   for(int i=0;i<total;i++)
     {
      //--- получаем указатель на объект
      CForm *obj=list.At(i);
      //--- если указатель получить не удалось - идём к следующему объекту в списке
      if(obj==NULL)
         continue;
      obj.OnMouseEventPostProcessing();
      //--- Создаём список объектов взаимодействия объекта и получаем их количество
      int count=obj.CreateListInteractObj();
      //--- В цикле по полученному списку
      for(int j=0;j<count;j++)
        {
         //--- получаем очередной объект
         CWinFormBase *elm=obj.GetInteractForm(j);
         if(elm==NULL || !elm.IsVisible() || !elm.Enabled())
            continue;
         //--- определяем расположение курсора относительно объекта 
         //--- и вызываем для объекта метод обработки событий мышки
         elm.MouseFormState(id,lparam,dparam,sparam);
         elm.OnMouseEventPostProcessing();
        }
     }
   ::ChartRedraw(main.ChartID());
  }
//+------------------------------------------------------------------+


В файле \MQL5\Include\DoEasy\Engine.mqh главного объекта библиотеки переименуем метод GetWFPanel(), возвращающий объект по имени, в GetWFPanelByName(), а метод GetWFPanel() сделаем возвращающим объект по его описанию:

//--- Возвращает объект WForm Panel по имени объекта на текущем графике
   CPanel              *GetWFPanelByName(const string name)
                          {
                           string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_WF_PANEL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,::ChartID(),EQUAL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
//--- Возвращает объект WForm Panel по описанию объекта на текущем графике
   CPanel              *GetWFPanel(const string descript)
                          {
                           CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_WF_PANEL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,::ChartID(),EQUAL);
                           list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_DESCRIPTION,descript,EQUAL);
                           return(list!=NULL ? list.At(0) : NULL);
                          }
//--- Возвращает объект WForm Panel по идентификатору графика и имени объекта

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

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

Для примера:

//--- Создаёт объект WinForm Element
   CGCnvElement        *CreateWFElement(const long chart_id,
                                        const int subwindow,
                                        const string descript,
                                        const int x,
                                        const int y,
                                        const int w,
                                        const int h,
                                        color &clr[],
                                        const uchar opacity,
                                        const bool v_gradient=true,
                                        const bool c_gradient=false,
                                        const bool redraw=false)
                          {
                           //--- Получаем идентификатор созданного объекта
                           int obj_id=
                             (
                              //--- Если вертикальный градиент:
                              v_gradient ?
                                (
                                 //--- если не циклический градиент - создаём объект с заливкой вертикальным градиентом
                                 !c_gradient ? this.m_graph_objects.CreateElementVGradient(chart_id,subwindow,descript,x,y,w,h,clr,opacity,false,true,redraw) :
                                 //--- иначе - создаём объект с заливкой циклическим вертикальным градиентом
                                 this.m_graph_objects.CreateElementVGradientCicle(chart_id,subwindow,descript,x,y,w,h,clr,opacity,false,true,redraw)
                                ) :
                              //--- Если не вертикальный градиент:
                              !v_gradient ?
                                (
                                 //--- если не циклический градиент - создаём объект с заливкой горизонтальным градиентом
                                 !c_gradient ? this.m_graph_objects.CreateElementHGradient(chart_id,subwindow,descript,x,y,w,h,clr,opacity,false,true,redraw) :
                                 //--- иначе - создаём объект с заливкой циклическим горизонтальным градиентом
                                 this.m_graph_objects.CreateElementHGradientCicle(chart_id,subwindow,descript,x,y,w,h,clr,opacity,false,true,redraw)
                                ) :
                              WRONG_VALUE
                             );
                           //--- возвращаем указатель на объект по его идентификатору
                           return this.GetWFElement(obj_id);
                          }
//--- Создаёт объект WinForm Element на текущем графике, в указанном подокне

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


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

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

Чтобы можно было выбрать режим установки размеров заголовкам вкладок, и при этом при английской версии компиляции константы перечислений были на английском языке, а при русской — на русском, создадим новые перечисления для условной компиляции:

enum ENUM_ELEMENT_ALIGNMENT
  {
   ELEMENT_ALIGNMENT_TOP=CANV_ELEMENT_ALIGNMENT_TOP,                    // Top
   ELEMENT_ALIGNMENT_BOTTOM=CANV_ELEMENT_ALIGNMENT_BOTTOM,              // Bottom
   ELEMENT_ALIGNMENT_LEFT=CANV_ELEMENT_ALIGNMENT_LEFT,                  // Left
   ELEMENT_ALIGNMENT_RIGHT=CANV_ELEMENT_ALIGNMENT_RIGHT,                // Right
  };
enum ENUM_ELEMENT_TAB_SIZE_MODE
  {
   ELEMENT_TAB_SIZE_MODE_NORMAL=CANV_ELEMENT_TAB_SIZE_MODE_NORMAL,      // Fit to tab title text width
   ELEMENT_TAB_SIZE_MODE_FIXED=CANV_ELEMENT_TAB_SIZE_MODE_FIXED,        // Fixed size
   ELEMENT_TAB_SIZE_MODE_FILL=CANV_ELEMENT_TAB_SIZE_MODE_FILL,          // Fit TabControl Width
  };
#else 
enum ENUM_AUTO_SIZE_MODE
  {
   AUTO_SIZE_MODE_GROW=CANV_ELEMENT_AUTO_SIZE_MODE_GROW,                // Только увеличение
   AUTO_SIZE_MODE_GROW_SHRINK=CANV_ELEMENT_AUTO_SIZE_MODE_GROW_SHRINK   // Увеличение и уменьшение
  };
enum ENUM_BORDER_STYLE
  {
   BORDER_STYLE_NONE=FRAME_STYLE_NONE,                                  // Нет рамки
   BORDER_STYLE_SIMPLE=FRAME_STYLE_SIMPLE,                              // Простая рамка
   BORDER_STYLE_FLAT=FRAME_STYLE_FLAT,                                  // Плоская рамка
   BORDER_STYLE_BEVEL=FRAME_STYLE_BEVEL,                                // Рельефная (выпуклая)
   BORDER_STYLE_STAMP=FRAME_STYLE_STAMP,                                // Рельефная (вдавленная)
  };
enum ENUM_CHEK_STATE
  {
   CHEK_STATE_UNCHECKED=CANV_ELEMENT_CHEK_STATE_UNCHECKED,              // Не установлен
   CHEK_STATE_CHECKED=CANV_ELEMENT_CHEK_STATE_CHECKED,                  // Установлен
   CHEK_STATE_INDETERMINATE=CANV_ELEMENT_CHEK_STATE_INDETERMINATE,      // Неопределённый
  };
enum ENUM_ELEMENT_ALIGNMENT
  {
   ELEMENT_ALIGNMENT_TOP=CANV_ELEMENT_ALIGNMENT_TOP,                    // Сверху
   ELEMENT_ALIGNMENT_BOTTOM=CANV_ELEMENT_ALIGNMENT_BOTTOM,              // Снизу
   ELEMENT_ALIGNMENT_LEFT=CANV_ELEMENT_ALIGNMENT_LEFT,                  // Слева
   ELEMENT_ALIGNMENT_RIGHT=CANV_ELEMENT_ALIGNMENT_RIGHT,                // Справа
  };
enum ENUM_ELEMENT_TAB_SIZE_MODE
  {
   ELEMENT_TAB_SIZE_MODE_NORMAL=CANV_ELEMENT_TAB_SIZE_MODE_NORMAL,      // По ширине текста заголовка вкладки
   ELEMENT_TAB_SIZE_MODE_FIXED=CANV_ELEMENT_TAB_SIZE_MODE_FIXED,        // Фиксированный размер
   ELEMENT_TAB_SIZE_MODE_FILL=CANV_ELEMENT_TAB_SIZE_MODE_FILL,          // По ширине элемента управления TabControl
  };
#endif 
//--- input parameters


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

//--- input parameters
sinput   bool                          InpMovable           =  true;                   // Panel Movable flag
sinput   ENUM_INPUT_YES_NO             InpAutoSize          =  INPUT_YES;              // Panel Autosize
sinput   ENUM_AUTO_SIZE_MODE           InpAutoSizeMode      =  AUTO_SIZE_MODE_GROW;    // Panel Autosize mode
sinput   ENUM_BORDER_STYLE             InpFrameStyle        =  BORDER_STYLE_SIMPLE;    // Label border style
sinput   ENUM_ANCHOR_POINT             InpTextAlign         =  ANCHOR_CENTER;          // Label text align
sinput   ENUM_INPUT_YES_NO             InpTextAutoSize      =  INPUT_NO;               // Label autosize
sinput   ENUM_ANCHOR_POINT             InpCheckAlign        =  ANCHOR_LEFT;            // Check flag align
sinput   ENUM_ANCHOR_POINT             InpCheckTextAlign    =  ANCHOR_LEFT;            // Check label text align
sinput   ENUM_CHEK_STATE               InpCheckState        =  CHEK_STATE_UNCHECKED;   // Check flag state
sinput   ENUM_INPUT_YES_NO             InpCheckAutoSize     =  INPUT_YES;              // CheckBox autosize
sinput   ENUM_BORDER_STYLE             InpCheckFrameStyle   =  BORDER_STYLE_NONE;      // CheckBox border style
sinput   ENUM_ANCHOR_POINT             InpButtonTextAlign   =  ANCHOR_CENTER;          // Button text align
sinput   ENUM_INPUT_YES_NO             InpButtonAutoSize    =  INPUT_YES;              // Button autosize
sinput   ENUM_AUTO_SIZE_MODE           InpButtonAutoSizeMode=  AUTO_SIZE_MODE_GROW;    // Button Autosize mode
sinput   ENUM_BORDER_STYLE             InpButtonFrameStyle  =  BORDER_STYLE_NONE;      // Button border style
sinput   bool                          InpButtonToggle      =  true ;                  // Button toggle flag
sinput   bool                          InpListBoxMColumn    =  true;                   // ListBox MultiColumn flag
sinput   bool                          InpButtListMSelect   =  false;                  // ButtonListBox Button MultiSelect flag
//sinput   ENUM_ELEMENT_ALIGNMENT        InpHeaderAlignment   =  ELEMENT_ALIGNMENT_TOP;  // TabHeader Alignment
sinput   ENUM_ELEMENT_TAB_SIZE_MODE    InpTabPageSizeMode   =  ELEMENT_TAB_SIZE_MODE_NORMAL; // TabHeader Size Mode
//--- global variables


Создание WinForms-объектов в обработчике OnInit() советника теперь будет таким:

//+------------------------------------------------------------------+
//| 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;
   pnl=engine.CreateWFPanel("WFPanel",50,50,400,200,array_clr,200,true,true,false,-1,FRAME_STYLE_BEVEL,true,false);
   if(pnl!=NULL)
     {
      //--- Установим значение Padding равным 4
      pnl.SetPaddingAll(4);
      //--- Установим флаги перемещаемости, автоизменения размеров и режим автоизменения из входных параметров
      pnl.SetMovable(InpMovable);
      pnl.SetAutoSize(InpAutoSize,false);
      pnl.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpAutoSizeMode,false);

      //--- В цикле создадим 2 привязанных объекта-панели
      CPanel *obj=NULL;
      for(int i=0;i<2;i++)
        {
         //--- создадим объект-панель с рассчитанными координатами, шириной 90 и высотой 40
         CPanel *prev=pnl.GetElement(i-1);
         int xb=0, yb=0;
         int x=(prev==NULL ? xb : xb+prev.Width()+20);
         int y=0;
         if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_PANEL,x,y,90,40,C'0xCD,0xDA,0xD7',200,true,false))
           {
            obj=pnl.GetElement(i);
            if(obj==NULL)
               continue;
            obj.SetBorderSizeAll(3);
            obj.SetBorderStyle(FRAME_STYLE_BEVEL);
            obj.SetBackgroundColor(obj.ChangeColorLightness(obj.BackgroundColor(),4*i),true);
            obj.SetForeColor(clrRed,true);
            //--- Рассчитаем ширину и высоту будущего объекта-текстовой метки
            int w=obj.Width()-obj.BorderSizeLeft()-obj.BorderSizeRight();
            int h=obj.Height()-obj.BorderSizeTop()-obj.BorderSizeBottom();
            //--- Создадим объект-текстовую метку
            obj.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_LABEL,0,0,w,h,clrNONE,255,false,false);
            //--- Получаем указатель на вновь созданный объект
            CLabel *lbl=obj.GetElement(0);
            if(lbl!=NULL)
              {
               //--- Если объект имеет чётный, или нулевой индекс в списке - зададим ему цвет текста по умолчанию
               if(i % 2==0)
                  lbl.SetForeColor(CLR_DEF_FORE_COLOR,true);
               //--- Если индекс объекта в списке нечётный - зададим объекту непрозрачность 127
               else
                  lbl.SetForeColorOpacity(127);
               //--- Укажем для шрифта тип толщины "Black" и
               //--- укажем выравнивание текста из настроек советника
               lbl.SetFontBoldType(FW_TYPE_BLACK);
               lbl.SetTextAlign(InpTextAlign);
               lbl.SetAutoSize((bool)InpTextAutoSize,false);
               //--- Для объекта с чётным, или нулевым индексом укажем для текста цену Bid, иначе - цену Ask символа 
               lbl.SetText(GetPrice(i % 2==0 ? SYMBOL_BID : SYMBOL_ASK));
               //--- Укажем толщину, тип и цвет рамки для текстовой метки и обновим модифицированный объект
               lbl.SetBorderSizeAll(1);
               lbl.SetBorderStyle((ENUM_FRAME_STYLE)InpFrameStyle);
               lbl.SetBorderColor(CLR_DEF_BORDER_COLOR,true);
               lbl.Update(true);
              }
           }
        }
      //--- Создадим объект WinForms GroupBox1
      CGroupBox *gbox1=NULL;
      //--- Координатой Y GroupBox1 будет являться отступ от прикреплённых панелей на 6 пикселей
      int w=pnl.GetUnderlay().Width();
      int y=obj.BottomEdgeRelative()+6;
      //--- Если прикреплённый объект GroupBox создан
      if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,0,y,200,150,C'0x91,0xAA,0xAE',0,true,false))
        {
         //--- получим указатель на объект GroupBox по его индексу в списке прикреплённых объектов с типом GroupBox
         gbox1=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,0);
         if(gbox1!=NULL)
           {
            //--- установим тип рамки "вдавленная рамка", цвет рамки как цвет фона основной панели,
            //--- а цвет текста - затемнённый на 1 цвет фона последней прикреплённой панели
            gbox1.SetBorderStyle(FRAME_STYLE_STAMP);
            gbox1.SetBorderColor(pnl.BackgroundColor(),true);
            gbox1.SetForeColor(gbox1.ChangeColorLightness(obj.BackgroundColor(),-1),true);
            gbox1.SetText("GroupBox1");
            //--- Создадим объект CheckBox
            gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_CHECKBOX,2,10,50,20,clrNONE,255,true,false);
            //--- получим указатель на объект CheckBox по его индексу в списке прикреплённых объектов с типом CheckBox
            CCheckBox *cbox=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_CHECKBOX,0);
            //--- Если CheckBox создан и указатель на него получен
            if(cbox!=NULL)
              {
               //--- Установим параметры CheckBox из входных параметров советника
               cbox.SetAutoSize((bool)InpCheckAutoSize,false);
               cbox.SetCheckAlign(InpCheckAlign);
               cbox.SetTextAlign(InpCheckTextAlign);
               //--- Зададим выводимый текст, стиль и цвет рамки и состояние флажка
               cbox.SetText("CheckBox");
               cbox.SetBorderStyle((ENUM_FRAME_STYLE)InpCheckFrameStyle);
               cbox.SetBorderColor(CLR_DEF_BORDER_COLOR,true);
               cbox.SetChecked(true);
               cbox.SetCheckState((ENUM_CANV_ELEMENT_CHEK_STATE)InpCheckState);
              }
            //--- Создадим 4 WinForms-объекта RadioButton
            CRadioButton *rbtn=NULL;
            for(int i=0;i<4;i++)
              {
               //--- Создадим объект RadioButton
               int yrb=(rbtn==NULL ? cbox.BottomEdgeRelative() : rbtn.BottomEdgeRelative());
               gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,2,yrb+4,50,20,clrNONE,255,true,false);
               //--- получим указатель на объект RadioButton по его индексу в списке прикреплённых объектов с типом RadioButton
               rbtn=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,i);
               //--- Если RadioButton1 создан и указатель на него получен
               if(rbtn!=NULL)
                 {
                  //--- Установим параметры RadioButton из входных параметров советника
                  rbtn.SetAutoSize((bool)InpCheckAutoSize,false);
                  rbtn.SetCheckAlign(InpCheckAlign);
                  rbtn.SetTextAlign(InpCheckTextAlign);
                  //--- Зададим выводимый текст, стиль и цвет рамки и состояние флажка
                  rbtn.SetText("RadioButton"+string(i+1));
                  rbtn.SetBorderStyle((ENUM_FRAME_STYLE)InpCheckFrameStyle);
                  rbtn.SetBorderColor(CLR_DEF_BORDER_COLOR,true);
                  rbtn.SetChecked(!i);
                  rbtn.SetGroup(2);
                 }
              }
            //--- Создадим 3 WinForms-объекта Button
            CButton *butt=NULL;
            for(int i=0;i<3;i++)
              {
               //--- Создадим объект Button
               int ybtn=(butt==NULL ? 12 : butt.BottomEdgeRelative()+4);
               gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_BUTTON,(int)fmax(rbtn.RightEdgeRelative(),cbox.RightEdgeRelative())+20,ybtn,78,18,clrNONE,255,true,false);
               //--- получим указатель на объект Button по его индексу в списке прикреплённых объектов с типом Button
               butt=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_BUTTON,i);
               //--- Если Button создан и указатель на него получен
               if(butt!=NULL)
                 {
                  //--- Установим параметры Button из входных параметров советника
                  butt.SetAutoSize((bool)InpButtonAutoSize,false);
                  butt.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpButtonAutoSizeMode,false);
                  butt.SetTextAlign(InpButtonTextAlign);
                  //--- Зададим цвет текста, стиль и цвет рамки
                  butt.SetForeColor(butt.ChangeColorLightness(CLR_DEF_FORE_COLOR,2),true);
                  butt.SetBorderStyle((ENUM_FRAME_STYLE)InpButtonFrameStyle);
                  butt.SetBorderColor(butt.ChangeColorLightness(butt.BackgroundColor(),-10),true);
                  //--- Установим режим toggle в зависимости от настроек
                  butt.SetToggleFlag(InpButtonToggle);
                  //--- Зададим выводимый текст на кнопке в зависимости от флага toggle
                  string txt="Button"+string(i+1);
                  if(butt.Toggle())
                     butt.SetText("Toggle-"+txt);
                  else
                     butt.SetText(txt);
                  if(i<2)
                    {
                     butt.SetGroup(2);
                     if(butt.Toggle())
                       {
                        butt.SetBackgroundColorMouseOver(butt.ChangeColorLightness(butt.BackgroundColor(),-5));
                        butt.SetBackgroundColorMouseDown(butt.ChangeColorLightness(butt.BackgroundColor(),-10));
                        butt.SetBackgroundStateOnColor(C'0xE2,0xC5,0xB1',true);
                        butt.SetBackgroundStateOnColorMouseOver(butt.ChangeColorLightness(butt.BackgroundStateOnColor(),-5));
                        butt.SetBackgroundStateOnColorMouseDown(butt.ChangeColorLightness(butt.BackgroundStateOnColor(),-10));
                       }
                    }
                 }
              }
            //--- Создадим 2 WinForms-объекта RadioButton
            rbtn=NULL;
            for(int i=0;i<2;i++)
              {
               //--- Создадим объект RadioButton
               int yrb=(rbtn==NULL ? butt.BottomEdgeRelative() : rbtn.BottomEdgeRelative());
               gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,butt.CoordXRelative()-4,yrb+3,50,20,clrNONE,255,true,false);
               //--- получим указатель на объект RadioButton по его индексу в списке прикреплённых объектов с типом RadioButton
               rbtn=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,i+4);
               //--- Если RadioButton1 создан и указатель на него получен
               if(rbtn!=NULL)
                 {
                  //--- Установим параметры RadioButton из входных параметров советника
                  rbtn.SetAutoSize((bool)InpCheckAutoSize,false);
                  rbtn.SetCheckAlign(InpCheckAlign);
                  rbtn.SetTextAlign(InpCheckTextAlign);
                  //--- Зададим выводимый текст, стиль и цвет рамки и состояние флажка
                  rbtn.SetText("RadioButton"+string(i+5));
                  rbtn.SetBorderStyle((ENUM_FRAME_STYLE)InpCheckFrameStyle);
                  rbtn.SetBorderColor(CLR_DEF_BORDER_COLOR,true);
                  rbtn.SetChecked(!i);
                  rbtn.SetGroup(3);
                 }
              }
           }
        }
      
      //--- Создадим объект WinForms GroupBox2
      CGroupBox *gbox2=NULL;
      //--- Координатой Y GroupBox2 будет являться отступ от прикреплённых панелей на 6 пикселей
      w=gbox1.Width()-1;
      int x=gbox1.RightEdgeRelative()+1;
      int h=gbox1.BottomEdgeRelative()-6;
      //--- Если прикреплённый объект GroupBox создан
      if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,x,2,w,h,C'0x91,0xAA,0xAE',0,true,false))
        {
         //--- получим указатель на объект GroupBox по его индексу в списке прикреплённых объектов с типом GroupBox
         gbox2=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,1);
         if(gbox2!=NULL)
           {
            //--- установим тип рамки "вдавленная рамка", цвет рамки как цвет фона основной панели,
            //--- а цвет текста - затемнённый на 1 цвет фона последней прикреплённой панели
            gbox2.SetBorderStyle(FRAME_STYLE_STAMP);
            gbox2.SetBorderColor(pnl.BackgroundColor(),true);
            gbox2.SetForeColor(gbox2.ChangeColorLightness(obj.BackgroundColor(),-1),true);
            gbox2.SetText("GroupBox2");
            
            //--- Создадим объект TabControl
            gbox2.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,4,12,gbox2.Width()-12,gbox2.Height()-20,clrNONE,255,true,false);
            //--- получим указатель на объект TabControl по его индексу в списке прикреплённых объектов с типом TabControl
            CTabControl *tab_ctrl=gbox2.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,0);
            //--- Если TabControl создан и указатель на него получен
            if(tab_ctrl!=NULL)
              {
               //--- Установим расположение заголовков вкладок на элементе, текст вкладок, и создадим девять вкладок
               tab_ctrl.SetTabSizeMode((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)InpTabPageSizeMode);
               tab_ctrl.SetAlignment(CANV_ELEMENT_ALIGNMENT_TOP/*(ENUM_CANV_ELEMENT_ALIGNMENT)InpHeaderAlignment*/);
               tab_ctrl.SetMultiline(true);
               tab_ctrl.SetHeaderPadding(6,0);
               tab_ctrl.CreateTabPages(9,0,50,16,TextByLanguage("Вкладка","TabPage"));

               //--- Создадим объект CheckedListBox в первой вкладке
               tab_ctrl.CreateNewElement(0,GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,4,12,160,20,clrNONE,255,true,false);
               //--- получим указатель на объект CheckedListBox с первой вкладки по его индексу в списке прикреплённых объектов с типом CheckBox
               CCheckedListBox *check_lbox=tab_ctrl.GetTabElementByType(0,GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,0);
               //--- Если CheckedListBox создан и указатель на него получен
               if(check_lbox!=NULL)
                 {
                  check_lbox.SetBackgroundColor(tab_ctrl.BackgroundColor(),true);
                  check_lbox.SetMultiColumn(InpListBoxMColumn);
                  check_lbox.SetColumnWidth(0);
                  check_lbox.CreateCheckBox(4,66);
                 }
               
               //--- Создадим объект ButtonListBox во второй вкладке
               tab_ctrl.CreateNewElement(1,GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX,4,12,160,30,clrNONE,255,true,false);
               //--- получим указатель на объект ButtonListBox со второй вкладки по его индексу в списке прикреплённых объектов с типом Button
               CButtonListBox *butt_lbox=tab_ctrl.GetTabElementByType(1,GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX,0);
               //--- Если ButtonListBox создан и указатель на него получен
               if(butt_lbox!=NULL)
                 {
                  butt_lbox.SetBackgroundColor(tab_ctrl.BackgroundColor(),true);
                  butt_lbox.SetMultiColumn(true);
                  butt_lbox.SetColumnWidth(0);
                  butt_lbox.CreateButton(4,66,16);
                  butt_lbox.SetMultiSelect(InpButtListMSelect);
                  butt_lbox.SetToggle(InpButtonToggle);
                  for(int i=0;i<butt_lbox.ElementsTotal();i++)
                    {
                     butt_lbox.SetButtonGroup(i,(i % 2==0 ? butt_lbox.Group()+1 : butt_lbox.Group()+2));
                     butt_lbox.SetButtonGroupFlag(i,(i % 2==0 ? true : false));
                    }
                 }
               
               //--- Создадим объект ListBox в третьей вкладке
               int lbw=146;
               if(!InpListBoxMColumn)
                  lbw=100;
               tab_ctrl.CreateNewElement(2,GRAPH_ELEMENT_TYPE_WF_LIST_BOX,4,12,lbw,60,clrNONE,255,true,false);
               //--- получим указатель на объект ListBox с третьей вкладки по его индексу в списке прикреплённых объектов с типом ListBox
               CListBox *list_box=tab_ctrl.GetTabElementByType(2,GRAPH_ELEMENT_TYPE_WF_LIST_BOX,0);
               //--- Если ListBox создан и указатель на него получен
               if(list_box!=NULL)
                 {
                  list_box.SetBackgroundColor(tab_ctrl.BackgroundColor(),true);
                  list_box.SetMultiColumn(InpListBoxMColumn);
                  list_box.CreateList(8,68);
                 }
               
               //--- На остальные вкладки (3 - 8) разместим текстовые метки с наименованием вкладки
               for(int i=3;i<9;i++)
                 {
                  CTabField *field=tab_ctrl.GetTabField(i);
                  if(field==NULL)
                     continue;
                  tab_ctrl.CreateNewElement(i,GRAPH_ELEMENT_TYPE_WF_LABEL,1,1,field.Width()-2,field.Height()-2,clrNONE,255,true,false);
                  CLabel *label=tab_ctrl.GetTabElementByType(i,GRAPH_ELEMENT_TYPE_WF_LABEL,0);
                  if(label!=NULL)
                    {
                     label.SetTextAlign(ANCHOR_CENTER);
                     label.SetText(tab_ctrl.GetTabHeader(i).Text());
                    }
                 }
              }
           }
        }
      //--- Перерисуем все объекты в порядке их иерархии
      pnl.Redraw(true);
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

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

Скомпилируем советник и запустим его на графике:


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


Что дальше

В следующей статье продолжим работу над WinForms-объектом TabControl.

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

К содержанию

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

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



Прикрепленные файлы |
MQL5.zip (4515.59 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
Koros Jafarzadeh
Koros Jafarzadeh | 14 авг. 2022 в 12:26

Hi @Artyom Trishkin

thanks for your amazing libraries,

I found a little bug or missing parameters about market event logs, you can see the STATUS UNKNOWN appears for orders or positions modification,

you can see the yellow highlighted lines in picture



I think you forgot to put this line to CEvent::StatusDescription


string CEvent::StatusDescription(void) const
  {
   ENUM_EVENT_STATUS status=(ENUM_EVENT_STATUS)this.GetProperty(EVENT_PROP_STATUS_EVENT);
   return
     (
      status==EVENT_STATUS_MARKET_PENDING    ?  CMessage::Text(MSG_EVN_STATUS_MARKET_PENDING)   :
      status==EVENT_STATUS_MARKET_POSITION   ?  CMessage::Text(MSG_EVN_STATUS_MARKET_POSITION)  :
      status==EVENT_STATUS_HISTORY_PENDING   ?  CMessage::Text(MSG_EVN_STATUS_HISTORY_PENDING)  :
      status==EVENT_STATUS_HISTORY_POSITION  ?  CMessage::Text(MSG_EVN_STATUS_HISTORY_POSITION) :
      status==EVENT_STATUS_MODIFY            ?  CMessage::Text(MSG_EVN_REASON_MODIFY)           :
      status==EVENT_STATUS_BALANCE           ?  CMessage::Text(MSG_LIB_PROP_BALANCE)            :
      CMessage::Text(MSG_EVN_STATUS_UNKNOWN)
     );
  }
Artyom Trishkin
Artyom Trishkin | 14 авг. 2022 в 14:16
Koros Jafarzadeh #:

Hi @Artyom Trishkin

thanks for your amazing libraries,

I found a little bug or missing parameters about market event logs, you can see the STATUS UNKNOWN appears for orders or positions modification,

you can see the yellow highlighted lines in picture



I think you forgot to put this line to CEvent::StatusDescription


Thank you. I'll take a look in a week - I'm on vacation.
Возможности Мастера MQL5, которые вам нужно знать (Часть 1): Регрессионный анализ Возможности Мастера MQL5, которые вам нужно знать (Часть 1): Регрессионный анализ
Современный трейдер почти всегда сознательно или бессознательно находится в поиске новых идей. Он постоянно пробует новые стратегии, модифицирует их и отбрасывает те, что не оправдали себя. Этот исследовательский процесс требует много времени и сопряжен с ошибками. В этой серии статей я постараюсь доказать, что Мастер MQL5 является настоящей опорой трейдера. Благодаря Мастеру, трейдер экономит время при реализации своих идей. Кроме того, снижается вероятность ошибок, возникающих при дублировании кода. Вместо того чтобы тратить время на оформление кода, трейдеры претворяют в жизнь свою торговую философию.
Популяционные алгоритмы оптимизации Популяционные алгоритмы оптимизации
Вводная статья об алгоритмах оптимизации (АО). Классификация. В статье предпринята попытка создать тестовый стенд (набор функций), который послужит в дальнейшем для сравнения АО между собой, и, даже, возможно, выявления самого универсального алгоритма из всех широко известных.
Машинное обучение и Data Science (Часть 05): Деревья решений на примере погодных условий для игры в теннис Машинное обучение и Data Science (Часть 05): Деревья решений на примере погодных условий для игры в теннис
Деревья решений классифицируют данные, имитируя то, каким образом размышляют люди. В этой статье посмотрим, как строить деревья и использовать их для классификации и прогнозирования данных. Основная цель алгоритма деревьев решений состоит в том, чтобы разделить выборку на данные с "примесями" и на "чистые" или близкие к узлам.
Нейросети — это просто (Часть 24): Совершенствуем инструмент для Transfer Learning Нейросети — это просто (Часть 24): Совершенствуем инструмент для Transfer Learning
В прошлой статье мы создали инструмент для создания и редактирования архитектуры нейронных сетей. И сегодня я хочу Вам предложить продолжить работу над этим инструментом. Чтобы сделать его более дружелюбным к пользователю. В чем-то это шаг в сторону от нашей темы. Но согласитесь, организация рабочего пространства играет не последнюю роль в достижении результата.