DoEasy. Элементы управления (Часть 14): Новый алгоритм именования графических элементов. Продолжаем работу над WinForms-объектом TabControl

5 августа 2022, 14:39
Artyom Trishkin
0
156

Содержание


Концепция

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

Например, при создании элементов GUI для тестовой программы этой статьи у нас получился такой список графических элементов
(видна только первая часть из всех построенных элементов, но и этого достаточно для понимания принятой концепции):


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

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

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

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


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

Некоторые элементы управления для своей работы используют уже имеющиеся элементы управления, например, ListBox использует для рисования своей коллекции (Items) элементы управления Button, но с небольшой доработкой функционала. Для его реализации нужно создать новый объект-наследник от элемента Button и добавить требуемый функционал. И вот такой, и некоторые другие такие же объекты, желательно вынести в отдельную категорию вспомогательных объектов, которые будем размещать не в папках категорий элементов управления, а в их корневом каталоге.

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

//+------------------------------------------------------------------+
//| Список типов графических элементов                               |
//+------------------------------------------------------------------+
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
  };
//+------------------------------------------------------------------+


В перечислении строковых свойств графического элемента на канвасе добавим новое свойство — описание графического элемента,
и увеличим общее количество строковых свойств с 3 до 4:

//+------------------------------------------------------------------+
//| Строковые свойства графического элемента на канвасе              |
//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_PROP_STRING
  {
   CANV_ELEMENT_PROP_NAME_OBJ = (CANV_ELEMENT_PROP_INTEGER_TOTAL+CANV_ELEMENT_PROP_DOUBLE_TOTAL), // Имя объекта-графического элемента
   CANV_ELEMENT_PROP_NAME_RES,                        // Имя графического ресурса
   CANV_ELEMENT_PROP_TEXT,                            // Текст графического элемента
   CANV_ELEMENT_PROP_DESCRIPTION,                     // Описание графического элемента
  };
#define CANV_ELEMENT_PROP_STRING_TOTAL  (4)           // Общее количество строковых свойств
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Возможные критерии сортировки графических элементов на канвасе   |
//+------------------------------------------------------------------+
#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_ALIGNMENT,                // Сортировать по местоположению вкладок внутри элемента управления
   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 добавим индексы новых сообщений, удалим индекс ненужного сообщения:

//--- WinForms-стандартные
   MSG_GRAPH_ELEMENT_TYPE_WF_COMMON_BASE,             // Базовый стандартный элемент управления WinForms
   MSG_GRAPH_ELEMENT_TYPE_WF_LABEL,                   // Элемент управления Label
   MSG_GRAPH_ELEMENT_TYPE_WF_CHECKBOX,                // Элемент управления CheckBox
   MSG_GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,             // Элемент управления RadioButton
   MSG_GRAPH_ELEMENT_TYPE_WF_BUTTON,                  // Элемент управления Button
   MSG_GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX,       // Базовый объект-список Windows Forms элементов
   MSG_GRAPH_ELEMENT_TYPE_WF_LIST_BOX,                // Элемент управления ListBox
   MSG_GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM,           // Объект коллекции элемента управления ListBox
   MSG_GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,        // Элемент управления CheckedListBox
   MSG_GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX,         // Элемент управления ButtonListBox
   MSG_GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,              // Заголовок вкладки
   MSG_GRAPH_ELEMENT_TYPE_WF_TAB_PAGE,                // Элемент управления TabPage
   MSG_GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,             // Элемент управления TabControl
   MSG_GRAPH_OBJ_BELONG_PROGRAM,                      // Графический объект принадлежит программе
   MSG_GRAPH_OBJ_BELONG_NO_PROGRAM,                   // Графический объект не принадлежит программе

...

//--- Строковые свойства графических элементов
   MSG_CANV_ELEMENT_PROP_NAME_OBJ,                    // Имя объекта-графического элемента
   MSG_CANV_ELEMENT_PROP_NAME_RES,                    // Имя графического ресурса
   MSG_CANV_ELEMENT_PROP_TEXT,                        // Текст графического элемента
   MSG_CANV_ELEMENT_PROP_DESCRIPTION,                 // Описание графического элемента
  };
//+------------------------------------------------------------------+


и впишем тексты новых сообщений, соответствующие вновь добавленным индексам. Текст удалённого индекса, соответственно, тоже удалим:

//--- WinForms-стандартные
   {"Базовый стандартный элемент управления WinForms","Basic Standard WinForms Control"},
   {"Элемент управления \"Label\"","Control element \"Label\""},
   {"Элемент управления \"CheckBox\"","Control element \"CheckBox\""},
   {"Элемент управления \"RadioButton\"","Control element \"RadioButton\""},
   {"Элемент управления \"Button\"","Control element \"Button\""},
   {"Базовый объект-список Windows Forms элементов","Basic Windows Forms List Object"},
   {"Элемент управления \"ListBox\"","Control element \"ListBox\""},
   {"Объект коллекции элемента управления ListBox","Collection object of the ListBox control"},
   {"Элемент управления \"CheckedListBox\"","Control element \"CheckedListBox\""},
   {"Элемент управления \"ButtonListBox\"","Control element \"ButtonListBox\""},
   {"Заголовок вкладки","Tab header"},
                                                                                             
   {"Элемент управления \"TabControl\"","Control element \"TabControl\""},
   {"Графический объект принадлежит программе","The graphic object belongs to the program"},
   {"Графический объект не принадлежит программе","The graphic object does not belong to the program"},

...

//--- Строковые свойства графических элементов
   {"Имя объекта-графического элемента","The name of the graphic element object"},
   {"Имя графического ресурса","Image resource name"},
   {"Текст графического элемента","Text of the graphic element"},
   {"Описание графического элемента","Description of the graphic element"},
  };
//+---------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Возвращает тип графического объекта как string                   |
//+------------------------------------------------------------------+
string TypeGraphElementAsString(const ENUM_GRAPH_ELEMENT_TYPE type)
  {
   ushort array[];
   int total=StringToShortArray(StringSubstr(::EnumToString(type),18),array);
   for(int i=0;i<total-1;i++)
     {
      if(array[i]==95)
        {
         i+=1;
         continue;
        }
      else
         array[i]+=0x20;
     }
   string txt=ShortArrayToString(array);
   StringReplace(txt,"_Wf_Base","WFBase");
   StringReplace(txt,"_Wf_","");
   StringReplace(txt,"_Obj","");
   StringReplace(txt,"_","");
   StringReplace(txt,"Groupbox","GroupBox");
   return txt;
  }
//+------------------------------------------------------------------+

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

int total=StringToShortArray(StringSubstr(EnumToString(type),18),array);

получаем количество символов подстроки, выделенной из наименования константы перечисления типа.

Разберём на примере константы GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX.

Преобразуем константу перечисления в текст "GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX":

EnumToString(type)

Из полученного текста "GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX" выделяем строку "_WF_CHECKED_LIST_BOX", начиная с символа 18:

StringSubstr(EnumToString(type),18)

и полученную строку "_WF_CHECKED_LIST_BOX" посимвольно копируем в ushort-массив, получая при этом количество скопированных символов:

int total=StringToShortArray(StringSubstr(EnumToString(type),18),array);

В итоге у нас в массиве array[] содержатся коды каждого символа строки "_WF_CHECKED_LIST_BOX".


Теперь нам нужно после каждого знака "_" оставить прописную (заглавную) букву, а все остальные сделать строчными (маленькими).

Это делаем в цикле по массиву символов:

   for(int i=0;i<total-1;i++)
     {
      if(array[i]==95)
        {
         i+=1;
         continue;
        }
      else
         array[i]+=0x20;
     }

Первый символ строки и, соответственно, и в массиве, у нас "_", и как только мы встречаем код этого символа (95) в массиве, нам нужно установить индекс цикла на следующий символ за ним.

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

Таким образом, в массиве после всего цикла будут содержаться коды символов строки "_Wf_Checked_List_Box", которые мы преобразуем в строку:

string txt=ShortArrayToString(array);

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

   StringReplace(txt,"_Wf_Base","WFBase");
   StringReplace(txt,"_Wf_","");
   StringReplace(txt,"_Obj","");
   StringReplace(txt,"_","");
   StringReplace(txt,"Groupbox","GroupBox");
   return txt;

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


В файле базового графического объекта \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_HEADER          ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_HEADER)         :
      type==GRAPH_ELEMENT_TYPE_WF_TAB_PAGE            ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_PAGE)           :
      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)    :
      "Unknown"
     );
  }  
//+------------------------------------------------------------------+


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

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

//--- Создаёт (1) структуру объекта, (2) объект из структуры
   virtual bool      ObjectToStruct(void);
   virtual void      StructToObject(void);
//--- Копирует массив цветов в указанный массив цветов фона
   void              CopyArraysColors(color &array_dst[],const color &array_src[],const string source);
   
//--- Возвращает количество графических элементов по типу
   int               GetNumGraphElements(const ENUM_GRAPH_ELEMENT_TYPE type) const;
//--- Создаёт и возвращает имя графического элемента по его типу
   string            CreateNameGraphElement(const ENUM_GRAPH_ELEMENT_TYPE type);
   
private:

Реализацию методов рассмотрим чуть позже.

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

private:
   int               m_shift_coord_x;                          // Смещение координаты X относительно базового объекта
   int               m_shift_coord_y;                          // Смещение координаты Y относительно базового объекта
   struct SData
     {
      //--- Целочисленные свойства объекта
      int            id;                                       // Идентификатор элемента
      int            type;                                     // Тип графического элемента
      //---...
      //---...
      bool           button_toggle;                            // Флаг "Переключатель" элемента управления, имеющего кнопку
      bool           button_state;                             // Состояние элемента управления "Переключатель", имеющего кнопку
      bool           button_group_flag;                        // Флаг группы кнопки
      bool           multicolumn;                              // Горизонтальное отображение столбцов в элементе управления ListBox
      int            column_width;                             // Ширина каждого столбца элемента управления ListBox
      bool           tab_multiline;                            // Несколько рядов вкладок в элементе управления TabControl
      int            tab_alignment;                            // Местоположение вкладок внутри элемента управления
      int            alignment;                                // Местоположение объекта внутри элемента управления
      //--- Вещественные свойства объекта

      //--- Строковые свойства объекта
      uchar          name_obj[64];                             // Имя объекта-графического элемента
      uchar          name_res[64];                             // Имя графического ресурса
      uchar          text[256];                                // Текст графического элемента
      uchar          descript[256];                            // Описание графического элемента
     };
   SData             m_struct_obj;                             // Структура объекта
   uchar             m_uchar_array[];                          // uchar-массив структуры объекта

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

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

//--- Создаёт элемент
   bool              Create(const long chart_id,
                            const int wnd_num,
                            const string name,
                            const int x,
                            const int y,
                            const int w,
                            const int h,
                            const bool redraw=false);

Теперь имя объекта будет не передаваться в метод, а создаваться в нём, исходя из типа создаваемого объекта.

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

protected:
//--- Защищённый конструктор
                     CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                  const long    chart_id,
                                  const int     wnd_num,
                                  const string  descript,
                                  const int     x,
                                  const int     y,
                                  const int     w,
                                  const int     h);
public:
//--- (1) Устанавливает, (2) возвращает смещение координаты X относительно базового объекта
   void              SetCoordXRelative(const int value)                                { this.m_shift_coord_x=value;                }
   int               CoordXRelative(void)                                        const { return this.m_shift_coord_x;               }
//--- (1) Устанавливает, (2) возвращает смещение координаты Y относительно базового объекта
   void              SetCoordYRelative(const int value)                                { this.m_shift_coord_y=value;                }
   int               CoordYRelative(void)                                        const { return this.m_shift_coord_y;               }
   
//--- Обработчик событий
   virtual void      OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- Параметрический конструктор
                     CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                  const int     element_id,
                                  const int     element_num,
                                  const long    chart_id,
                                  const int     wnd_num,
                                  const string  descript,
                                  const int     x,
                                  const int     y,
                                  const int     w,
                                  const int     h,
                                  const color   colour,
                                  const uchar   opacity,
                                  const bool    movable=true,
                                  const bool    activity=true,
                                  const bool    redraw=false);

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

Точно так же доработке подверглись и установка типа графического элемента. Ранее во всех конструкторах мы дважды прописывали тип элемента в каждом конструкторе каждого класса WinForms-объектов — сначала тип записывали в базовый объект графических элементов библиотеки (в его переменную m_type_element):

void              SetTypeElement(const ENUM_GRAPH_ELEMENT_TYPE type) { this.m_type_element=type;   }

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

Упростим это, создав публичный метод для записи (и возврата) типа объекта сразу в оба значения — в переменную и в свойство:

//--- Устанавливает смещение (1) левого, (2) верхнего, (3) правого, (4) нижнего края активной зоны относительно элемента,
//--- (5) все смещения краёв активной зоны относительно элемента, (6) непрозрачность
   void              SetActiveAreaLeftShift(const int value)   { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_LEFT,fabs(value));       }
   void              SetActiveAreaRightShift(const int value)  { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT,fabs(value));      }
   void              SetActiveAreaTopShift(const int value)    { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_TOP,fabs(value));        }
   void              SetActiveAreaBottomShift(const int value) { this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM,fabs(value));     }
   void              SetActiveAreaShift(const int left_shift,const int bottom_shift,const int right_shift,const int top_shift);
   void              SetOpacity(const uchar value,const bool redraw=false);
   
//--- (1) Устанавливает, (2) возвращает тип графического элемента
   void              SetTypeElement(const ENUM_GRAPH_ELEMENT_TYPE type)
                       {
                        CGBaseObj::SetTypeElement(type);
                        this.SetProperty(CANV_ELEMENT_PROP_TYPE,type);
                       }
   ENUM_GRAPH_ELEMENT_TYPE TypeGraphElement(void)  const { return (ENUM_GRAPH_ELEMENT_TYPE)this.GetProperty(CANV_ELEMENT_PROP_TYPE);   }
   
//--- Устанавливает основной цвет фона

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


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

//--- Группа графического объекта
   virtual int       Group(void)                         const { return (int)this.GetProperty(CANV_ELEMENT_PROP_GROUP);                }
   virtual void      SetGroup(const int value)
                       {
                        CGBaseObj::SetGroup(value);
                        this.SetProperty(CANV_ELEMENT_PROP_GROUP,value);
                       }
//--- Описание графического элемента
   string            Description(void)                   const { return this.GetProperty(CANV_ELEMENT_PROP_DESCRIPTION);               }
   void              SetDescription(const string descr)        { this.SetProperty(CANV_ELEMENT_PROP_DESCRIPTION,descr);                }
   
//+------------------------------------------------------------------+
//| Методы получения растровых данных                                |
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Параметрический конструктор                                      |
//+------------------------------------------------------------------+
CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                           const int      element_id,
                           const int      element_num,
                           const long     chart_id,
                           const int      wnd_num,
                           const string   descript,
                           const int      x,
                           const int      y,
                           const int      w,
                           const int      h,
                           const color    colour,
                           const uchar    opacity,
                           const bool     movable=true,
                           const bool     activity=true,
                           const bool     redraw=false) : m_shadow(false)
  {
   this.SetTypeElement(element_type);
   this.m_type=OBJECT_DE_TYPE_GELEMENT; 
   this.m_element_main=NULL;
   this.m_element_base=NULL;
   this.m_chart_color_bg=(color)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_COLOR_BACKGROUND);
   this.m_name=this.CreateNameGraphElement(element_type);
   this.m_chart_id=(chart_id==NULL || chart_id==0 ? ::ChartID() : chart_id);
   this.m_subwindow=wnd_num;
   this.SetFont(DEF_FONT,DEF_FONT_SIZE);
   this.m_text_anchor=0;
   this.m_text_x=0;
   this.m_text_y=0;
   this.SetBackgroundColor(colour,true);
   this.SetOpacity(opacity);
   this.m_shift_coord_x=0;
   this.m_shift_coord_y=0;
   if(::ArrayResize(this.m_array_colors_bg,1)==1)
      this.m_array_colors_bg[0]=this.BackgroundColor();
   if(::ArrayResize(this.m_array_colors_bg_dwn,1)==1)
      this.m_array_colors_bg_dwn[0]=this.BackgroundColor();
   if(::ArrayResize(this.m_array_colors_bg_ovr,1)==1)
      this.m_array_colors_bg_ovr[0]=this.BackgroundColor();
   if(this.Create(chart_id,wnd_num,x,y,w,h,redraw))
     {
      this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,this.m_canvas.ResourceName()); // Имя графического ресурса
      this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,CGBaseObj::ChartID());         // Идентификатор графика

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

      this.SetProperty(CANV_ELEMENT_PROP_CHECK_STATE,CANV_ELEMENT_CHEK_STATE_UNCHECKED);  // Состояние элемента управления, имеющего флажок проверки
      this.SetProperty(CANV_ELEMENT_PROP_AUTOCHECK,true);                        // Автоматическое изменение состояния флажка при его выборе
      
      //---...
      //---...

      this.SetProperty(CANV_ELEMENT_PROP_BUTTON_TOGGLE,false);                                        // Флаг "Переключатель" элемента управления, имеющего кнопку
      this.SetProperty(CANV_ELEMENT_PROP_BUTTON_STATE,false);                                         // Состояние элемента управления "Переключатель", имеющего кнопку
      this.SetProperty(CANV_ELEMENT_PROP_BUTTON_GROUP,false);                                         // Флаг группы кнопки
      this.SetProperty(CANV_ELEMENT_PROP_LIST_BOX_MULTI_COLUMN,false);                                // Горизонтальное отображение столбцов в элементе управления ListBox
      this.SetProperty(CANV_ELEMENT_PROP_LIST_BOX_COLUMN_WIDTH,0);                                    // Ширина каждого столбца элемента управления ListBox
      this.SetProperty(CANV_ELEMENT_PROP_TAB_MULTILINE,false);                                        // Несколько рядов вкладок в элементе управления TabControl
      this.SetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT,CANV_ELEMENT_ALIGNMENT_TOP);                   // Местоположение вкладок внутри элемента управления
      this.SetProperty(CANV_ELEMENT_PROP_ALIGNMENT,CANV_ELEMENT_ALIGNMENT_TOP);                       // Местоположение объекта внутри элемента управления
      this.SetProperty(CANV_ELEMENT_PROP_TEXT,"");                                                    // Текст графического элемента
      this.SetProperty(CANV_ELEMENT_PROP_DESCRIPTION,descript);                                       // Описание графического элемента
     }
   else
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),"\"",this.TypeElementDescription(element_type),"\" ",this.NameObj());
     }
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Создаёт структуру объекта                                        |
//+------------------------------------------------------------------+
bool CGCnvElement::ObjectToStruct(void)
  {
//--- Сохранение целочисленных свойств
   this.m_struct_obj.id=(int)this.GetProperty(CANV_ELEMENT_PROP_ID);                               // Идентификатор элемента
   this.m_struct_obj.type=(int)this.GetProperty(CANV_ELEMENT_PROP_TYPE);                           // Тип графического элемента
   
   //---...
   //---...

   this.m_struct_obj.button_toggle=(bool)this.GetProperty(CANV_ELEMENT_PROP_BUTTON_TOGGLE);                             // Флаг "Переключатель" элемента управления, имеющего кнопку
   this.m_struct_obj.button_state=(bool)this.GetProperty(CANV_ELEMENT_PROP_BUTTON_STATE);                               // Состояние элемента управления "Переключатель", имеющего кнопку
   
   this.m_struct_obj.button_group_flag=(bool)this.GetProperty(CANV_ELEMENT_PROP_BUTTON_GROUP);                          // Флаг группы кнопки
   this.m_struct_obj.multicolumn=(bool)this.GetProperty(CANV_ELEMENT_PROP_LIST_BOX_MULTI_COLUMN);                       // Горизонтальное отображение столбцов в элементе управления ListBox
   this.m_struct_obj.column_width=(int)this.GetProperty(CANV_ELEMENT_PROP_LIST_BOX_COLUMN_WIDTH);                       // Ширина каждого столбца элемента управления ListBox
   this.m_struct_obj.tab_multiline=(bool)this.GetProperty(CANV_ELEMENT_PROP_TAB_MULTILINE);                             // Несколько рядов вкладок в элементе управления TabControl
   this.m_struct_obj.tab_alignment=(int)this.GetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT);                              // Местоположение вкладок внутри элемента управления
   this.m_struct_obj.alignment=(int)this.GetProperty(CANV_ELEMENT_PROP_ALIGNMENT);                                      // Местоположение объекта внутри элемента управления
//--- Сохранение вещественных свойств

//--- Сохранение строковых свойств
   ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_NAME_OBJ),this.m_struct_obj.name_obj);   // Имя объекта-графического элемента
   ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_NAME_RES),this.m_struct_obj.name_res);   // Имя графического ресурса
   ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_TEXT),this.m_struct_obj.text);           // Текст графического элемента
   ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_DESCRIPTION),this.m_struct_obj.descript);// Описание графического элемента
   //--- Сохранение структуры в uchar-массив
   ::ResetLastError();
   if(!::StructToCharArray(this.m_struct_obj,this.m_uchar_array))
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_SAVE_OBJ_STRUCT_TO_UARRAY,true);
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Создаёт объект из структуры                                      |
//+------------------------------------------------------------------+
void CGCnvElement::StructToObject(void)
  {
//--- Сохранение целочисленных свойств
   this.SetProperty(CANV_ELEMENT_PROP_ID,this.m_struct_obj.id);                                    // Идентификатор элемента
   this.SetProperty(CANV_ELEMENT_PROP_TYPE,this.m_struct_obj.type);                                // Тип графического элемента
   
   //---...
   //---...

   this.SetProperty(CANV_ELEMENT_PROP_BUTTON_TOGGLE,this.m_struct_obj.button_toggle);                             // Флаг "Переключатель" элемента управления, имеющего кнопку
   this.SetProperty(CANV_ELEMENT_PROP_BUTTON_STATE,this.m_struct_obj.button_state);                               // Состояние элемента управления "Переключатель", имеющего кнопку

   this.SetProperty(CANV_ELEMENT_PROP_BUTTON_GROUP,this.m_struct_obj.button_group_flag);                          // Флаг группы кнопки
   this.SetProperty(CANV_ELEMENT_PROP_LIST_BOX_MULTI_COLUMN,this.m_struct_obj.multicolumn);                       // Горизонтальное отображение столбцов в элементе управления ListBox
   this.SetProperty(CANV_ELEMENT_PROP_LIST_BOX_COLUMN_WIDTH,this.m_struct_obj.column_width);                      // Ширина каждого столбца элемента управления ListBox
   this.SetProperty(CANV_ELEMENT_PROP_TAB_MULTILINE,this.m_struct_obj.tab_multiline);                             // Несколько рядов вкладок в элементе управления TabControl
   this.SetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT,this.m_struct_obj.tab_alignment);                             // Местоположение вкладок внутри элемента управления
   this.SetProperty(CANV_ELEMENT_PROP_ALIGNMENT,this.m_struct_obj.alignment);                                     // Местоположение объекта внутри элемента управления
//--- Сохранение вещественных свойств

//--- Сохранение строковых свойств
   this.SetProperty(CANV_ELEMENT_PROP_NAME_OBJ,::CharArrayToString(this.m_struct_obj.name_obj));   // Имя объекта-графического элемента
   this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,::CharArrayToString(this.m_struct_obj.name_res));   // Имя графического ресурса
   this.SetProperty(CANV_ELEMENT_PROP_TEXT,::CharArrayToString(this.m_struct_obj.text));           // Текст графического элемента
   this.SetProperty(CANV_ELEMENT_PROP_DESCRIPTION,::CharArrayToString(this.m_struct_obj.descript));// Описание графического элемента
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Создаёт графический объект-элемент                               |
//+------------------------------------------------------------------+
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;
     }
   CMessage::ToLog(DFUN_ERR_LINE,::GetLastError(),true);
   return false;
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Возвращает количество графических элементов по типу              |
//+------------------------------------------------------------------+
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;
  }
//+------------------------------------------------------------------+

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


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

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

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

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


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

Как же нам отправить тип создаваемого объекта, расположенного в иерархии наследования далеко от класса CForm? Ответ очевиден: у каждого такого класса должен быть ещё один конструктор, в котором не указывается однозначно тип создаваемого объекта (как это делается сейчас), а передаётся в родительский класс через переменную конструктора в его списке инициализации. И такой конструктор должен быть защищённым — чтобы он мог работать только в наследуемых классах, а доступ извне к нему был запрещён. И вот такие конструкторы уже созданы для каждого WinForms-объекта, и при наследовании друг от друга тип дочернего класса передаётся в класс родительского. И так происходит по всей цепочке иерархии объектов вплоть до объекта CGCnvElement, в котором будет создан нужный графический элемент с тем типом, который "дошёл" по всей цепочке наследования до своего родителя.

И не забываем, что во всех файлах всех классов WInForms-объектов формальные переменные "name" уже переименованы в "descript" в конструкторах и методах создания графических элементов. Сообщаю об этом ещё раз — чтобы более к этому вопросу не возвращаться и не описывать уже сделанные одинаковые изменения для каждого WinForms-объекта из всех имеющихся.

В файле объекта-формы \MQL5\Include\DoEasy\Objects\Graph\Form.mqh удалим объявление более не нужного метода, возвращающего имя зависимого объекта (все имена теперь создаются автоматически в классе CGCnvElement):

//--- Создаёт объект для тени
   void              CreateShadowObj(const color colour,const uchar opacity);
//--- Возвращает имя зависимого объекта
   string            CreateNameDependentObject(const string base_name)  const
                       { return ::StringSubstr(this.NameObj(),::StringLen(::MQLInfoString(MQL_PROGRAM_NAME))+1)+"_"+base_name;   }
//--- Обновляет координаты привязанных объектов
   virtual bool      MoveDependentObj(const int x,const int y,const bool redraw=false);

Также удалим и код реализации этого метода из листинга класса.


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

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

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

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

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

//+------------------------------------------------------------------+
//| Защищённый конструктор с указанием типа объекта,                 |
//| идентификатора чарта и подокна                                   |
//+------------------------------------------------------------------+
CForm::CForm(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) : CGCnvElement(type,chart_id,subwindow,descript,x,y,w,h)
  {
   this.m_type=OBJECT_DE_TYPE_GFORM; 
   this.Initialize();
   this.SetCoordXInit(x);
   this.SetCoordYInit(y);
   this.SetWidthInit(w);
   this.SetHeightInit(h);
  }
//+------------------------------------------------------------------+

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

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

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

//+------------------------------------------------------------------+
//| Создаёт новый графический объект                                 |
//+------------------------------------------------------------------+
CGCnvElement *CForm::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)
  {
   string name=this.CreateNameDependentObject(obj_name);
   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(type,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));
   element.SetMovable(movable);
   element.SetCoordXRelative(element.CoordX()-this.CoordX());
   element.SetCoordYRelative(element.CoordY()-this.CoordY());
   return element;
  }
//+------------------------------------------------------------------+

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

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

//--- Создаём имя графического элемента
   string ns=(::StringLen((string)num)<2 ? ::IntegerToString(num,2,'0') : (string)num);
   string name="Elm"+ns;

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

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

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

   return obj;
  }
//+------------------------------------------------------------------+


В методе, создающем объект тени, впишем вместо имени объекта его описание по умолчанию:

//+------------------------------------------------------------------+
//| Создаёт объект тени                                              |
//+------------------------------------------------------------------+
void CForm::CreateShadowObj(const color colour,const uchar opacity)
  {
//--- Если флаг тени выключен, или объект тени уже существует - уходим
   if(!this.m_shadow || this.m_shadow_obj!=NULL)
      return;

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

//--- Создаём новый объект тени и записываем указатель на него в переменную
   this.m_shadow_obj=new CShadowObj(this.ChartID(),this.SubWindow(),this.NameObj()+"Shadow",x,y,w,h);
   if(this.m_shadow_obj==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_FAILED_CREATE_SHADOW_OBJ));
      return;
     }
   this.m_list_tmp.Add(this.m_shadow_obj);
//--- Устанавливаем свойства созданному объекту-тени
   
   //---...
   //---...
   //---...

//--- Объект-форму перемещаем на передний план
   this.BringToTop();
  }
//+------------------------------------------------------------------+

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


В файле класса базового WinForms-объекта \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh объявим защищённый конструктор и удалим один из публичных — он не нужен здесь:

protected:
//--- Защищённый конструктор с указанием типа объекта, идентификатора чарта и подокна
                     CWinFormBase(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:
//--- Конструкторы
                     CWinFormBase(const long chart_id,
                                  const int subwindow,
                                  const string descript,
                                  const int x,
                                  const int y,
                                  const int w,
                                  const int h);
                     CWinFormBase(const string name) : CForm(::ChartID(),0,name,0,0,0,0)
                       {
                        this.m_type=OBJECT_DE_TYPE_GWF_BASE; 
                       }

//--- (1) Устанавливает, (2) возвращает цвет текста по умолчанию всех объектов на панели


Реализация защищённого конструктора практически полностью повторяет реализацию публичного параметрического:

//+------------------------------------------------------------------+
//| Защищённый конструктор с указанием типа объекта,                 |
//| идентификатора чарта и подокна                                   |
//+------------------------------------------------------------------+
CWinFormBase::CWinFormBase(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) : CForm(type,chart_id,subwindow,descript,x,y,w,h)
  {
//--- Установим объекту указанный тип графического элемента, а тип объекта библиотеки - как тип этого объекта
   this.SetTypeElement(type);
   this.m_type=OBJECT_DE_TYPE_GWF_BASE; 
//--- Инициализируем все переменные
   this.SetText("");
   this.SetForeColor(CLR_DEF_FORE_COLOR,true);
   this.SetForeStateOnColor(this.ForeColor(),true);
   this.SetForeStateOnColorMouseDown(this.ForeColor());
   this.SetForeStateOnColorMouseOver(this.ForeColor());
   this.SetForeColorOpacity(CLR_DEF_FORE_COLOR_OPACITY);
   this.SetFontBoldType(FW_TYPE_NORMAL);
   this.SetMarginAll(0);
   this.SetPaddingAll(0);
   this.SetBorderSizeAll(0);
   this.SetDockMode(CANV_ELEMENT_DOCK_MODE_NONE,false);
   this.SetBorderStyle(FRAME_STYLE_NONE);
   this.SetAutoSize(false,false);
   CForm::SetCoordXInit(x);
   CForm::SetCoordYInit(y);
   CForm::SetWidthInit(w);
   CForm::SetHeightInit(h);
   this.m_shadow=false;
   this.m_gradient_v=true;
   this.m_gradient_c=false;
  }
//+------------------------------------------------------------------+

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

//--- Установим объекту указанный тип графического элемента, а тип объекта библиотеки - как тип этого объекта
   this.SetTypeElement(type);
   this.m_type=OBJECT_DE_TYPE_GWF_BASE; 
//--- Инициализируем все переменные

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

//--- Установим тип графического элемента и тип объекта библиотеки как базовый WinForms-объект
   CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_WF_BASE);
   CGCnvElement::SetProperty(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_WF_BASE);
   this.m_type=OBJECT_DE_TYPE_GWF_BASE; 
//--- Инициализируем все переменные

Аналогичные доработки внесны во все классы всех WinForms-объектов, и далее уже рассматриваться не будут.


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

//+------------------------------------------------------------------+
//| Возвращает описание строкового свойства элемента                 |
//+------------------------------------------------------------------+
string CWinFormBase::GetPropertyDescription(ENUM_CANV_ELEMENT_PROP_STRING property,bool only_prop=false)
  {
   return
     (
      property==CANV_ELEMENT_PROP_NAME_OBJ      ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_NAME_OBJ)+": \""+this.GetProperty(property)+"\""     :
      property==CANV_ELEMENT_PROP_NAME_RES      ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_NAME_RES)+": \""+this.GetProperty(property)+"\""     :
      property==CANV_ELEMENT_PROP_TEXT          ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TEXT)+": \""+this.GetProperty(property)+"\""         :
      property==CANV_ELEMENT_PROP_DESCRIPTION   ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_DESCRIPTION)+": \""+this.GetProperty(property)+"\""  :
      ""
     );
  }
//+------------------------------------------------------------------+


Все вышеозвученные доработки сделаны в классах WinForms-объектов в файлах:

\MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\CommonBase.mqh,
\MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Label.mqh,
\MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\CheckBox.mqh,
\MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\RadioButton.mqh,
\MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\CheckedListBox.mqh,
\MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\ButtonListBox.mqh,

В файле объекта-кнопки \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\Button.mqh помимо таких же общих для всех WinForms-объектов доработок, сделаем метод для установки состояния виртуальным, так как в наследуемых классах его потребуется переопределить:

   bool              Toggle(void)                        const { return (bool)this.GetProperty(CANV_ELEMENT_PROP_BUTTON_TOGGLE);                               }
//--- (1) Устанавливает, (2) возвращает состояние элемента управления "Переключатель"
   virtual void      SetState(const bool flag)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_BUTTON_STATE,flag);
                        if(this.State())
                          {
                           this.SetBackgroundColor(this.BackgroundStateOnColor(),false);
                           this.SetForeColor(this.ForeStateOnColor(),false);
                           this.UnpressOtherAll();
                          }
                        else
                          {
                           this.SetBackgroundColor(this.BackgroundColorInit(),false);
                           this.SetForeColor(this.ForeColorInit(),false);
                           this.SetBorderColor(this.BorderColorInit(),false);
                          }
                       }
   bool              State(void)                         const { return (bool)this.GetProperty(CANV_ELEMENT_PROP_BUTTON_STATE);                                }

 

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

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

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


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

//+------------------------------------------------------------------+
//| Класс объекта Label элементов управления WForms                  |
//+------------------------------------------------------------------+
class CListBoxItem : public CButton
  {
private:
   uchar             m_text_shift;                          // Смещение текста элемента вправо от левого края в символах
   string            m_shift_space;                         // Строка смещения
protected:
//--- Защищённый конструктор с указанием типа объекта, идентификатора чарта и подокна
                     CListBoxItem(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:
//--- Возвращает смещение текста элемента вправо от левого края в символах
   uchar             TextShift(void)                        const { return this.m_text_shift;   }
//--- (1) Создаёт, (2) возвращает строку, состоящую из количества символов смещения
   void              SetTextShiftSpace(const uchar value);
   string            GetTextShiftSpace(void)                const { return this.m_shift_space;  }
//--- Устанавливает текст элемента
   virtual void      SetText(const string text);
//--- Конструктор
                     CListBoxItem(const long chart_id,
                                  const int subwindow,
                                  const string descript,
                                  const int x,
                                  const int y,
                                  const int w,
                                  const int h);
  };
//+------------------------------------------------------------------+


Конструкторы класса:

//+------------------------------------------------------------------+
//| Защищённый конструктор с указанием типа объекта,                 |
//| идентификатора чарта и подокна                                   |
//+------------------------------------------------------------------+
CListBoxItem::CListBoxItem(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(type);
   this.m_type=OBJECT_DE_TYPE_GWF_COMMON;
   this.SetTextAlign(ANCHOR_LEFT);
   this.SetTextShiftSpace(1);
  }
//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CListBoxItem::CListBoxItem(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_LIST_BOX_ITEM,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM);
   this.m_type=OBJECT_DE_TYPE_GWF_COMMON;
   this.SetTextAlign(ANCHOR_LEFT);
   this.SetTextShiftSpace(1);
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Создаёт строку, состоящую из количества символов смещения        |
//+------------------------------------------------------------------+
void CListBoxItem::SetTextShiftSpace(const uchar value)
  {
   this.m_text_shift=value;
   this.m_shift_space="";
   switch(this.TextAlign())
     {
      case ANCHOR_LEFT        :
      case ANCHOR_LEFT_LOWER  :
      case ANCHOR_LEFT_UPPER  :
        for(int i=0;i<(int)this.m_text_shift;i++)
           this.m_shift_space+=" ";
        break;
      default:
        break;
     }
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Устанавливает текст элемента                                     |
//+------------------------------------------------------------------+
void CListBoxItem::SetText(const string text)
  {
   this.SetProperty(CANV_ELEMENT_PROP_TEXT,this.GetTextShiftSpace()+text);
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Класс объекта ListBox элементов управления WForms                |
//+------------------------------------------------------------------+
class CListBox : public CElementsListBox
  {
private:
   uchar             m_text_shift;                       // Смещение текста элементов ListBoxItem вправо от левого края в символах
//--- Создаёт новый графический объект
   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);
public:
//--- (1) Устанавливает, (2) возвращает смещение текста элемента вправо от левого края в символах
   void              SetTextShift(const uchar value);
   uchar             TextShift(void)                  const { return this.m_text_shift;   }
//--- Создаёт список из указанного количества строк (объектов Label)
   void              CreateList(const int line_count,const int new_column_width=0,const bool autosize=true);
   
protected:
//--- Защищённый конструктор с указанием типа объекта, идентификатора чарта и подокна
                     CListBox(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:
//--- Конструктор
                     CListBox(const long chart_id,
                              const int subwindow,
                              const string descript,
                              const int x,
                              const int y,
                              const int w,
                              const int h);
  };
//+------------------------------------------------------------------+

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

Доработаем метод, создающий список из указанного количества строк (ListBoxItem).

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

//+------------------------------------------------------------------+
//| Создаёт список из указанного количества строк (ListBoxItem)      |
//+------------------------------------------------------------------+
void CListBox::CreateList(const int count,const int new_column_width=0,const bool autosize=true)
  {
//--- Создаём указатель на объект CListBoxItem
   CListBoxItem *obj=NULL;
//--- Рассчитываем ширину создаваемого объекта в зависимости от указанной ширины столбца
   int width=(new_column_width>0 ? new_column_width : this.Width()-this.BorderSizeLeft()-this.BorderSizeRight());
//--- Создаём указанное количество объектов ListBoxItem
   CElementsListBox::CreateElements(GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM,count,0,0,width,15,new_column_width,autosize);
//--- В цикле по созданному количеству объектов
   for(int i=0;i<this.ElementsTotal();i++)
     {
      //--- Получаем созданный объект из списка по индексу цикла
      obj=this.GetElement(i);
      //--- Если объект получить не удалось - выводим об этом сообщение в журнал и идём к следующему
      if(obj==NULL)
        {
         ::Print(DFUN,MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ,": ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM));
         continue;
        }
      //--- Устанавливаем выравнивание текста слева по центру
      obj.SetTextAlign(ANCHOR_LEFT);
      obj.SetTextShiftSpace(this.TextShift());
      //--- Устанавливаем текст объекта
      obj.SetFontSize(8);
      obj.SetText(TypeGraphElementAsString(obj.TypeGraphElement())+string(i+1));
      //--- Устанавливаем цвета фона, текста и рамки
      obj.SetBackgroundStateOnColor(clrDodgerBlue,true);
      obj.SetBackgroundStateOnColorMouseOver(obj.ChangeColorLightness(obj.BackgroundStateOnColor(),-5));
      obj.SetBackgroundStateOnColorMouseDown(obj.ChangeColorLightness(obj.BackgroundStateOnColor(),-10));
      obj.SetForeStateOnColor(this.BackgroundColor(),true);
      obj.SetForeStateOnColorMouseOver(obj.ChangeColorLightness(obj.ForeStateOnColor(),-5));
      obj.SetForeStateOnColorMouseDown(obj.ChangeColorLightness(obj.ForeStateOnColor(),-10));
      obj.SetBorderColor(obj.BackgroundColor(),true);
      obj.SetBorderColorMouseDown(obj.BackgroundColorMouseDown());
      obj.SetBorderColorMouseOver(obj.BackgroundColorMouseOver());
      //--- Ставим флаги кнопки-переключателя и групповой кнопки
      obj.SetToggleFlag(true);
      obj.SetGroupButtonFlag(true);
     }
//--- Если в метод передан флаг автоматического изменения размеров базового объекта -
//--- устанавливаем режим автоизменения размера как "увиличить и уменьшить"
   if(autosize)
      this.SetAutoSizeMode(CANV_ELEMENT_AUTO_SIZE_MODE_GROW_SHRINK,false);
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Устанавливает смещение текста элемента                           |
//| вправо от левого края в символах                                 |
//+------------------------------------------------------------------+
void CListBox::SetTextShift(const uchar value)
  {
   this.m_text_shift=value;
   for(int i=0;i<this.ElementsTotal();i++)
     {
      CListBoxItem *obj=this.GetElement(i);
      if(obj==NULL || obj.TextShift()==value)
         continue;
      obj.SetTextShiftSpace(value);
      obj.SetText(obj.Text());
      obj.Update(false);
     }
  }
//+------------------------------------------------------------------+

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

В классе базового объекта-контейнера в файле MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Container.mqh удалим лишний конструктор:

                     CContainer(const string name) : CWinFormBase(::ChartID(),0,name,0,0,0,0)
                       {
                        CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_WF_CONTAINER);
                        CGCnvElement::SetProperty(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_WF_CONTAINER);
                        this.m_type=OBJECT_DE_TYPE_GWF_CONTAINER; 
                        this.SetForeColor(CLR_DEF_FORE_COLOR,true);
                        this.SetFontBoldType(FW_TYPE_NORMAL);
                        this.SetMarginAll(3);
                        this.SetPaddingAll(0);
                        this.SetDockMode(CANV_ELEMENT_DOCK_MODE_NONE,false);
                        this.SetBorderStyle(FRAME_STYLE_NONE);
                        this.SetAutoScroll(false,false);
                        this.SetAutoScrollMarginAll(0);
                        this.SetAutoSize(false,false);
                        this.SetAutoSizeMode(CANV_ELEMENT_AUTO_SIZE_MODE_GROW,false);
                        this.Initialize();
                       }
//--- Деструктор
                    ~CContainer();
  };
//+------------------------------------------------------------------+


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

protected:
//--- Защищённый конструктор с указанием типа объекта, идентификатора чарта и подокна
                     CContainer(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:
//--- Конструктор
                     CContainer(const long chart_id,
                                const int subwindow,
                                const string descript,
                                const int x,
                                const int y,
                                const int w,
                                const int h);

Реализация защищённого конструктора идентична всем, ранее добавленным защищённым конструкторам в других классах.


В методе, устанавливающим параметры присоединённому объекту, добавим обработку объекта ListBoxItem такую же, как и для объектов Button и  TabHeader:

      //--- Для 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", "ListBoxItem"
      case GRAPH_ELEMENT_TYPE_WF_BUTTON            :
      case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER        :
      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   :
        //--- устанавливаем цвет текста объекта как цвет текста контейнера в зависимости от переданного в метод:
        //--- цвет фона устанавливаем  в зависимости от переданного в метод:
        //--- либо цвет фона стандартных элементов управления по умолчанию, либо переданный в метод.
        //--- Цвет рамки устанавливаем равным цвету текста
        obj.SetBackgroundColor(colour==clrNONE ? CLR_DEF_CONTROL_STD_BACK_COLOR : colour,true);
        obj.SetBorderColor(CLR_DEF_BORDER_COLOR,true);
        obj.SetForeColor(CLR_DEF_FORE_COLOR,true);
        break;


Обработку теперь отсутствующего объекта TabPage удалим из метода:

        break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_PAGE          :
        //--- устанавливаем цвет текста объекта как цвет текста контейнера в зависимости от переданного в метод:
        //--- цвет фона устанавливаем  в зависимости от переданного в метод:
        //--- либо цвет фона стандартных элементов управления по умолчанию, либо переданный в метод.
        //--- Цвет рамки устанавливаем равным цвету текста
        obj.SetBackgroundColor(colour==clrNONE ? CLR_DEF_CONTROL_TAB_PAGE_BACK_COLOR : colour,true);
        obj.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_DOWN);
        obj.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_OVER);
        obj.SetBorderColor(CLR_DEF_CONTROL_TAB_PAGE_BORDER_COLOR,true);
        obj.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_DOWN);
        obj.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_OVER);
        obj.SetForeColor(CLR_DEF_FORE_COLOR,true);
        obj.SetOpacity(CLR_DEF_CONTROL_TAB_PAGE_OPACITY);
        obj.SetBorderSizeAll(1);
        obj.SetBorderStyle(FRAME_STYLE_NONE);
        break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL       :
  


К файлу класса базового объекта-списка элементов управления \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\ElementsListBox.mqh подключим файл класса CListBoxItem:

//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "..\Containers\Container.mqh"
#include "..\ListBoxItem.mqh"
//+------------------------------------------------------------------+
//| Класс базового объекта списка элементов управления WForms        |
//+------------------------------------------------------------------+

Теперь класс коллекции списка элементов управления будет доступен в других объектах библиотеки.

Так же, как и в остальных объектах, объявим защищённый конструктор:

protected:
//--- Создаёт указанное количество указанных WinForms-объектов
   void              CreateElements(ENUM_GRAPH_ELEMENT_TYPE element_type,
                                    const int count,
                                    const int x,
                                    const int y,
                                    const int w,
                                    const int h,
                                    uint new_column_width=0,
                                    const bool autosize=true);
                                    
//--- Защищённый конструктор с указанием типа объекта, идентификатора чарта и подокна
                     CElementsListBox(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:
//--- Конструктор

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


Класс TabHeader — заголовок вкладки объекта TabControl

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

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

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

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


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

//+------------------------------------------------------------------+
//| Класс объекта TabHeader элемента управления WForms TabControl    |
//+------------------------------------------------------------------+
class CTabHeader : public CButton
  {
private:
   int               m_width_off;                        // Ширина объекта в состоянии "не выбран"
   int               m_height_off;                       // Высота объекта в состоянии "не выбран"
   int               m_width_on;                         // Ширина объекта в состоянии "выбран"
   int               m_height_on;                        // Высота объекта в состоянии "выбран"
   int               m_col;                              // Номер колонки заголовка
   int               m_row;                              // Номер строки заголовка
//--- Устанавливает ширину, высоту и смещение элемента в зависимости от состояния
   void              SetWH(void);
//--- Подстраивает размер и расположение элемента в зависимости от состояния
   void              WHProcessStateOn(void);
   void              WHProcessStateOff(void);
//--- Русует рамку элемента в зависимости от расположения
   void              DrawFrame(void);
   
protected:
//--- Обработчик события  Курсор в пределах активной области, отжата кнопка мышки (левая)
   virtual void      MouseActiveAreaReleasedHandler(const int id,const long& lparam,const double& dparam,const string& sparam);

public:
//---Устанавливает размеры элемента управления в состоянии (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;  }
//--- Возвращает размеры элемента управления
   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 index,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);
                        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);
                       }
   ENUM_CANV_ELEMENT_ALIGNMENT Alignment(void)  const { return (ENUM_CANV_ELEMENT_ALIGNMENT)this.GetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT);  }
   

//--- Устанавливает состояние элемента управления
   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);

protected:
//--- Защищённый конструктор с указанием типа объекта, идентификатора чарта и подокна
                     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);
public:
//--- Конструктор
                     CTabHeader(const long chart_id,
                             const int subwindow,
                             const string descript,
                             const int x,
                             const int y,
                             const int w,
                             const int h);
  };
//+------------------------------------------------------------------+

Из описаний переменных и методов должно быть понятно их назначение. Рассмотрим их подробнее.

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

//+------------------------------------------------------------------+
//| Защищённый конструктор с указанием типа объекта,                 |
//| идентификатора чарта и подокна                                   |
//+------------------------------------------------------------------+
CTabHeader::CTabHeader(const ENUM_GRAPH_ELEMENT_TYPE type,
                       const long chart_id,
                       const int subwindow,
                       const string descript,
                       const int x,
                       const int y,
                       const int w,
                       const int h) : CButton(type,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
   this.m_type=OBJECT_DE_TYPE_GWF_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.SetWidthOff(this.Width());
   this.SetHeightOff(this.Height());
   this.SetWidthOn(this.Width()+4);
   this.SetHeightOn(this.Height()+2);
   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.SetWidthOff(this.Width());
   this.SetHeightOff(this.Height());
   this.SetWidthOn(this.Width()+4);
   this.SetHeightOn(this.Height()+2);
   this.SetState(false);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Устанавливает состояние элемента управления                      |
//+------------------------------------------------------------------+
void CTabHeader::SetState(const bool flag)
  {
   bool state=this.State();
   CButton::SetState(flag);
   if(state!=flag)
      this.SetWH();
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Устанавливает ширину, высоту и смещение элемента                 |
//| в зависимости от его состояния                                   |
//+------------------------------------------------------------------+
void CTabHeader::SetWH(void)
  {
   if(this.State())
      this.WHProcessStateOn();
   else
      this.WHProcessStateOff();
  }
//+------------------------------------------------------------------+

Если состояние "включено", то вызываем метод изменения размера и расположения для состояния "включено", иначе — вызываем метод изменения размера и расположения для состояния "выключено".


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

//+------------------------------------------------------------------+
//| Подстраивает размер и расположение элемента                      |
//| в состоянии "выбран" в зависимости от его расположения           |
//+------------------------------------------------------------------+
void CTabHeader::WHProcessStateOn(void)
  {
//--- Еси не удалось установить новый размер - уходим
   if(!this.SetSizeOn())
      return;
//--- В зависимости от расположения заголовка
   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;
      default:
        break;
     }
   this.Update(false);
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Подстраивает размер и расположение элемента                      |
//| в состоянии "не выбран" в зависимости от его расположения        |
//+------------------------------------------------------------------+
void CTabHeader::WHProcessStateOff(void)
  {
//--- Еси не удалось установить новый размер - уходим
   if(!this.SetSizeOff())
      return;
//--- В зависимости от расположения заголовка
   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;
      default:
        break;
     }
   this.Update(false);
  }
//+------------------------------------------------------------------+

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


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

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

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


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

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

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


Метод, очищающий элемент заливкой градиентом:

//+------------------------------------------------------------------+
//| Очищает элемент заливкой градиентом                              |
//+------------------------------------------------------------------+
void CTabHeader::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 && redraw)
      this.DrawFrame();
//--- Обновляем элемент с указанным флагом необходимости перерисовки
   this.Update(redraw);
  }
//+------------------------------------------------------------------+

Метод идентичен вышерассмотренному, но заливает фон градиентным цветом из массива цветов, переданного в метод.


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

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


Обработчик последнего события мышки:

//+------------------------------------------------------------------+
//| Обработчик последнего события мышки                              |
//+------------------------------------------------------------------+
void CTabHeader::OnMouseEventPostProcessing(void)
  {
   ENUM_MOUSE_FORM_STATE state=GetMouseState();
   switch(state)
     {
      //--- Курсор за пределами формы, кнопки мышки не нажаты
      //--- Курсор за пределами формы, нажата кнопка мышки (любая)
      //--- Курсор за пределами формы, прокручивается колёсико мышки
      case MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED :
      case MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED     :
      case MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL       :
        if(this.MouseEventLast()==MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED || this.MouseEventLast()==MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED)
          {
           this.SetBackgroundColor(this.State() ? this.BackgroundStateOnColor() : this.BackgroundColorInit(),false);
           this.SetForeColor(this.State() ? this.ForeStateOnColor() : this.ForeColorInit(),false);
           this.SetBorderColor(this.BorderColorInit(),false);
           this.m_mouse_event_last=ENUM_MOUSE_EVENT(state+MOUSE_EVENT_NO_EVENT);
           this.Redraw(false);
          }
        break;

      //--- Курсор в пределах формы, кнопки мышки не нажаты
      //--- Курсор в пределах формы, нажата кнопка мышки (любая)
      //--- Курсор в пределах формы, прокручивается колёсико мышки
      //--- Курсор в пределах активной области, кнопки мышки не нажаты
      //--- Курсор в пределах активной области, нажата кнопка мышки (любая)
      //--- Курсор в пределах активной области, прокручивается колёсико мышки
      //--- Курсор в пределах активной области, отжата кнопка мышки (левая)
      //--- Курсор в пределах области прокрутки окна, кнопки мышки не нажаты
      //--- Курсор в пределах области прокрутки окна, нажата кнопка мышки (любая)
      //--- Курсор в пределах области прокрутки окна, прокручивается колёсико мышки
      case MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_FORM_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_FORM_WHEEL :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL :
      case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_RELEASED :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED :
      case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL :
        break;
      //---MOUSE_EVENT_NO_EVENT
      default:
        break;
     }
  }
//+------------------------------------------------------------------+

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


Класс TabControl — продолжаем разработку

В прошлой статье мы начали разработку элемента управления TabControl, но столкнулись с ограничением длины имён создаваемых графических объектов. После создания нового алгоритма именования графических элементов библиотеки,
продолжим работу в фале \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\TabControl.mqh.

Подключим к файлу элемента управления файл класса объекта-заголовка вкладки:

//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "Container.mqh"
#include "GroupBox.mqh"
#include "..\TabHeader.mqh"
//+------------------------------------------------------------------+


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

private:
   int                  m_item_width;                 // Фиксированная ширина заголовков вкладок
   int                  m_item_height;                // Фиксированная высота заголовков вкладок
//--- Создаёт новый графический объект
   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);
//--- Устанавливает вкладку выбранной
   void              SetSelected(const int index);
//--- Устанавливает вкладку не выбранной
   void              SetUnselected(const int index);
   
public:


Публичный метод CreateTabPage() переименуем в CreateTabPages(), сделаем его с возвращаемым типом bool и с иным набором формальных параметров, а также добавим два метода, возвращающие указатели на заголовок и поле вкладки по индексу:

public:
//--- Создаёт указанное количество вкладок
   bool              CreateTabPages(const int total,const int selected_page,const int tab_w=0,const int tab_h=0,const string header_text="");
   
//--- Возвращает указатель на (1) заголовок, (2) поле вкладки
   CTabHeader       *GetHeader(const int index)          { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,index); }
   CContainer       *GetField(const int index)           { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_CONTAINER,index);  }


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

//--- (1) Устанавливает, (2) возвращает местоположение вкладок на элементе управления
   void              SetAlignment(const ENUM_CANV_ELEMENT_ALIGNMENT alignment)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_TAB_ALIGNMENT,alignment);
                        CArrayObj *list=this.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;
                           header.SetAlignment(alignment);
                          }
                       }

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


Объявим ещё три публичных метода:

//--- Устанавливает фиксированный размер вкладки
   void              SetItemSize(const int w,const int h)
                       {
                        if(this.ItemWidth()!=w)
                           this.SetItemWidth(w);
                        if(this.ItemHeight()!=h)
                           this.SetItemHeight(h);
                       }
//--- Устанавливает вкладку выбранной/не выбранной
   void              Select(const int index,const bool flag);

//--- Устанавливает текст заголовка (1) указанной вкладки, (2) по индексу
   void              SetHeaderText(CTabHeader *header,const string text);
   void              SetHeaderText(const int index,const string text);

//--- Конструктор


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

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

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


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

//+------------------------------------------------------------------+
//| Создаёт указанное количество вкладок                             |
//+------------------------------------------------------------------+
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;
   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.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());
      if(header_text!="" && header_text!=NULL)
         this.SetHeaderText(header,header_text+string(i+1));
      
      //--- В зависимости от расположения заголовков вкладок устанавливаем начальные координаты полей вкладок
      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;
      //--- Создаём объект Container (поле вкладки)
      CContainer *field=NULL;
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_CONTAINER,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_CONTAINER),string(i+1));
         return false;
        }
      field=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_CONTAINER,i);
      if(field==NULL)
        {
         ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_CONTAINER),string(i+1));
         return false;
        }
      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.Hide();
   //---
     }
   this.Select(selected_page,true);
   return true;
  }
//+------------------------------------------------------------------+

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

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


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

//+------------------------------------------------------------------+
//| Устанавливает вкладку выбранной                                  |
//+------------------------------------------------------------------+
void CTabControl::SetSelected(const int index)
  {
   CTabHeader *header=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,index);
   CContainer *field=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_CONTAINER,index);
   if(header==NULL || field==NULL)
      return;
   field.Show();
   field.BringToTop();
   header.SetState(true);
   header.BringToTop();
  }
//+------------------------------------------------------------------+

Получаем указатели на заголовок и поле вкладки по индексу.
Отображаем поле и выводим его на передний план
.
Устанавливаем выбранным заголовок и вводим его на передний план.


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

//+------------------------------------------------------------------+
//| Устанавливает вкладку не выбранной                               |
//+------------------------------------------------------------------+
void CTabControl::SetUnselected(const int index)
  {
   CTabHeader *header=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,index);
   CContainer *field=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_CONTAINER,index);
   if(header==NULL || field==NULL)
      return;
   field.Hide();
   header.SetState(false);
  }
//+------------------------------------------------------------------+

Получаем указатели на заголовок и поле вкладки по индексу.
Скрываем поле и у
станавливаем заголовок как не выбранный.


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

//+------------------------------------------------------------------+
//| Устанавливает вкладку выбранной/не выбранной                     |
//+------------------------------------------------------------------+
void CTabControl::Select(const int index,const bool flag)
  {
   if(flag)
      this.SetSelected(index);
   else
      this.SetUnselected(index);
  }
//+------------------------------------------------------------------+

В метод передаётся индекс вкладки и флаг, и в зависимости от значения флага, вызываем один из двух вышерассмотренных методов.


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

//+------------------------------------------------------------------+
//| Устанавливает текст заголовка указанной вкладки                  |
//+------------------------------------------------------------------+
void CTabControl::SetHeaderText(CTabHeader *header,const string text)
  {
   if(header==NULL)
      return;
   header.SetText(text);
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Устанавливает текст заголовка вкладки по индексу                 |
//+------------------------------------------------------------------+
void CTabControl::SetHeaderText(const int index,const string text)
  {
   CTabHeader *header=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,index);
   this.SetHeaderText(header,text);
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Создаёт новый графический объект                                 |
//+------------------------------------------------------------------+
CGCnvElement *CTabControl::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                         const int obj_num,
                                         const string descript,
                                         const int x,
                                         const int y,
                                         const int w,
                                         const int h,
                                         const color colour,
                                         const uchar opacity,
                                         const bool movable,
                                         const bool activity)
  {

   CGCnvElement *element=NULL;
   switch(type)
     {
      case GRAPH_ELEMENT_TYPE_ELEMENT              :
         element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),descript,x,y,w,h,colour,opacity,movable,activity);
        break;
      case GRAPH_ELEMENT_TYPE_FORM                 :
         element=new CForm(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_CONTAINER         :
         element=new CContainer(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_GROUPBOX          :
         element=new CGroupBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_PANEL             :
         element=new CPanel(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_LABEL             :
         element=new CLabel(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKBOX          :
         element=new CCheckBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON       :
         element=new CRadioButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON            :
         element=new CButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX          :
         element=new CListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM     :
         element=new CListBoxItem(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX  :
         element=new CCheckedListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX   :
         element=new CButtonListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
      case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER        :
         element=new CTabHeader(this.ChartID(),this.SubWindow(),descript,x,y,w,h);
        break;
                                                                                     
                                                                                     
                                                                                     
      case GRAPH_ELEMENT_TYPE_WF_TAB_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;
  }
//+------------------------------------------------------------------+

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


В классах объекта Panel в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Panel.mqh
и объекта GroupBox в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\GroupBox.mqh
сделаны аналогичные доработки, какие мы делали для WinForms-объектов: добавлен новый защищённый конструктор, удалён один ненужный параметрический конструктор и доработан виртуальный метод CreateNewGObject() точно так же, как мы только что рассмотрели выше.

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


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

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

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

//--- enumerations by compilation language
#ifdef COMPILE_EN
enum ENUM_AUTO_SIZE_MODE
  {
   AUTO_SIZE_MODE_GROW=CANV_ELEMENT_AUTO_SIZE_MODE_GROW,                // Grow
   AUTO_SIZE_MODE_GROW_SHRINK=CANV_ELEMENT_AUTO_SIZE_MODE_GROW_SHRINK   // Grow and Shrink
  };
enum ENUM_BORDER_STYLE
  {
   BORDER_STYLE_NONE=FRAME_STYLE_NONE,                                  // None
   BORDER_STYLE_SIMPLE=FRAME_STYLE_SIMPLE,                              // Simple
   BORDER_STYLE_FLAT=FRAME_STYLE_FLAT,                                  // Flat
   BORDER_STYLE_BEVEL=FRAME_STYLE_BEVEL,                                // Embossed (bevel)
   BORDER_STYLE_STAMP=FRAME_STYLE_STAMP,                                // Embossed (stamp)
  };
enum ENUM_CHEK_STATE
  {
   CHEK_STATE_UNCHECKED=CANV_ELEMENT_CHEK_STATE_UNCHECKED,              // Unchecked
   CHEK_STATE_CHECKED=CANV_ELEMENT_CHEK_STATE_CHECKED,                  // Checked
   CHEK_STATE_INDETERMINATE=CANV_ELEMENT_CHEK_STATE_INDETERMINATE,      // Indeterminate
  };
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
  };
#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,                // Справа
  };
#endif 
//--- 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      =  false;                  // Button toggle flag
//sinput   bool                          InpListBoxMColumn    =  false;                  // ListBox MultiColumn flag
sinput   bool                          InpButtListMSelect   =  false;                  // ButtonListBox Button MultiSelect flag
sinput   ENUM_ELEMENT_ALIGNMENT        InpHeaderAlignment   =  ELEMENT_ALIGNMENT_TOP;  // TabHeader Alignment
//--- global variables
CEngine        engine;
color          array_clr[];
//+------------------------------------------------------------------+


В обработчике OnInit() в блоке создания графических объектов закомментируем блок создания объекта TabControl из прошлой статьи (там мы его разместим позже) и код создания объекта ListBox (его мы будем размещать во вкладки будущего объекта TabControl). А после блока кода для создания объекта ButtonListBox разместим блок кода для создания объекта TabControl c тремя вкладками, первая из которых будет выбрана изначально:

      //--- Если прикреплённый объект 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 *tctrl=gbox2.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,0);
//            if(tctrl!=NULL)
//              {
//               //--- получим указатель на объект Container по его индексу в списке прикреплённых объектов с типом Container
//               CContainer *page=tctrl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_CONTAINER,0);
//               if(page!=NULL)
//                 {
//                  // Здесь будем создавать объекты, прикреплённые к указанной вкладке объекта TabControl
//                  // К сожалению, в текущем состоянии создания имён графических объектов библиотеки
//                  // дальнейшее их создание упирается в предел количества символов в имени ресурса в классе CCanvas
//                 }
//               
//              }
            ///*
            //--- Создадим объект CheckedListBox
            gbox2.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,4,12,160,20,clrNONE,255,true,false);
            //--- получим указатель на объект CheckedListBox по его индексу в списке прикреплённых объектов с типом CheckBox
            CCheckedListBox *clbox=gbox2.GetElementByType(GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,0);
            //--- Если CheckedListBox создан и указатель на него получен
            if(clbox!=NULL)
              {
               clbox.SetMultiColumn(true);
               clbox.SetColumnWidth(0);
               clbox.CreateCheckBox(4,66);
              }
            //--- Создадим объект ButtonListBox
            gbox2.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX,4,clbox.BottomEdgeRelative()+6,160,30,clrNONE,255,true,false);
            //--- получим указатель на объект ButtonListBox по его индексу в списке прикреплённых объектов с типом Button
            CButtonListBox *blbox=gbox2.GetElementByType(GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX,0);
            //--- Если ButtonListBox создан и указатель на него получен
            if(blbox!=NULL)
              {
               blbox.SetMultiColumn(true);
               blbox.SetColumnWidth(0);
               blbox.CreateButton(4,66,16);
               blbox.SetMultiSelect(InpButtListMSelect);
               blbox.SetToggle(InpButtonToggle);
               for(int i=0;i<blbox.ElementsTotal();i++)
                 {
                  blbox.SetButtonGroup(i,(i % 2==0 ? blbox.Group()+1 : blbox.Group()+2));
                  blbox.SetButtonGroupFlag(i,(i % 2==0 ? true : false));
                 }
              }
            
            int lbx=6;
            int lby=blbox.BottomEdgeRelative()+6;
            int lbw=180;
            //--- Создадим объект TabControl
            gbox2.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,lbx,lby,lbw,78,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.SetAlignment((ENUM_CANV_ELEMENT_ALIGNMENT)InpHeaderAlignment);
               tab_ctrl.CreateTabPages(3,0,56,16,TextByLanguage("Вкладка","TabPage"));
              }
                        
            //--- Создадим объект ListBox
            //int lbx=4;
            //int lby=blbox.BottomEdgeRelative()+6;
            //int lbw=146;
            //if(!InpListBoxMColumn)
            //  {
            //   lbx=blbox.RightEdgeRelative()+6;
            //   lby=14;
            //   lbw=100;
            //  }
            //gbox2.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_LIST_BOX,lbx,lby,lbw,60,clrNONE,255,true,false);
            ////--- получим указатель на объект ListBox по его индексу в списке прикреплённых объектов с типом ListBox
            //CListBox *lbox=gbox2.GetElementByType(GRAPH_ELEMENT_TYPE_WF_LIST_BOX,0);
            ////--- Если ListBox создан и указатель на него получен
            //if(lbox!=NULL)
            //  {
            //   lbox.SetMultiColumn(true);
            //   lbox.CreateList(8,68);
            //  }
            //*/
           }
        }

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


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


Что дальше

В следующей статье продолжим развитие элемента управления TabControl.

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

К содержанию

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

DoEasy. Элементы управления (Часть 1): Первые шаги
DoEasy. Элементы управления (Часть 2): Продолжаем работу над классом CPanel
DoEasy. Элементы управления (Часть 3): Создание привязанных элементов управления
DoEasy. Элементы управления (Часть 4): Элемент управления "Панель", параметры Padding и Dock
DoEasy. Элементы управления (Часть 5): Базовый WinForms-объект, элемент управления "Панель", параметр AutoSize
DoEasy. Элементы управления (Часть 6): Элемент управления "Панель", автоизменение размеров контейнера под внутреннее содержимое
DoEasy. Элементы управления (Часть 7): Элемент управления "Текстовая метка"
DoEasy. Элементы управления (Часть 8): Базовые WinForms-объекты по категориям, элементы управления "GroupBox" и "CheckBox
DoEasy. Элементы управления (Часть 9): Реорганизация методов WinForms-объектов, элементы управления "RadioButton" и "Button"
DoEasy. Элементы управления (Часть 10): WinForms-объекты — оживляем интерфейс
DoEasy. Элементы управления (Часть 11): WinForms-объекты — группы, WinForms-объект CheckedListBox
DoEasy. Элементы управления (Часть 12): Базовый объект-список, WinForms-объекты ListBox и ButtonListBox
DoEasy. Элементы управления (Часть 13): Оптимизация взаимодействия WinForms-объектов с мышкой, начало разработки WinForms-объекта TabControl



Прикрепленные файлы |
MQL5.zip (4505.48 KB)
Переход на новые рельсы: пользовательские индикаторы в MQL5 Переход на новые рельсы: пользовательские индикаторы в MQL5
Я не буду перечислять все новые возможности и особенности нового терминала и языка. Их действительно много, и некоторые новинки вполне достойны освещения в отдельной статье. Вы не увидите здесь кода, написанного по принципам объектно-ориентированного программирования — это слишком серьезная тема для того, чтобы просто быть упомянутой в контексте как дополнительная вкусность для кодописателей. В этой статье остановимся подробней на индикаторах, их строении, отображении, видах, а также особенностях их написания по сравнению с MQL4.
Нейросети — это просто (Часть 23): Создаём инструмент для Transfer Learning Нейросети — это просто (Часть 23): Создаём инструмент для Transfer Learning
В данной серии статей мы уже не один раз упоминали о Transfer Learning. Но дальше упоминаний пока дело не шло. Я предлагаю заполнить этот пробел. И посмотреть поближе на Transfer Learning.
Автоматическое создание документации к программам на MQL5 Автоматическое создание документации к программам на MQL5
Большинство Java программистов знакомы с автоматическим созданием документации, которая может быть создана при помощи программы JavaDocs. В мире C++ также есть несколько автоматических генераторов документации, одними из лидеров являются программы Microsoft's SandCastle и Doxygen. В статье описано, как можно использовать программу Doxygen для создания структурированных файлов справки HTML для программ, написанных на MQL5. Результаты данной работы убедили меня использовать Doxygen (или похожие программы) в будущем для создания документации к любому моему коду на MQL5, это значительно облегчает его понимание и использование.
Разработка торговой системы на основе индикатора объемов Volumes Разработка торговой системы на основе индикатора объемов Volumes
Представляю вашему вниманию новую статью из серии, в которой мы учимся создавать торговые системы на основе популярных технических индикаторов. Данная статья будет посвящена индикатору Volumes. Объем как понятие является важным факторов в торговле на финансовых рынках, и поэтому обязательно надо его учитывать. В этой статье узнаем, как разработать торговую систему на основе показателей от индикатора объемов Volumes.