DoEasy. Элементы управления (Часть 27): Продолжаем работу над WinForms-объектом "ProgressBar"

Artyom Trishkin | 18 ноября, 2022

Содержание


Концепция

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

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

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

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

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


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

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

Поэтому сделаем так: объект-блик будет унаследован от класса базового объекта всех WinForms-объектов библиотеки CWinFormBase и будет находиться в списке вспомогательных объектов. Тогда мы легко его сможем добавить всего лишь одной строкой кода в том классе, где он должен использоваться. А обработчики активных объектов у нас уже написаны. Такой подход избавит нас от рутинного добавления в разные классы библиотеки функционала, идентичного функционалу обработки объекта-тени. Мы просто воспользуемся уже готовыми методами добавления привязанных объектов и их обработки.

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

//+------------------------------------------------------------------+
//| Список типов графических элементов                               |
//+------------------------------------------------------------------+
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_SPLIT_CONTAINER,             // Windows Forms SplitContainer
   //--- Ниже нужно вписывать типы объектов "стандартный элемент управления"
   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
   GRAPH_ELEMENT_TYPE_WF_TOOLTIP,                     // Windows Forms ToolTip
   GRAPH_ELEMENT_TYPE_WF_PROGRESS_BAR,                // Windows Forms ProgressBar
   //--- Вспомогательные элементы WinForms-объектов
   GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM,               // Windows Forms ListBoxItem
   GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,                  // Windows Forms TabHeader
   GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,                   // Windows Forms TabField
   GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL,       // Windows Forms SplitContainerPanel
   GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON,                // Windows Forms ArrowButton
   GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP,             // Windows Forms UpArrowButton
   GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN,           // Windows Forms DownArrowButton
   GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT,           // Windows Forms LeftArrowButton
   GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT,          // Windows Forms RightArrowButton
   GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX,        // Windows Forms UpDownArrowButtonsBox
   GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX,        // Windows Forms LeftRightArrowButtonsBox
   GRAPH_ELEMENT_TYPE_WF_SPLITTER,                    // Windows Forms Splitter
   GRAPH_ELEMENT_TYPE_WF_HINT_BASE,                   // Windows Forms HintBase
   GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT,              // Windows Forms HintMoveLeft
   GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT,             // Windows Forms HintMoveRight
   GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP,                // Windows Forms HintMoveUp
   GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN,              // Windows Forms HintMoveDown
   GRAPH_ELEMENT_TYPE_WF_BAR_PROGRESS_BAR,            // Windows Forms BarProgressBar
   GRAPH_ELEMENT_TYPE_WF_GLARE_OBJ,                   // Объект блика
  };
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Список стилей визуальных эффектов                                |
//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_VISUAL_EFF_STYLE
  {
   CANV_ELEMENT_VISUAL_EFF_STYLE_RECTANGLE,           // Прямоугольник
   CANV_ELEMENT_VISUAL_EFF_STYLE_PARALLELOGRAMM,      // Параллелограмм
  };
//+------------------------------------------------------------------+

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

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

//--- CForm
   MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT,// Отсутствует объект тени. Необходимо сначала его создать при помощи метода CreateShadowObj()
   MSG_FORM_OBJECT_TEXT_NO_GLARE_OBJ_FIRST_CREATE_IT, // Отсутствует объект блика. Необходимо сначала его создать при помощи метода CreateGlareObj()
   MSG_FORM_OBJECT_ERR_FAILED_CREATE_SHADOW_OBJ,      // Не удалось создать новый объект для тени
   MSG_FORM_OBJECT_ERR_FAILED_CREATE_GLARE_OBJ,       // Не удалось создать новый объект для блика
   MSG_FORM_OBJECT_ERR_FAILED_CREATE_PC_OBJ,          // Не удалось создать новый объект-копировщик пикселей
   MSG_FORM_OBJECT_PC_OBJ_ALREADY_IN_LIST,            // В списке уже есть объект-копировщик пикселей с идентификатором 

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

//--- CForm
   {"Отсутствует объект тени. Необходимо сначала его создать при помощи метода CreateShadowObj()","There is no shadow object. You must first create it using the CreateShadowObj() method"},
   {"Отсутствует объект блика. Необходимо сначала его создать при помощи метода CreateGlareObj()","There is no glare object. You must first create it using the CreateGlareObj() method"},
   {"Не удалось создать новый объект для тени","Failed to create new object for shadow"},
   {"Не удалось создать новый объект для блика","Failed to create new object for glare"},
   {"Не удалось создать новый объект-копировщик пикселей","Failed to create new pixel copier object"},
   {"В списке уже есть объект-копировщик пикселей с идентификатором ","There is already a pixel copier object in the list with ID "},

Индекс сообщения "Объект блика" перенесём в группу стандартных WinForms-объектов:

   MSG_GRAPH_ELEMENT_TYPE_ELEMENT,                    // Элемент
   MSG_GRAPH_ELEMENT_TYPE_SHADOW_OBJ,                 // Объект тени
                                                                    
   MSG_GRAPH_ELEMENT_TYPE_FORM,                       // Форма
   MSG_GRAPH_ELEMENT_TYPE_WINDOW,                     // Окно
   MSG_GRAPH_ELEMENT_TYPE_WF_BAR_PROGRESS_BAR,        // Элемент управления BarProgressBar
   MSG_GRAPH_ELEMENT_TYPE_WF_PROGRESS_BAR,            // Элемент управления ProgressBar
   MSG_GRAPH_ELEMENT_TYPE_WF_GLARE_OBJ,               // Объект блика
//---
   MSG_GRAPH_ELEMENT_TYPE_WF_WRONG_TYPE_PASSED,       // Передан не правильный тип элемента управления

И точно так же перенесём текст этого сообщения:

   {"Элемент","Element"},
   {"Объект тени","Shadow object"},
                     
   {"Форма","Form"},
   {"Окно","Window"},
   {"Элемент управления BarProgressBar","Control element \"BarProgressBar\""},
   {"Элемент управления ProgressBar","Control element \"ProgressBar\""},
   {"Объект блика","Glare object"},
   
   {"Передан не правильный тип элемента управления","Wrong control type passed"},


В файле \MQL5\Include\DoEasy\GraphINI.mqh, где прописываются параметры различных цветовых схем, добавим свойство для указания значения непрозрачности для блика. Общее количество свойств увеличим до 12:

//+------------------------------------------------------------------+
//| Список индексов параметров стилей форм                           |
//+------------------------------------------------------------------+
enum ENUM_FORM_STYLE_PARAMS
  {
   //--- CForm
   FORM_STYLE_FRAME_GLARE_OPACITY,              // Непрозрачность блика
   FORM_STYLE_FRAME_SHADOW_OPACITY,             // Непрозрачность тени
   FORM_STYLE_FRAME_SHADOW_BLUR,                // Размытие тени
   FORM_STYLE_DARKENING_COLOR_FOR_SHADOW,       // Затемнённость цвета тени формы
   FORM_STYLE_FRAME_SHADOW_X_SHIFT,             // Смещение тени по оси X
   FORM_STYLE_FRAME_SHADOW_Y_SHIFT,             // Смещение тени по оси Y
   FORM_STYLE_GRADIENT_V,                       // Флаг вертикальной градиентной заливки
   FORM_STYLE_GRADIENT_C,                       // Флаг циклической градиентной заливки
   //--- CPanel
   FORM_STYLE_FRAME_WIDTH_LEFT,                 // Ширина рамки панели слева
   FORM_STYLE_FRAME_WIDTH_RIGHT,                // Ширина рамки панели справа
   FORM_STYLE_FRAME_WIDTH_TOP,                  // Ширина рамки панели сверху
   FORM_STYLE_FRAME_WIDTH_BOTTOM,               // Ширина рамки панели снизу
  };
#define TOTAL_FORM_STYLE_PARAMS        (12)     // Количество параметров стиля формы
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Массив, содержащий параметры стилей форм                         |
//+------------------------------------------------------------------+
int array_form_style[TOTAL_FORM_STYLES][TOTAL_FORM_STYLE_PARAMS]=
  {
//--- Параметры стиля формы "Плоская форма"
   {
      //--- CForm
      40,                                       // Непрозрачность блика
      80,                                       // Непрозрачность тени
      4,                                        // Размытие тени
      80,                                       // Затемнённость цвета тени формы
      2,                                        // Смещение тени по оси X
      2,                                        // Смещение тени по оси Y
      false,                                    // Флаг вертикальной градиентной заливки
      false,                                    // Флаг циклической градиентной заливки
      //--- CPanel
      3,                                        // Ширина рамки панели слева
      3,                                        // Ширина рамки панели справа
      3,                                        // Ширина рамки панели сверху
      3,                                        // Ширина рамки панели снизу
   },
//--- Параметры стиля формы "Рельефная форма"
   {
      //--- CForm
      40,                                       // Непрозрачность блика
      80,                                       // Непрозрачность тени
      4,                                        // Размытие тени
      80,                                       // Затемнённость цвета тени формы
      2,                                        // Смещение тени по оси X
      2,                                        // Смещение тени по оси Y
      true,                                     // Флаг вертикальной градиентной заливки
      false,                                    // Флаг циклической градиентной заливки
      //--- CPanel
      3,                                        // Ширина рамки панели слева
      3,                                        // Ширина рамки панели справа
      3,                                        // Ширина рамки панели сверху
      3,                                        // Ширина рамки панели снизу
   },
  };
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Возвращает описание типа графического элемента                   |
//+------------------------------------------------------------------+
string CGBaseObj::TypeElementDescription(const ENUM_GRAPH_ELEMENT_TYPE type)
  {
   return
     (
      type==GRAPH_ELEMENT_TYPE_STANDARD                  ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_STANDARD)                 :
      type==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED         ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)        :
      type==GRAPH_ELEMENT_TYPE_ELEMENT                   ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_ELEMENT)                  :
      type==GRAPH_ELEMENT_TYPE_SHADOW_OBJ                ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_SHADOW_OBJ)               :
                                                                                                                            
      type==GRAPH_ELEMENT_TYPE_FORM                      ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_FORM)                     :
      type==GRAPH_ELEMENT_TYPE_WINDOW                    ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WINDOW)                   :
      //--- WinForms
      type==GRAPH_ELEMENT_TYPE_WF_UNDERLAY               ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_UNDERLAY)              :
      type==GRAPH_ELEMENT_TYPE_WF_BASE                   ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BASE)                  :
      //--- Контейнеры
      type==GRAPH_ELEMENT_TYPE_WF_CONTAINER              ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CONTAINER)             :
      type==GRAPH_ELEMENT_TYPE_WF_GROUPBOX               ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_GROUPBOX)              :
      type==GRAPH_ELEMENT_TYPE_WF_PANEL                  ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_PANEL)                 :
      type==GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL            ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL)           :
      type==GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER        ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER)       :
      //--- Стандартные элементы управления
      type==GRAPH_ELEMENT_TYPE_WF_COMMON_BASE            ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_COMMON_BASE)           :
      type==GRAPH_ELEMENT_TYPE_WF_LABEL                  ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LABEL)                 :
      type==GRAPH_ELEMENT_TYPE_WF_CHECKBOX               ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CHECKBOX)              :
      type==GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON            ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON)           :
      type==GRAPH_ELEMENT_TYPE_WF_BUTTON                 ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BUTTON)                :
      type==GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX      ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX)     :
      type==GRAPH_ELEMENT_TYPE_WF_LIST_BOX               ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LIST_BOX)              :
      type==GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM          ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM)         :
      type==GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX       ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX)      :
      type==GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX        ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX)       :
      type==GRAPH_ELEMENT_TYPE_WF_TOOLTIP                ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TOOLTIP)               :
      type==GRAPH_ELEMENT_TYPE_WF_PROGRESS_BAR           ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_PROGRESS_BAR)          :
      //--- Вспомогательные объекты элементов управления
      type==GRAPH_ELEMENT_TYPE_WF_TAB_HEADER             ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_HEADER)            :
      type==GRAPH_ELEMENT_TYPE_WF_TAB_FIELD              ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_FIELD)             :
      type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON           ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON)          :
      type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP        ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP)       :
      type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN      ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN)     :
      type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT      ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT)     :
      type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT     ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT)    :
      type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX   ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX)  :
      type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX   ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX)  :
      type==GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL  ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL) :
      type==GRAPH_ELEMENT_TYPE_WF_SPLITTER               ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_SPLITTER)              :
      type==GRAPH_ELEMENT_TYPE_WF_HINT_BASE              ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_HINT_BASE)             :
      type==GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT         ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT)        :
      type==GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT        ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT)       :
      type==GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP           ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP)          :
      type==GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN         ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN)        :
      type==GRAPH_ELEMENT_TYPE_WF_BAR_PROGRESS_BAR       ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BAR_PROGRESS_BAR)      :
      type==GRAPH_ELEMENT_TYPE_WF_GLARE_OBJ              ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_GLARE_OBJ)             :
      "Unknown"
     );
  }  
//+------------------------------------------------------------------+

Технически никакой разницы в очерёдности проверки нет, но для порядка переместим на "правильное" место. К слову — технически, конечно, есть разница в скорости доступа, так как здесь организована проверка if-else и, если какой-то тип проверяется чаще других, но расположен ближе к концу списка проверок, то время доступа увеличится. Но это лишь теоретически в рамках тактов процессора. Впрочем, чтобы был доступ к любому элементу одинаковым, нужно сделать все такие проверки в методах библиотеки посредством оператора switch, что впоследствии будет сделано — в самом конце создания библиотеки.

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

Из файла \MQL5\Include\DoEasy\Objects\Graph\ShadowObj.mqh уберём подключение библиотеки AlgLib, удалим объявление методов для размытия и удалим коды реализации этих двух методов:

//+------------------------------------------------------------------+
//|                                                    ShadowObj.mqh |
//|                                  Copyright 2021, MetaQuotes Ltd. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2021, MetaQuotes Ltd."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "GCnvElement.mqh"
#include <Math\Alglib\alglib.mqh>
//+------------------------------------------------------------------+
//| Класс объекта Тени                                               |
//+------------------------------------------------------------------+
class CShadowObj : public CGCnvElement
  {
private:
   color             m_color;                         // Цвет тени
   uchar             m_opacity;                       // Непрозрачность тени
   uchar             m_blur;                          // Размытие
//--- Рисует форму тени объекта
   void              DrawShadowFigureRect(const int w,const int h);
protected:
//--- Размытие по-Гауссу
   bool              GaussianBlur(const uint radius);
//--- Возвращает массив весовых коэффициентов
   bool              GetQuadratureWeights(const double mu0,const int n,double &weights[]);
//--- Защищённый конструктор с указанием типа объекта, идентификатора чарта и подокна
                     CShadowObj(const ENUM_GRAPH_ELEMENT_TYPE type,
                                CGCnvElement *main_obj,CGCnvElement *base_obj,
                                const long chart_id,
                                const int subwindow,
                                const string descript,
                                const int x,
                                const int y,
                                const int w,
                                const int h);
public:


В файле \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh пропишем подключение библиотеки AlgLib и в защищённой секции объявим методы для реализации размытия:

//+------------------------------------------------------------------+
//|                                                  GCnvElement.mqh |
//|                                  Copyright 2021, MetaQuotes Ltd. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2021, MetaQuotes Ltd."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "GBaseObj.mqh"
#include <Math\Alglib\alglib.mqh>
//+------------------------------------------------------------------+
//| Класс объекта графического элемента                              |
//+------------------------------------------------------------------+
class CGCnvElement : public CGBaseObj
  {
protected:
   CGCnvElement     *m_element_main;                           // Указатель на первоначальный родительский элемент в составе всех групп связанных объектов
   CGCnvElement     *m_element_base;                           // Указатель на родительский элемент в составе связанных объектов текущей группы
   CCanvas           m_canvas;                                 // Объект класса CCanvas
   CPause            m_pause;                                  // Объект класса "Пауза"
   bool              m_shadow;                                 // Наличие тени
   color             m_chart_color_bg;                         // Цвет фона графика
   uint              m_duplicate_res[];                        // Массив для хранения копии данных ресурса
   color             m_array_colors_bg[];                      // Массив цветов фона элемента
   color             m_array_colors_bg_dwn[];                  // Массив цветов фона элемента при нажатии мышки на элемент управления
   color             m_array_colors_bg_ovr[];                  // Массив цветов фона элемента при наведении мышки на элемент управления
   bool              m_gradient_v;                             // Флаг вертикальной градиентной заливки
   bool              m_gradient_c;                             // Флаг циклической градиентной заливки
   int               m_init_relative_x;                        // Первоначальная относительная координата X
   int               m_init_relative_y;                        // Первоначальная относительная координата Y
   color             m_array_colors_bg_init[];                 // Массив цветов фона элемента (первоначальный цвет)

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

private:


За пределами тела класса добавим реализацию методов размытия, удалённую из класса CShadowObj:

//+------------------------------------------------------------------+
//| Размытие по-Гауссу                                               |
//| https://www.mql5.com/ru/articles/1612#chapter4                   |
//+------------------------------------------------------------------+
bool CGCnvElement::GaussianBlur(const uint radius)
  {
//---
   int n_nodes=(int)radius*2+1;
//--- Читаем данные графического ресурса, и если не получилось - возвращаем false
   if(!CGCnvElement::ResourceStamp(DFUN))
      return false;
//--- Проверяем величину размытия, и если радиус размытия больше половины высоты или ширины - возвращаем false
   if((int)radius>=this.Width()/2 || (int)radius>=this.Height()/2)
     {
      ::Print(DFUN,CMessage::Text(MSG_SHADOW_OBJ_IMG_SMALL_BLUR_LARGE));
      return false;
     }
     
//--- Раскладываем данные изображения из ресурса на компоненты цвета a, r, g, b
   int  size=::ArraySize(this.m_duplicate_res);
//--- массивы для хранения компонент цвета A, R, G и B
//--- для горизонтального и вертикального размытия
   uchar a_h_data[],r_h_data[],g_h_data[],b_h_data[];
   uchar a_v_data[],r_v_data[],g_v_data[],b_v_data[];
   
//--- Изменяем размеры массивов компонент под размер массива данных графического ресурса
   if(::ArrayResize(a_h_data,size)==-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"a_h_data\"");
      return false;
     }
   if(::ArrayResize(r_h_data,size)==-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"r_h_data\"");
      return false;
     }
   if(::ArrayResize(g_h_data,size)==-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"g_h_data\"");
      return false;
     }
   if(ArrayResize(b_h_data,size)==-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"b_h_data\"");
      return false;
     }
   if(::ArrayResize(a_v_data,size)==-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"a_v_data\"");
      return false;
     }
   if(::ArrayResize(r_v_data,size)==-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"r_v_data\"");
      return false;
     }
   if(::ArrayResize(g_v_data,size)==-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"g_v_data\"");
      return false;
     }
   if(::ArrayResize(b_v_data,size)==-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"b_v_data\"");
      return false;
     }
//--- Объявляем массив для хранения весовых коэффициентов размытия и
//--- если не удалось получить массив весовых коэффициентов - возвращаем false
   double weights[];
   if(!this.GetQuadratureWeights(1,n_nodes,weights))
      return false;
      
//--- В массивы компонент цвета записываем компоненты каждого пикселя изображения
   for(int i=0;i<size;i++)
     {
      a_h_data[i]=GETRGBA(this.m_duplicate_res[i]);
      r_h_data[i]=GETRGBR(this.m_duplicate_res[i]);
      g_h_data[i]=GETRGBG(this.m_duplicate_res[i]);
      b_h_data[i]=GETRGBB(this.m_duplicate_res[i]);
     }

//--- Размываем изображение горизонтально (по оси X)
   uint XY; // Координата пикселя в массиве
   double a_temp=0.0,r_temp=0.0,g_temp=0.0,b_temp=0.0;
   int coef=0;
   int j=(int)radius;
   //--- Цикл по ширине изображения
   for(int Y=0;Y<this.Height();Y++)
     {
      //--- Цикл по высоте изображения
      for(uint X=radius;X<this.Width()-radius;X++)
        {
         XY=Y*this.Width()+X;
         a_temp=0.0; r_temp=0.0; g_temp=0.0; b_temp=0.0;
         coef=0;
         //--- Каждую компоненту цвета умножаем на весовой коэффициент, соответствующий текущему пикселю изображения
         for(int i=-1*j;i<j+1;i=i+1)
           {
            a_temp+=a_h_data[XY+i]*weights[coef];
            r_temp+=r_h_data[XY+i]*weights[coef];
            g_temp+=g_h_data[XY+i]*weights[coef];
            b_temp+=b_h_data[XY+i]*weights[coef];
            coef++;
           }
         //--- Сохраняем в массивы компонент округлённые, рассчитанные по коэффициентам, каждую компоненту цвета
         a_h_data[XY]=(uchar)::round(a_temp);
         r_h_data[XY]=(uchar)::round(r_temp);
         g_h_data[XY]=(uchar)::round(g_temp);
         b_h_data[XY]=(uchar)::round(b_temp);
        }
      //--- Удаляем артефакты размытия слева методом копирования соседних пикселей
      for(uint x=0;x<radius;x++)
        {
         XY=Y*this.Width()+x;
         a_h_data[XY]=a_h_data[Y*this.Width()+radius];
         r_h_data[XY]=r_h_data[Y*this.Width()+radius];
         g_h_data[XY]=g_h_data[Y*this.Width()+radius];
         b_h_data[XY]=b_h_data[Y*this.Width()+radius];
        }
      //--- Удаляем артефакты размытия справа методом копирования соседних пикселей
      for(int x=int(this.Width()-radius);x<this.Width();x++)
        {
         XY=Y*this.Width()+x;
         a_h_data[XY]=a_h_data[(Y+1)*this.Width()-radius-1];
         r_h_data[XY]=r_h_data[(Y+1)*this.Width()-radius-1];
         g_h_data[XY]=g_h_data[(Y+1)*this.Width()-radius-1];
         b_h_data[XY]=b_h_data[(Y+1)*this.Width()-radius-1];
        }
     }

//--- Размываем вертикально (по оси Y) уже размытое горизонтально изображение
   int dxdy=0;
   //--- Цикл по высоте изображения
   for(int X=0;X<this.Width();X++)
     {
      //--- Цикл по ширине изображения
      for(uint Y=radius;Y<this.Height()-radius;Y++)
        {
         XY=Y*this.Width()+X;
         a_temp=0.0; r_temp=0.0; g_temp=0.0; b_temp=0.0;
         coef=0;
         //--- Каждую компоненту цвета умножаем на весовой коэффициент, соответствующий текущему пикселю изображения
         for(int i=-1*j;i<j+1;i=i+1)
           {
            dxdy=i*(int)this.Width();
            a_temp+=a_h_data[XY+dxdy]*weights[coef];
            r_temp+=r_h_data[XY+dxdy]*weights[coef];
            g_temp+=g_h_data[XY+dxdy]*weights[coef];
            b_temp+=b_h_data[XY+dxdy]*weights[coef];
            coef++;
           }
         //--- Сохраняем в массивы компонент округлённые, рассчитанные по коэффициентам, каждую компоненту цвета
         a_v_data[XY]=(uchar)::round(a_temp);
         r_v_data[XY]=(uchar)::round(r_temp);
         g_v_data[XY]=(uchar)::round(g_temp);
         b_v_data[XY]=(uchar)::round(b_temp);
        }
      //--- Удаляем артефакты размытия сверху методом копирования соседних пикселей
      for(uint y=0;y<radius;y++)
        {
         XY=y*this.Width()+X;
         a_v_data[XY]=a_v_data[X+radius*this.Width()];
         r_v_data[XY]=r_v_data[X+radius*this.Width()];
         g_v_data[XY]=g_v_data[X+radius*this.Width()];
         b_v_data[XY]=b_v_data[X+radius*this.Width()];
        }
      //--- Удаляем артефакты размытия снизу методом копирования соседних пикселей
      for(int y=int(this.Height()-radius);y<this.Height();y++)
        {
         XY=y*this.Width()+X;
         a_v_data[XY]=a_v_data[X+(this.Height()-1-radius)*this.Width()];
         r_v_data[XY]=r_v_data[X+(this.Height()-1-radius)*this.Width()];
         g_v_data[XY]=g_v_data[X+(this.Height()-1-radius)*this.Width()];
         b_v_data[XY]=b_v_data[X+(this.Height()-1-radius)*this.Width()];
        }
     }
     
//--- Записываем в массив данных графического ресурса дважды размытые (горизонтально и вертикально) пиксели изображения
   for(int i=0;i<size;i++)
      this.m_duplicate_res[i]=ARGB(a_v_data[i],r_v_data[i],g_v_data[i],b_v_data[i]);
//--- Выводим пиксели изображения на канвас в цикле по высоте и ширине изображения из массива данных графического ресурса
   for(int X=0;X<this.Width();X++)
     {
      for(uint Y=radius;Y<this.Height()-radius;Y++)
        {
         XY=Y*this.Width()+X;
         this.m_canvas.PixelSet(X,Y,this.m_duplicate_res[XY]);
        }
     }
//--- Готово
   return true;
  }
//+------------------------------------------------------------------+
//| Возвращает массив весовых коэффициентов                          |
//| https://www.mql5.com/ru/articles/1612#chapter3_2                 |
//+------------------------------------------------------------------+
bool CGCnvElement::GetQuadratureWeights(const double mu0,const int n,double &weights[])
  {
   CAlglib alglib;
   double  alp[];
   double  bet[];
   ::ArrayResize(alp,n);
   ::ArrayResize(bet,n);
   ::ArrayInitialize(alp,1.0);
   ::ArrayInitialize(bet,1.0);
//---
   double out_x[];
   int    info=0;
   alglib.GQGenerateRec(alp,bet,mu0,n,info,out_x,weights);
   if(info!=1)
     {
      string txt=(info==-3 ? "internal eigenproblem solver hasn't converged" : info==-2 ? "Beta[i]<=0" : "incorrect N was passed");
      ::Print("Call error in CGaussQ::GQGenerateRec: ",txt);
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

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


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

Рассмотрим доработанные методы. В случае, если значение непрозрачности рисуемого графического примитива превышает значение 127, то примитив рисуется методом со сглаживанием. Иначе — рисуем методом без сглаживания:

//+------------------------------------------------------------------+
//| Рисует иконку Info                                               |
//+------------------------------------------------------------------+
void CGCnvElement::DrawIconInfo(const int coord_x,const int coord_y,const uchar opacity)
  {
   int x=coord_x+8;
   int y=coord_y+8;
   this.DrawCircleFill(x,y,7,C'0x00,0x77,0xD7',opacity);
   if(opacity>127)
      this.DrawCircleWu(x,y,7.5,C'0x00,0x3D,0x8C',opacity);
   else
      this.DrawCircle(x,y,8,C'0x00,0x3D,0x8C',opacity);
   this.DrawRectangle(x,y-5,x+1,y-4, C'0xFF,0xFF,0xFF',opacity);
   this.DrawRectangle(x,y-2,x+1,y+4,C'0xFF,0xFF,0xFF',opacity);
  }
//+------------------------------------------------------------------+
//| Рисует иконку Warning                                            |
//+------------------------------------------------------------------+
void CGCnvElement::DrawIconWarning(const int coord_x,const int coord_y,const uchar opacity)
  {
   int x=coord_x+8;
   int y=coord_y+1;
   this.DrawTriangleFill(x,y,x+8,y+14,x-8,y+14,C'0xFC,0xE1,0x00',opacity);
   if(opacity>127)
      this.DrawTriangleWu(x,y,x+8,y+14,x-7,y+14,C'0xFF,0xB9,0x00',opacity);
   else
      this.DrawTriangle(x,y,x+8,y+14,x-7,y+14,C'0xFF,0xB9,0x00',opacity);
   this.DrawRectangle(x,y+5,x+1,y+9,  C'0x00,0x00,0x00',opacity);
   this.DrawRectangle(x,y+11,x+1,y+12,C'0x00,0x00,0x00',opacity);
  }
//+------------------------------------------------------------------+
//| Рисует иконку Error                                              |
//+------------------------------------------------------------------+
void CGCnvElement::DrawIconError(const int coord_x,const int coord_y,const uchar opacity)
  {
   int x=coord_x+8;
   int y=coord_y+8;
   this.DrawCircleFill(x,y,7,C'0xF0,0x39,0x16',opacity);
   if(opacity>127)
     {
      this.DrawCircleWu(x,y,7.5,C'0xA5,0x25,0x12',opacity);
      this.DrawLineWu(x-3,y-3,x+3,y+3,C'0xFF,0xFF,0xFF',opacity);
      this.DrawLineWu(x+3,y-3,x-3,y+3,C'0xFF,0xFF,0xFF',opacity);
     }
   else
     {
      this.DrawCircle(x,y,8,C'0xA5,0x25,0x12',opacity);
      this.DrawLine(x-3,y-3,x+3,y+3,C'0xFF,0xFF,0xFF',opacity);
      this.DrawLine(x+3,y-3,x-3,y+3,C'0xFF,0xFF,0xFF',opacity);
     }
  }
//+------------------------------------------------------------------+

Конечно же, это всего лишь скрытие проблемы, но здесь вполне себя оправдывает: при уменьшении непрозрачности от 255 до 128, сглаженная линия более-менее нормально выглядит, далее она начинает разрываться и рассыпаться на пиксели вместо того, чтобы становиться прозрачнее. Замена отрисовки линии на рисование без сглаживания здесь совершенно не заметна — линия уже полупрозрачная и в таком состоянии не заметна её ступенчатость, которая хорошо видна, если рисовать несглаженную линию полностью непрозрачной.

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


Так как теперь объект-блик не является отдельным обособленным типом (как объект-тень), а принадлежит к списку вспомогательных WinForms-объектов, то подключение файла класса этого объекта теперь нужно сделать в файле класса объекта-панели. Этот объект является объектом-контейнером, к которому подключены файлы всех WinForms-объектов библиотеки.
Из файла \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh удалим строку подключения класса объекта-блика:

//+------------------------------------------------------------------+
//|                                                  WinFormBase.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 "GlareObj.mqh"
#include "..\Form.mqh"
#include "..\..\..\Services\Select.mqh"
//+------------------------------------------------------------------+

и пропишем подключение файла класса объекта-блика в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Panel.mqh класса объекта-панели:

//+------------------------------------------------------------------+
//|                                                        Panel.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "Container.mqh"
#include "..\Helpers\TabField.mqh"
#include "..\Helpers\ArrowUpButton.mqh"
#include "..\Helpers\ArrowDownButton.mqh"
#include "..\Helpers\ArrowLeftButton.mqh"
#include "..\Helpers\ArrowRightButton.mqh"
#include "..\Helpers\ArrowUpDownBox.mqh"
#include "..\Helpers\ArrowLeftRightBox.mqh"
#include "..\Helpers\HintMoveLeft.mqh"
#include "..\Helpers\HintMoveRight.mqh"
#include "..\Helpers\HintMoveUp.mqh"
#include "..\Helpers\HintMoveDown.mqh"
#include "GroupBox.mqh"
#include "TabControl.mqh"
#include "SplitContainer.mqh"
#include "..\..\WForms\Common Controls\ListBox.mqh"
#include "..\..\WForms\Common Controls\CheckedListBox.mqh"
#include "..\..\WForms\Common Controls\ButtonListBox.mqh"
#include "..\..\WForms\Common Controls\ToolTip.mqh"
#include "..\..\WForms\Common Controls\ProgressBar.mqh"
#include "..\..\WForms\GlareObj.mqh"
//+------------------------------------------------------------------+
//| Класс объекта Panel элементов управления WForms                  |
//+------------------------------------------------------------------+
class CPanel : public CContainer

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


В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Container.mqh класса базового объекта-контейнера, в его методе, устанавливающем параметры присоединённому объекту, добавим в конце блок кода для установки параметров вновь созданного объекта-блика:

//+------------------------------------------------------------------+
//| Устанавливает параметры присоединённому объекту                  |
//+------------------------------------------------------------------+
void CContainer::SetObjParams(CWinFormBase *obj,const color colour)
  {
//--- Устанавливаем объекту цвет текста как у базового контейнера
   obj.SetForeColor(this.ForeColor(),true);
//--- Если созданный объект не является контейнером - устанавливаем для него такую же группу, как у его базового объекта
   if(obj.TypeGraphElement()<GRAPH_ELEMENT_TYPE_WF_CONTAINER || obj.TypeGraphElement()>GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER)
      obj.SetGroup(this.Group());
//--- В зависимости от типа объекта
   switch(obj.TypeGraphElement())
     {
      //--- Для WinForms-объектов "Контейнер", "Панель", "GroupBox"
      case GRAPH_ELEMENT_TYPE_WF_CONTAINER            :
      case GRAPH_ELEMENT_TYPE_WF_PANEL                :
      case GRAPH_ELEMENT_TYPE_WF_GROUPBOX             :
        obj.SetBorderColor(obj.BackgroundColor(),true);
        break;
      //--- Для WinForms-объектов "Label", "CheckBox", "RadioButton"

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

      //--- Для WinForms-объекта "ProgressBar"
      case GRAPH_ELEMENT_TYPE_WF_PROGRESS_BAR         :
        obj.SetBackgroundColor(CLR_DEF_CONTROL_PROGRESS_BAR_BACK_COLOR,true);
        obj.SetBorderColor(CLR_DEF_CONTROL_PROGRESS_BAR_BORDER_COLOR,true);
        obj.SetForeColor(CLR_DEF_CONTROL_PROGRESS_BAR_FORE_COLOR,true);
        obj.SetBorderStyle(FRAME_STYLE_SIMPLE);
        break;
      //--- Для WinForms-объекта "GlareObj"
      case GRAPH_ELEMENT_TYPE_WF_GLARE_OBJ            :
        obj.SetBackgroundColor(CLR_CANV_NULL,true);
        obj.SetBorderColor(CLR_CANV_NULL,true);
        obj.SetForeColor(CLR_CANV_NULL,true);
        obj.SetBorderStyle(FRAME_STYLE_NONE);
        break;
      default:
        break;
     }
   obj.Crop();
  }
//+------------------------------------------------------------------+

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


Доработаем класс объекта-блика в файле \MQL5\Include\DoEasy\Objects\Graph\WForms\GlareObj.mqh.

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

//+------------------------------------------------------------------+
//|                                                     GlareObj.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 "WinFormBase.mqh"
//+------------------------------------------------------------------+
//| Класс объекта блика                                              |
//+------------------------------------------------------------------+
class CGlareObj : public CWinFormBase
  {
   //---...


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

class CGlareObj : public CWinFormBase
  {
private:
   ENUM_CANV_ELEMENT_VISUAL_EFF_STYLE m_style;        // Стиль блика
   color             m_color;                         // Цвет блика
   uchar             m_opacity;                       // Непрозрачность блика
   uchar             m_blur;                          // Размытие
//--- Рисует форму блика объекта
   void              DrawGlareFigure(void);
   void              DrawFigureRectangle(void);
   void              DrawFigureParallelogram(void);
protected:

Стиль блика будет определять какой метод вызывать для его рисования. При этом метод DrawGlareFigure() будет вызывать соответствующие установленному стилю методы рисования. Формальных параметров в методах рисования блика не будет — пока нечего указывать для правильной отрисовки изображения на графическом элементе с предопределённым размером. Цвет и непрозрачность рисуемого блика будут устанавливаться предварительно соответствующими методами. Размытие пока использоваться не будет.

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

public:
//--- Конструктор с указанием главного и базового объектов, идентификатора чарта и подокна
                     CGlareObj(CGCnvElement *main_obj,CGCnvElement *base_obj,
                               const long chart_id,
                               const int subwindow,
                               const string descript,
                               const int x,
                               const int y,
                               const int w,
                               const int h);

//--- Поддерживаемые свойства объекта (1) целочисленные, (2) вещественные, (3) строковые
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER property) { return true; }
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_DOUBLE property)  { return true; }
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING property)  { return true; }
   
//--- Рисует блик объекта
   void              Draw(void);
//--- Перерисовывает объект
   virtual void      Redraw(bool redraw);
//--- Устанавливает непрозрачность элемента
   virtual void      SetOpacity(const uchar value,const bool redraw=false);
//--- Инициализирует свойства
   void              Initialize(void);

//+------------------------------------------------------------------+
//| Методы упрощённого доступа к свойствам объекта                   |
//+------------------------------------------------------------------+
//--- (1) Устанавливает, (2) возвращает цвет блика
   void              SetColor(const color colour)                             { this.m_color=colour;     }
   color             Color(void)                                        const { return this.m_color;     }
//--- (1) Устанавливает, (2) возвращает непрозрачность рисуемого блика
   void              SetOpacityDraw(const uchar opacity)                      { this.m_opacity=opacity;  }
   uchar             OpacityDraw(void)                                  const { return this.m_opacity;   }
//--- (1) Устанавливает, (2) возвращает размытие блика
   void              SetBlur(const uchar blur)                                { this.m_blur=blur;        }
   uchar             Blur(void)                                         const { return this.m_blur;      }
//--- (1) Устанавливает, (2) возвращает стиль визуального эффекта блика
   void              SetVisualEffectStyle(const ENUM_CANV_ELEMENT_VISUAL_EFF_STYLE style)
                       { this.m_style=style;                                                             }
   ENUM_CANV_ELEMENT_VISUAL_EFF_STYLE VisualEffectStyle(void)           const { return this.m_style;     }
//--- Устанавливает стиль визуального эффекта блика как (1) прямоугольник, (2) параллелограмм
   void              SetStyleRectangle(void)
                       { this.SetVisualEffectStyle(CANV_ELEMENT_VISUAL_EFF_STYLE_RECTANGLE);             }
   void              SetStyleParallelogramm(void)
                       { this.SetVisualEffectStyle(CANV_ELEMENT_VISUAL_EFF_STYLE_PARALLELOGRAMM);        }
  };
//+------------------------------------------------------------------+

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

В конструкторах классов заменим инициализацию родительского класса CShadowObj на CWinFormBase и добавим вызов метода инициализации переменных:

//+------------------------------------------------------------------+
//| Защищённый конструктор с указанием типа объекта,                 |
//| идентификатора чарта и подокна                                   |
//+------------------------------------------------------------------+
CGlareObj::CGlareObj(const ENUM_GRAPH_ELEMENT_TYPE type,
                     CGCnvElement *main_obj,CGCnvElement *base_obj,
                     const long chart_id,
                     const int subwindow,
                     const string descript,
                     const int x,
                     const int y,
                     const int w,
                     const int h) : CWinFormBase(type,main_obj,base_obj,chart_id,subwindow,descript,x-OUTER_AREA_SIZE,y-OUTER_AREA_SIZE,w+OUTER_AREA_SIZE*2,h+OUTER_AREA_SIZE*2)
  {
//--- Установим объекту указанный тип графического элемента, а тип объекта библиотеки - как тип этого объекта
   this.SetTypeElement(type);
   this.m_type=OBJECT_DE_TYPE_GGLARE;
   this.Initialize();
  }
//+------------------------------------------------------------------+
//| Конструктор с указанием главного и базового объектов,            |
//| идентификатора чарта и подокна                                   |
//+------------------------------------------------------------------+
CGlareObj::CGlareObj(CGCnvElement *main_obj,CGCnvElement *base_obj,
                     const long chart_id,
                     const int subwindow,
                     const string descript,
                     const int x,
                     const int y,
                     const int w,
                     const int h) : CWinFormBase(GRAPH_ELEMENT_TYPE_WF_GLARE_OBJ,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
   this.m_type=OBJECT_DE_TYPE_GGLARE; 
   this.Initialize();
  }
//+------------------------------------------------------------------+

Теперь, вместо прописывания в каждом конструкторе строк инициализации каждой переменной (одинаковые для обоих конструкторов) мы просто вызываем метод инициализации свойств объекта:

//+------------------------------------------------------------------+
//| Инициализирует свойства                                          |
//+------------------------------------------------------------------+
void CGlareObj::Initialize(void)
  {
   CGCnvElement::SetBackgroundColor(CLR_CANV_NULL,true);
   CGCnvElement::SetActive(false);
   this.SetBlur(4);
   this.SetColor(clrWhite);
   this.SetOpacity(127);
   this.m_shadow=false;
   this.SetVisibleFlag(false,false);
   this.SetVisualEffectStyle(CANV_ELEMENT_VISUAL_EFF_STYLE_RECTANGLE);
   this.SetDisplayState(CANV_ELEMENT_DISPLAY_STATE_NORMAL);
   this.SetDisplayed(false);
  }
//+------------------------------------------------------------------+

Метод устанавливает значения по умолчанию для свойств объекта. Размытие устанавливаем равным 4, цвет блика — белый. Непрозрачность устанавливаем равной 127 — полупрозрачный объект, тень отсутствует (что естественно). Устанавливаем флаг невидимости объекта, а тип визуального эффекта — прямоугольный блик. Состояние отображения — нормальное (объект скрыт), а флаг отображения сброшен — объект не должен показываться на отображаемом родительском объекте.


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

//+------------------------------------------------------------------+
//| Устанавливает непрозрачность элемента                            |
//+------------------------------------------------------------------+
void CGlareObj::SetOpacity(const uchar value,const bool redraw=false)
  {
   CGCnvElement::SetOpacity(0,false);
   this.SetOpacityDraw(value>(uchar)CLR_DEF_SHADOW_OPACITY ? (uchar)CLR_DEF_SHADOW_OPACITY : value);
   this.m_canvas.Update(redraw);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Рисует блик объекта                                              |
//+------------------------------------------------------------------+
void CGlareObj::Draw(void)
  {
   if(!this.IsVisible())
      return;
//--- Рисуем блик
   this.DrawGlareFigure();
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Рисует форму блика объекта                                       |
//+------------------------------------------------------------------+
void CGlareObj::DrawGlareFigure()
  {
   switch(this.VisualEffectStyle())
     {
      case CANV_ELEMENT_VISUAL_EFF_STYLE_RECTANGLE       : this.DrawFigureRectangle();       break;
      case CANV_ELEMENT_VISUAL_EFF_STYLE_PARALLELOGRAMM  : this.DrawFigureParallelogram();   break;
      default:
        break;
     }
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Рисует прямоугольную форму блика объекта                         |
//+------------------------------------------------------------------+
void CGlareObj::DrawFigureRectangle(void)
  {
   CGCnvElement::DrawRectangleFill(0,0,this.Width()-1,this.Height()-1,this.m_color,this.OpacityDraw());
   CGCnvElement::Update();
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Рисует форму блика объекта в виде параллелограмма                |
//+------------------------------------------------------------------+
void CGlareObj::DrawFigureParallelogram(void)
  {
   int array_x[]={6,this.Width()-1,this.Width()-1-6,0};
   int array_y[]={0,0,this.Height()-1,this.Height()-1};
   CGCnvElement::DrawPolygonFill(array_x,array_y,this.m_color,this.OpacityDraw());
   CGCnvElement::Update();
  }
//+------------------------------------------------------------------+

Здесь рисуется "скошенный" прямоугольник. Сначала объявляем массивы координат X и Y вершин полигона, а далее по указанным в массивах вершинам рисуем заполненный полигон в виде скошенного прямоугольника — параллелограмма с установленным для объекта цветом и непрозрачностью рисования.


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

Метод, перерисовывающий объект:

//+------------------------------------------------------------------+
//| Перерисовывает объект                                            |
//+------------------------------------------------------------------+
void CGlareObj::Redraw(bool redraw)
  {
   CGCnvElement::Erase(false);
   this.Draw();
   this.Crop();
   this.Update(redraw);
  }
//+------------------------------------------------------------------+

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

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


Разработаем функционал объекта ProgressBar для управления его полосой прогресса.

Чтобы отображать ход некоего процесса нам нужно увеличивать ширину объекта BarProgressBar на величину шага увеличения полосы прогресса. Вообще, все значения ширины полосы прогресса будут относительными — в процентах от ширины объекта ProgressBar и в процентах же от максимального значения параметра Value полосы прогресса. Индикатор выполнения может изменять свои значения в пределах, установленных в параметрах Minimum и Maximum. Но эти значения должны задаваться не в пикселях, а в процентах от ширины объекта. Тогда значение Value, равное 100 будет соответствовать ширине 100 пикселей при таком же значении ширины объекта ProgressBar, но будет иметь значение 200 при ширине, равной 200 пикселей объекта ProgressBar. При этом, если значение Value равно 50, то это будет соответствовать значениям 50 и 100 пикселей при указанных выше значениях ширины объекта.

Для того, чтобы мы могли просто управлять значениями линии прогресса, сделаем так, что перед использованием объекта — перед началом цикла, процесс которого необходимо отображать, мы зададим минимальное и максимальное значения пределов и установим шаг, на который нужно увеличивать значение ширины полосы прогресса. А далее, после завершения очередного действия в отслеживаемом цикле, нам нужно просто вызвать метод PerformStep() объекта ProgressBar, и ширина полосы прогресса будет увеличена в соответствии с заданным шагом её увеличения. Таким образом мы должны всегда действовать так: зная количество итераций отслеживаемого цикла, задаём нужные значения объекту ProgressBar, а внутри цикла при завершении очередной итерации вызываем метод PerformStep(), который вызовет изменение полосы прогресса на один шаг.

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

//+------------------------------------------------------------------+
//| Класс объекта ArrowLeftRightBox элементов управления WForms      |
//+------------------------------------------------------------------+
class CProgressBar : public CContainer
  {
private:
   int               m_progress_bar_max;  // Максимальная ширина полосы прогресса
   int               m_value_by_max;      // Значение Value относительно Maximum
   int               m_steps_skipped;     // Количество пропускаемых шагов увеличения ширины полосы прогресса
//--- Создаёт новый графический объект
   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              CreateProgressBar(void);
//--- Рассчитывает ширину полосы прогресса
   int               CalculateProgressBarWidth(void);
//--- Инициализирует свойства элемента
   void              Initialize(void);

protected:


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

//--- (1) Устанавливает, (2) возвращает величину приращения значения полосы прогресса для его перерисовки
   void              SetStep(const int value)
                       {
                        this.SetProperty(CANV_ELEMENT_PROP_PROGRESS_BAR_STEP,value);
                        CBarProgressBar *bar=this.GetProgressBar();
                        if(bar!=NULL)
                           bar.SetStep(value);
                       }
   int               Step(void)                          const { return (int)this.GetProperty(CANV_ELEMENT_PROP_PROGRESS_BAR_STEP);    }
   
//--- (1) Устанавливает, (2) возвращает текущее значение полосы прогресса в диапазоне от Min до Max
   void              SetValue(const int value);
   int               Value(void)                         const { return (int)this.GetProperty(CANV_ELEMENT_PROP_PROGRESS_BAR_VALUE);   }
   
//--- (1) Устанавливает, (2) возвращает верхнюю границу диапазона, в котором действует ProgressBar
   void              SetMaximum(const int value)               { this.SetProperty(CANV_ELEMENT_PROP_PROGRESS_BAR_MAXIMUM,value);       }
   int               Maximum(void)                       const { return (int)this.GetProperty(CANV_ELEMENT_PROP_PROGRESS_BAR_MAXIMUM); }


И объявим ещё несколько методов:

//--- Возвращает указатель на объект-полосу прогресса
   CBarProgressBar  *GetProgressBar(void)                { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_BAR_PROGRESS_BAR,0);     }

//--- Сбрасывает значения полосы прогресса к установленному минимальному
   void              ResetProgressBar(void)              { this.SetValue(this.Minimum());    }
//--- Устанавливает ширину элемента
   virtual bool      SetWidth(const int width);
//--- Инициализирует значения для обработки в PerformStep
   void              SetValuesForProcessing(const int minimum,const int maximum,const int step,const int steps_skipped);
//--- Увеличивает текущую позицию полосы прогресса на значение шага
   void              PerformStep();

//--- Поддерживаемые свойства объекта (1) целочисленные, (2) вещественные, (3) строковые
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER property) { return true; }
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_DOUBLE property)  { return true; }
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING property)  { return true; }

Метод ResetProgressBar() реализован прямо в теле класса и устанавливает значение Value полосы прогресса в значение, записанное в свойстве Minimum. Что позволяет вернуть полосу прогресса к исходному состоянию.

В методе инициализации значений по умолчанию установим значение Value полосы прогресса в 50% от ширины объекта, рассчитаем максимальную ширину в пикселях для объекта BarProgressBar, который является полосой прогресса, и рассчитаем значение Value в процентах от ширины объекта:

//+------------------------------------------------------------------+
//| Инициализирует свойства элемента                                 |
//+------------------------------------------------------------------+
void CProgressBar::Initialize(void)
  {
   this.SetBorderSizeAll(1);
   this.SetBorderStyle(FRAME_STYLE_SIMPLE);
   this.SetBackgroundColor(CLR_DEF_CONTROL_PROGRESS_BAR_BACK_COLOR,true);
   this.SetBorderColor(CLR_DEF_CONTROL_PROGRESS_BAR_BORDER_COLOR,true);
   this.SetForeColor(CLR_DEF_CONTROL_PROGRESS_BAR_FORE_COLOR,true);
   this.SetMarqueeAnimationSpeed(10);
   this.SetMaximum(100);
   this.SetMinimum(0);
   this.SetValue(50);
   this.SetStep(10);
   this.SetStyle(CANV_ELEMENT_PROGRESS_BAR_STYLE_CONTINUOUS);
   this.m_progress_bar_max=this.Width()-this.BorderSizeLeft()-this.BorderSizeRight();
   this.m_value_by_max=this.Value()*100/this.Maximum();
   this.m_steps_skipped=0;
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Создаёт объект-полосу прогресса                                  |
//+------------------------------------------------------------------+
void CProgressBar::CreateProgressBar(void)
  {
//--- Устанавливаем длину полосы прогресса, равной значению Value() объекта
//--- Высота полосы прогресса устанавливается равнойвысоте объекта минус размеры рамки сверху и снизу
   int w=this.CalculateProgressBarWidth();
   int h=this.Height()-this.BorderSizeTop()-this.BorderSizeBottom();
//--- Создаём  объект-полосу прогресса
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_BAR_PROGRESS_BAR,0,0,w,h,clrNONE,255,false,false);
//--- Создаём объект-блик
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_GLARE_OBJ,0,0,w,h,CLR_CANV_NULL,0,true,false);
//--- Добавляем текущий объект CProgressBar в список активных элементов коллекции
   this.AddObjToListActiveElements();
  }
//+------------------------------------------------------------------+

Логика метода расписана в комментариях к коду. Здесь всё просто: рассчитываем ширину полосы прогресса и создаём объект с рассчитанной шириной. По умолчанию — 50% от ширины объекта ProgressBar. Затем создаём объект-блик и добавляем текущий объект в список активных объектов. Все объекты, находящиеся в списке активных элементов, обрабатываются в таймере библиотеки и могут выполнять некие независимые действия, которые реализованы в обработчике таймера этих объектов. Для данного элемента управления в таймере будет обрабатываться появление блика на полосе прогресса.


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

//+------------------------------------------------------------------+
//| Инициализирует значения для обработки в PerformStep              |
//+------------------------------------------------------------------+
void CProgressBar::SetValuesForProcessing(const int minimum,const int maximum,const int step,const int steps_skipped)
  {
   this.SetMinimum(minimum<0 ? 0 : minimum);
   this.SetMaximum(maximum<this.Minimum() ? this.Minimum() : maximum);
   this.SetStep(step<0 ? 0 : step);
   this.m_steps_skipped=steps_skipped;
  }
//+------------------------------------------------------------------+

Здесь: в метод передаются значения минимального и максимального пределов, в которых может изменяться значение Value, шаг изменения и количество пропускаемых шагов. Далее проверяются значения минимума, максимума и шага на корректность и записывается значение пропускаемых шагов в переменную. Так как Value полосы прогресса рассчитывается в процентном отношении от ширины объекта ProgressBar, то, скорее всего, значение пропускаемых шагов использоваться не будет — это будет ясно из серии тестов объекта после его готовности. В данной реализации это значение нигде не используется.

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

//+------------------------------------------------------------------+
//| Устанавливает текущее значение полосы прогресса                  |
//+------------------------------------------------------------------+
void CProgressBar::SetValue(const int value)
  {
//--- Корректируем переданное в метод значение value и устанавливаем его в свойство объекта
   int v=(value<this.Minimum() ? this.Minimum() : value>this.Maximum() ? this.Maximum() : value);
   this.SetProperty(CANV_ELEMENT_PROP_PROGRESS_BAR_VALUE,v);
//--- Получаем объект-полосу прогресса
   CBarProgressBar *bar=this.GetProgressBar();
   if(bar!=NULL)
     {
      //--- Устанавливаем для полосы прогресса значение value
      bar.SetValue(v);
      //--- Рассчитываем ширину объекта-полосы прогресса
      int w=this.CalculateProgressBarWidth();
      //--- Если рассчитанное значение ширины больше максимально-возможной - делаем ширину максимальной
      if(w>this.m_progress_bar_max)
         w=this.m_progress_bar_max;
      //--- Если значение ширины меньше 1, то
      if(w<1)
        {
         //--- скрываем полосу прогресса и перерисовываем график для немедленного отображения изменений
         bar.Hide();
         ::ChartRedraw(bar.ChartID());
        }
      //--- Если значение ширины не меньше 1
      else
        {
         //--- Если полоса прогресса скрыта - отображаем её и
         if(!bar.IsVisible())
            bar.Show();
         //--- изменяем её размер в соответствии с полученной шириной
         bar.Resize(w,bar.Height(),true);
        }
     }
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Увеличивает текущую позицию полосы прогресса на значение шага    |
//+------------------------------------------------------------------+
void CProgressBar::PerformStep(void)
  {
   this.SetValue(this.Value()+this.Step());
  }
//+------------------------------------------------------------------+

Здесь мы просто устанавливаем в свойство Value объекта его прошлое значение плюс заданное ранее значение шага приращения этой величины. Таким образом, при каждом последующем вызове этого метода значение Value будет увеличиваться на шаг приращения, записанный в Step.


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

//+------------------------------------------------------------------+
//| Рассчитывает ширину полосы прогресса                             |
//+------------------------------------------------------------------+
int CProgressBar::CalculateProgressBarWidth(void)   

  {
   this.m_value_by_max=this.Value()*100/this.Maximum();
   return this.m_progress_bar_max*this.m_value_by_max/100;
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Устанавливает новую ширину                                       |
//+------------------------------------------------------------------+
bool CProgressBar::SetWidth(const int width)
  {
   if(!CGCnvElement::SetWidth(width))
      return false;
   this.m_progress_bar_max=this.Width()-this.BorderSizeLeft()-this.BorderSizeRight();
   CBarProgressBar *bar=this.GetProgressBar();
   if(bar==NULL)
      return false;
   int w=this.CalculateProgressBarWidth();
   bar.SetWidth(w);
   return true;
  }
//+------------------------------------------------------------------+

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


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

К файлу \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\BarProgressBar.mqh класса объекта-полосы прогресса подключим файл объекта-блика, а в его приватной секции напишем методы для установки/возврата задержки и объявим метод инициализации свойств объекта:

//+------------------------------------------------------------------+
//|                                               BarProgressBar.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 "..\WinFormBase.mqh"
#include "..\GlareObj.mqh"
//+------------------------------------------------------------------+
//| Класс объекта BarProgressBar элемента управления ProgressBar     |
//+------------------------------------------------------------------+
class CBarProgressBar : public CWinFormBase
  {
private:
//--- (1) Устанавливает, (2) возвращает задержку перед отображением эффекта
   void              SetShowDelay(const long delay)               { this.SetProperty(CANV_ELEMENT_PROP_TOOLTIP_RESHOW_DELAY,delay);             }
   ulong             ShowDelay(void)                              { return this.GetProperty(CANV_ELEMENT_PROP_TOOLTIP_RESHOW_DELAY);            }
//--- Инициализирует свойства
   void              Initialize(void);
protected:


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

//+------------------------------------------------------------------+
//| Защищённый конструктор с указанием типа объекта,                 |
//| идентификатора чарта и подокна                                   |
//+------------------------------------------------------------------+
CBarProgressBar::CBarProgressBar(const ENUM_GRAPH_ELEMENT_TYPE type,
                                 CGCnvElement *main_obj,CGCnvElement *base_obj,
                                 const long chart_id,
                                 const int subwindow,
                                 const string descript,
                                 const int x,
                                 const int y,
                                 const int w,
                                 const int h) : CWinFormBase(type,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
//--- Установим объекту указанный тип графического элемента, а тип объекта библиотеки - как тип этого объекта
   this.SetTypeElement(type);
   this.m_type=OBJECT_DE_TYPE_GWF_HELPER;
   this.Initialize();
  }
//+------------------------------------------------------------------+
//| Конструктор с указанием главного и базового объектов,            |
//| идентификатора чарта и подокна                                   |
//+------------------------------------------------------------------+
CBarProgressBar::CBarProgressBar(CGCnvElement *main_obj,CGCnvElement *base_obj,
                                 const long chart_id,
                                 const int subwindow,
                                 const string descript,
                                 const int x,
                                 const int y,
                                 const int w,
                                 const int h) : CWinFormBase(GRAPH_ELEMENT_TYPE_WF_BAR_PROGRESS_BAR,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_BAR_PROGRESS_BAR);
   this.m_type=OBJECT_DE_TYPE_GWF_HELPER;
   this.Initialize();
  }
//+------------------------------------------------------------------+


Перенесём строки инициализации свойств, удалённые из конструкторов, в метод инициализации:

//+------------------------------------------------------------------+
//| Инициализирует свойства                                          |
//+------------------------------------------------------------------+
void CBarProgressBar::Initialize(void)
  {
   this.SetPaddingAll(0);
   this.SetMarginAll(0);
   this.SetBorderSizeAll(0);
   this.SetBackgroundColor(CLR_DEF_CONTROL_PROGRESS_BAR_BAR_COLOR,true);
   this.SetBorderColor(CLR_DEF_CONTROL_PROGRESS_BAR_BAR_COLOR,true);
   this.SetForeColor(CLR_DEF_CONTROL_PROGRESS_BAR_FORE_COLOR,true);
   this.SetShowDelay(2000);
  }
//+------------------------------------------------------------------+

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

Ранее в таймере объекта мы просто в коментарии отправляли на график значение из функции GetTickCount().
Сейчас же напишем полноценный обработчик.

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

//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void CBarProgressBar::OnTimer(void)
  {
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
   CWinFormBase *glare=base.GetElementByType(GRAPH_ELEMENT_TYPE_WF_GLARE_OBJ,0);
   if(glare==NULL)
      return;
      
//--- Если объект находится в обычном состоянии (скрыт)
   if(glare.DisplayState()==CANV_ELEMENT_DISPLAY_STATE_NORMAL)
     {
      //--- задаём объекту состояние ожидания плавного появления (здесь - смещения по полосе прогресса),
      //--- задаём длительность ожидания и устанавливаем время отсчёта
      glare.SetDisplayState(CANV_ELEMENT_DISPLAY_STATE_WAITING_FADE_IN);
      this.m_pause.SetWaitingMSC(this.ShowDelay());
      this.m_pause.SetTimeBegin();
      //--- Если правый край объекта-блика правее левого края объекта-полосы прогресса
      if(glare.RightEdge()>=this.CoordX())
        {
         //--- Скрываем объект-блик и сдвигаем его за правый край полосы прогресса
         glare.Hide();
         if(glare.Move(this.CoordX()-glare.Width(),this.CoordY()))
           {
            //--- Устанавливаем относительные координаты объекта-блика
            glare.SetCoordXRelative(glare.CoordX()-this.CoordX());
            glare.SetCoordYRelative(glare.CoordY()-this.CoordY());
            //--- и его область видимости размером с весь объект
            glare.SetVisibleArea(0,0,glare.Width(),glare.Height());
           }
        }
      return;
     }
//--- Если объект находится в состоянии ожидания плавного появления (здесь - смещения по полосе прогресса)
   if(glare.DisplayState()==CANV_ELEMENT_DISPLAY_STATE_WAITING_FADE_IN)
     {
      //--- Если ещё не прошло время ожидания - уходим
      if(this.m_pause.Passed()<this.ShowDelay())
         return;
      //--- Устанавливаем состояние нахождения объекта в процессе смещения по полосе прогресса и
      //---  устанавливаем время отсчёта начала процесса
      glare.SetDisplayState(CANV_ELEMENT_DISPLAY_STATE_PROCESS_FADE_IN);
      this.m_pause.SetTimeBegin();
      return;
     }
//--- Если объект находится в состоянии смещения по полосе прогресса
   if(glare.DisplayState()==CANV_ELEMENT_DISPLAY_STATE_PROCESS_FADE_IN)
     {
      //--- Если координата X объекта-блика всё ещё не вышла за правый край полосы прогресса
      if(glare.CoordX()<=this.RightEdge())
        {
         //--- устанавливаем флаг отображения и показываем объект
         if(!glare.Displayed())
            glare.SetDisplayed(true);
         if(!glare.IsVisible())
            glare.Show();
         //--- выводим объект-блик на передний план
         glare.BringToTop();
         //--- Смещаем блик на 16 пикселей вправо
         if(glare.Move(glare.CoordX()+16,this.CoordY()))
           {
            //--- Устанавливаем относительные координаты объекта-блика и перерисовываем его
            glare.SetCoordXRelative(glare.CoordX()-this.CoordX());
            glare.SetCoordYRelative(glare.CoordY()-this.CoordY());
            glare.Redraw(true);
           }
         return;
        }
      //--- Устанавливаем состояние завершения смещения по полосе прогресса
      glare.SetDisplayState(CANV_ELEMENT_DISPLAY_STATE_COMPLETED_FADE_IN);
     }
//--- Если объект находится в состоянии завершения смещения по полосе прогресса
   if(glare.DisplayState()==CANV_ELEMENT_DISPLAY_STATE_COMPLETED_FADE_IN)
     {
      //--- задаём объекту обычное состояние (невидимый),
      //--- скрываем объект и устанавливаем ему флаг невидимости
      glare.SetDisplayState(CANV_ELEMENT_DISPLAY_STATE_NORMAL);
      glare.Hide();
      glare.SetDisplayed(false);
      return;
     }
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Список состояний отображения элемента управления                 |
//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_DISPLAY_STATE
  {
   CANV_ELEMENT_DISPLAY_STATE_NORMAL,                 // Нормально
   CANV_ELEMENT_DISPLAY_STATE_WAITING_FADE_IN,        // Ожидание процесса постепенного появления
   CANV_ELEMENT_DISPLAY_STATE_PROCESS_FADE_IN,        // Процесс постепенного появления
   CANV_ELEMENT_DISPLAY_STATE_COMPLETED_FADE_IN,      // Завершение процесса постепенного появления
   CANV_ELEMENT_DISPLAY_STATE_WAITING_FADE_OUT,       // Ожидание процесса постепенного исчезания
   CANV_ELEMENT_DISPLAY_STATE_PROCESS_FADE_OUT,       // Процесс постепенного исчезания
   CANV_ELEMENT_DISPLAY_STATE_COMPLETED_FADE_OUT,     // Завершение процесса постепенного исчезания
   CANV_ELEMENT_DISPLAY_STATE_COMPLETED,              // Завершение процесса обработки
  };
//+------------------------------------------------------------------+

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

У нас всё готово для тестирования.


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

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

У нас уже ранее был создан статический объект ProgressBar на второй панели элемента управления SplitContainer, расположенного на первой вкладке элемента управления TabControl. Сейчас нам нужно получить указатель на объект ProgressBar и сначала в цикле увеличить его размер — чтобы визуально увидеть, как при этом изменяется относительный размер полосы прогресса, установленный в 50% от ширины ProgressBar. Затем, опять же, в цикле будем увеличивать значение полосы прогресса при помощи метода PerformStep. Для работы метода заранее установим нужные параметры: минимум = 0, максимум = 350, шаг = 1. После того как оба цикла будут завершены, получим указатель на объект-блик и установим для него параметры отображения.

Для реализации всего этого в конце обработчика OnInit() напишем такой блок кода:

//--- Отобразим и перерисуем все созданные панели
   for(int i=0;i<FORMS_TOTAL;i++)
     {
      //--- Получаем объект-панель
      pnl=engine.GetWFPanel("WinForms Panel"+(string)i);
      if(pnl!=NULL)
        {
         //--- отображаем и перерисовываем панель
         pnl.Show();
         pnl.Redraw(true);
         //--- Получаем с панели объект TabControl
         CTabControl *tc=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,0);
         //--- С первой вкладки объекта TabControl получаем объект SplitContainer
         CSplitContainer *sc=tc.GetTabElementByType(0,GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER,0);
         //--- С объекта SplitContainer получаем его вторую панель
         CSplitContainerPanel *scp=sc.GetPanel(1);
         //--- С полученной панели получаем объект ProgressBar
         CProgressBar *pb=scp.GetElementByType(GRAPH_ELEMENT_TYPE_WF_PROGRESS_BAR,0);
         //--- Ожидаем 1/10 секунды
         Sleep(100);
         //--- Получаем ширину объекта ProgressBar
         int w=pb.Width();
         //--- В цикле с задержкой 1/50 увеличиваем ширину ProgressBar на 180 пикселей
         for(int n=0;n<180;n++)
           {
            Sleep(20);
            pb.Resize(w+n,pb.Height(),true);
           }
         //--- Устанавливаем значения для PerformStep объекта ProgressBar
         pb.SetValuesForProcessing(0,350,1,0);
         //--- Сбрасываем полосу прогресса объекта ProgressBar в минимальное положение
         pb.ResetProgressBar();
         //--- Ожидаем 1/5 секунды
         Sleep(200);
         //--- В цикле от минимального до максимального значения ProgressBar
         for(int n=0;n<=pb.Maximum();n++)
           {
            //--- вызываем метод увеличения полосы прогресса на заданный шаг с ожиданием в 1/5 секунды
            pb.PerformStep();
            Sleep(20);
           }
         //--- Получаем объект-блик
         CGlareObj *gl=pb.GetElementByType(GRAPH_ELEMENT_TYPE_WF_GLARE_OBJ,0);
         if(gl!=NULL)
           {
            //--- Устанавливаем тип блика - прямоугольник, непрозрачность 40, цвет - белый
            gl.SetStyleRectangle();
            gl.SetOpacity(40);
            gl.SetColor(clrWhite);
           }
        }
     }
        
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+


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


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

Во втором цикле мы каждый раз вызываем метод PerformStep, который на каждом шаге увеличивает значение Value (относительное) на значение шага приращения. Хоть и выполняется 350 шагов приращения, но видно, что полоса прогресса увеличивается больше, чем на 1 пиксель за раз. Это происходит из-за того, что все величины относительны и расчитываются в процентном отношении к ширине ProgressBar. И это как раз нормально и правильно — невозможно приращать на каждом шаге по одному пикселю, если ширина ProgressBar равна 100 пикселей, а количество шагов 1000, то на один пиксель 10 шагов приращения получается. И они пропускаются при расчёте относительных величин.

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

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


Что дальше

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

К содержанию

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

 
DoEasy. Элементы управления (Часть 20): WinForms-объект SplitContainer
DoEasy. Элементы управления (Часть 21): Элемент управления SplitContainer. Разделитель панелей
DoEasy. Элементы управления (Часть 22): SplitContainer. Изменение свойств созданного объекта
DoEasy. Элементы управления (Часть 23): дорабатываем WinForms-объекты TabControl и SplitContainer
DoEasy. Элементы управления (Часть 24): Вспомогательный WinForms-объект "Подсказка"
DoEasy. Элементы управления (Часть 25): WinForms-объект "Tooltip"
DoEasy. Элементы управления (Часть 26): Дорабатываем WinForms-объект "ToolTip" и начинаем разработку индикатора выполнения "ProgressBar"