Графика в библиотеке DoEasy (Часть 77): Класс объекта Тень

24 июня 2021, 13:22
Artyom Trishkin
0
1 363

Содержание


Концепция

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

Рассматривалось два варианта рисования теней объектам:

  1. прямо на канвасе самого объекта-формы,
  2. на отдельном объекте, "лежащем" под объектом-формой.

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

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

Несомненно, у способа рисовать тени прямо на канвасе формы есть свои преимущества, но всё же мы выберем второй вариант из-за достаточной простоты его реализации и управления им. В первой реализации объекта-тени мы будем использовать метод размытия по Гауссу при помощи библиотеки численного анализа ALGLIB. Некоторые моменты её использования для построения теней были описаны в статье "Изучаем класс CCanvas. Сглаживание и тени", написанной Владимиром Карпутовым. Возьмём на вооружение описанные в его статье методы размытия по Гауссу.

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


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

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

   MSG_LIB_SYS_FAILED_DRAWING_ARRAY_RESIZE,           // Не удалось изменить размер массива рисуемых буферов
   MSG_LIB_SYS_FAILED_COLORS_ARRAY_RESIZE,            // Не удалось изменить размер массива цветов
   MSG_LIB_SYS_FAILED_ARRAY_RESIZE,                   // Не удалось изменить размер массива
   MSG_LIB_SYS_FAILED_ADD_BUFFER,                     // Не удалось добавить объект-буфер в список
   MSG_LIB_SYS_FAILED_CREATE_BUFFER_OBJ,              // Не удалось создать объект \"Индикаторный буфер\"

...

//--- CChartObjCollection
   MSG_CHART_COLLECTION_TEXT_CHART_COLLECTION,        // Коллекция чартов
   MSG_CHART_COLLECTION_ERR_FAILED_CREATE_CHART_OBJ,  // Не удалось создать новый объект-чарт
   MSG_CHART_COLLECTION_ERR_FAILED_ADD_CHART,         // Не удалось добавить объект-чарт в коллекцию
   MSG_CHART_COLLECTION_ERR_CHARTS_MAX,               // Нельзя открыть новый график, так как количество открытых графиков уже максимальное
   MSG_CHART_COLLECTION_CHART_OPENED,                 // Открыт график
   MSG_CHART_COLLECTION_CHART_CLOSED,                 // Закрыт график
   MSG_CHART_COLLECTION_CHART_SYMB_CHANGED,           // Изменён символ графика
   MSG_CHART_COLLECTION_CHART_TF_CHANGED,             // Изменён таймфрейм графика
   MSG_CHART_COLLECTION_CHART_SYMB_TF_CHANGED,        // Изменён символ и таймфрейм графика
  
//--- CForm
   MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT,// Отсутствует объект тени. Необходимо сначала его создать при помощи метода CreateShadowObj()
   MSG_FORM_OBJECT_ERR_FAILED_CREATE_SHADOW_OBJ,      // Не удалось создать новый объект для тени
   
//--- CShadowObj
   MSG_SHADOW_OBJ_IMG_SMALL_BLUR_LARGE,               // Ошибка! Размер изображения очень маленький или очень большое размытие
   
  };
//+------------------------------------------------------------------+

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

   {"Не удалось изменить размер массива рисуемых буферов","Failed to resize drawing buffers array"},
   {"Не удалось изменить размер массива цветов","Failed to resize color array"},
   {"Не удалось изменить размер массива ","Failed to resize array "},
   {"Не удалось добавить объект-буфер в список","Failed to add buffer object to list"},
   {"Не удалось создать объект \"Индикаторный буфер\"","Failed to create object \"Indicator buffer\""},

...

//--- CChartObjCollection
   {"Коллекция чартов","Chart collection"},
   {"Не удалось создать новый объект-чарт","Failed to create new chart object"},
   {"Не удалось добавить объект-чарт в коллекцию","Failed to add chart object to collection"},
   {"Нельзя открыть новый график, так как количество открытых графиков уже максимальное","You cannot open a new chart, since the number of open charts is already maximum"},
   {"Открыт график","Open chart"},
   {"Закрыт график","Closed chart"},
   {"Изменён символ графика","Changed chart symbol"},
   {"Изменён таймфрейм графика","Changed chart timeframe"},
   {"Изменён символ и таймфрейм графика","Changed the symbol and timeframe of the chart"},
   
//--- CForm
   {"Отсутствует объект тени. Необходимо сначала его создать при помощи метода CreateShadowObj()","There is no shadow object. You must first create it using the CreateShadowObj () method"},
   {"Не удалось создать новый объект для тени","Failed to create new object for shadow"},
   
//--- CShadowObj
   {"Ошибка! Размер изображения очень маленький или очень большое размытие","Error! Image size is very small or very large blur"},
      
  };
//+---------------------------------------------------------------------+

В прошлой статье мы для рисования тени оставляли пустое пространство вокруг объекта-формы размером в пять пикселей с каждой стороны. Как оказалось, для нормального размытия по Гауссу нам нужно большее пространство. Эмпирическим путём выявил, что при радиусе размытия в 4 пикселя нам необходимо оставить с каждой из сторон по 16 пикселей пустого пространства. Меньшее количество пикселей при размытии приводит к появлению артефактов (загрязнений фона где тень уже полностью прозрачна и фактически отсутствует) по краям канваса, на котором рисуется тень.

В файле \MQL5\Include\DoEasy\Defines.mqh впишем размер свободного пространства по умолчанию для тени равным 16 (вместо ранее 5):

//--- Параметры канваса
#define PAUSE_FOR_CANV_UPDATE          (16)                       // Частота обновления канваса
#define NULL_COLOR                     (0x00FFFFFF)               // Ноль для канваса с альфа-каналом
#define OUTER_AREA_SIZE                (16)                       // Размер одной стороны внешней области вокруг рабочего пространства
//+------------------------------------------------------------------+

А в перечисление типов графических элементов добавим новый тип — Объект тени:

//+------------------------------------------------------------------+
//| Список типов графических элементов                               |
//+------------------------------------------------------------------+
enum ENUM_GRAPH_ELEMENT_TYPE
  {
   GRAPH_ELEMENT_TYPE_ELEMENT,                        // Элемент
   GRAPH_ELEMENT_TYPE_SHADOW_OBJ,                     // Объект тени
   GRAPH_ELEMENT_TYPE_FORM,                           // Форма
   GRAPH_ELEMENT_TYPE_WINDOW,                         // Окно
  };
//+------------------------------------------------------------------+

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

Так как сегодня будем создавать объект тени, то у него будут свои свойства, влияющие на внешний вид тени, отбрасываемой объектом-формой.
Внесём эти параметры в настройки стилей форм в файле \MQL5\Include\DoEasy\GraphINI.mqh:

//+------------------------------------------------------------------+
//| Список индексов параметров стилей форм                           |
//+------------------------------------------------------------------+
enum ENUM_FORM_STYLE_PARAMS
  {
   FORM_STYLE_FRAME_WIDTH_LEFT,                 // Ширина рамки формы слева
   FORM_STYLE_FRAME_WIDTH_RIGHT,                // Ширина рамки формы справа
   FORM_STYLE_FRAME_WIDTH_TOP,                  // Ширина рамки формы сверху
   FORM_STYLE_FRAME_WIDTH_BOTTOM,               // Ширина рамки формы снизу
   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
  };
#define TOTAL_FORM_STYLE_PARAMS        (9)      // Количество параметров стиля формы
//+------------------------------------------------------------------+
//| Массив, содержащий параметры стилей форм                         |
//+------------------------------------------------------------------+
int array_form_style[TOTAL_FORM_STYLES][TOTAL_FORM_STYLE_PARAMS]=
  {
//--- Параметры стиля формы "Плоская форма"
   {
      3,                                        // Ширина рамки формы слева
      3,                                        // Ширина рамки формы справа
      3,                                        // Ширина рамки формы сверху
      3,                                        // Ширина рамки формы снизу
      80,                                       // Непрозрачность тени
      4,                                        // Размытие тени
      80,                                       // Затемнённость цвета тени формы
      2,                                        // Смещение тени по оси X
      2,                                        // Смещение тени по оси Y
   },
//--- Параметры стиля формы "Рельефная форма"
   {
      4,                                        // Ширина рамки формы слева
      4,                                        // Ширина рамки формы справа
      4,                                        // Ширина рамки формы сверху
      4,                                        // Ширина рамки формы снизу
      80,                                       // Непрозрачность тени
      4,                                        // Размытие тени
      80,                                       // Затемнённость цвета тени формы
      2,                                        // Смещение тени по оси X
      2,                                        // Смещение тени по оси Y
   },
  };
//+------------------------------------------------------------------+

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

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

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

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

//+------------------------------------------------------------------+
//| Список индексов параметров цветов цветовой схемы                 |
//+------------------------------------------------------------------+
enum ENUM_COLOR_THEME_COLORS
  {
   COLOR_THEME_COLOR_FORM_BG,                   // Цвет фона формы
   COLOR_THEME_COLOR_FORM_FRAME,                // Цвет рамки формы
   COLOR_THEME_COLOR_FORM_RECT_OUTER,           // Цвет очерчивающего прямоугольника формы
   COLOR_THEME_COLOR_FORM_SHADOW,               // Цвет тени формы
  };
#define TOTAL_COLOR_THEME_COLORS       (4)      // Количество параметров в цветовой теме
//+------------------------------------------------------------------+
//| Массив, содержащий цветовые схемы                                |
//+------------------------------------------------------------------+
color array_color_themes[TOTAL_COLOR_THEMES][TOTAL_COLOR_THEME_COLORS]=
  {
//--- Параметры цветовой схемы "Голубая сталь"
   {
      C'134,160,181',                           // Цвет фона формы
      C'134,160,181',                           // Цвет рамки формы
      clrDimGray,                               // Цвет очерчивающего прямоугольника формы
      clrGray,                                  // Цвет тени формы
   },
//--- Параметры цветовой схемы "Светлый серо-циановый"
   {
      C'181,196,196',                           // Цвет фона формы
      C'181,196,196',                           // Цвет рамки формы
      clrGray,                                  // Цвет очерчивающего прямоугольника формы
      clrGray,                                  // Цвет тени формы
   },
  };
//+------------------------------------------------------------------+


Доработаем класс графического элемента в файле \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh.

В публичной секции класса у нас есть метод ChangeColorLightness(), изменяющий на указанную величину яркость (Lightness) цвета.
В метод передаётся цвет, который нужно поменять, в формате ARGB. Это не всегда удобно, поэтому объявим перегруженный метод, в который будем передавать цвет в формате color и непрозрачность:

//--- Обновляет координаты (сдвигает канвас)
   bool              Move(const int x,const int y,const bool redraw=false);

//--- Изменяет яркость(1) ARGB, (2) COLOR цвета на указанную величину
   uint              ChangeColorLightness(const uint clr,const double change_value);
   color             ChangeColorLightness(const color colour,const uchar opacity,const double change_value);

Также нам потребуются методы для изменения насыщенности цвета. Например, чтобы из любого цвета сделать серый цвет, нам необходимо его составляющую Saturation (S в форматах HSL, HSI, HSV, HSB) сместить влево — к нулю. Таким образом, цвет станет полностью ненасыщенным — будет оттенком серого, что нам и нужно для рисования тени.

Объявим два перегруженных метода, изменяющих насыщенность цвета:

//--- Изменяет яркость(1) ARGB, (2) COLOR цвета на указанную величину
   uint              ChangeColorLightness(const uint clr,const double change_value);
   color             ChangeColorLightness(const color colour,const double change_value);
//--- Изменяет насыщенность (1) ARGB, (2) COLOR цвета на указанную величину
   uint              ChangeColorSaturation(const uint clr,const double change_value);
   color             ChangeColorSaturation(const color colour,const double change_value);
   
protected:

За пределами тела класса напишем реализацию объявленных методов.

Метод, изменяющий насыщенность ARGB-цвета на указанную величину:

//+------------------------------------------------------------------+
//| Изменяет насыщенность ARGB-цвета на указанную величину           |
//+------------------------------------------------------------------+
uint CGCnvElement::ChangeColorSaturation(const uint clr,const double change_value)
  {
   if(change_value==0.0)
      return clr;
   double a=GETRGBA(clr);
   double r=GETRGBR(clr);
   double g=GETRGBG(clr);
   double b=GETRGBB(clr);
   double h=0,s=0,l=0;
   CColors::RGBtoHSL(r,g,b,h,s,l);
   double ns=s+change_value*0.01;
   if(ns>1.0) ns=1.0;
   if(ns<0.0) ns=0.0;
   CColors::HSLtoRGB(h,ns,l,r,g,b);
   return ARGB(a,r,g,b);
  }
//+------------------------------------------------------------------+

Здесь: раскладываем, полученный как uint-значение цвет, на составляющие — альфа-канал, красный, зелёный и синий.
При помощи метода RGBtoHSL() класса CColors, описанного в статье 75, переводим RGB-цвет в цветовую модель HSL, в которой нам нужна его составляющая S — насыщенность цвета. Далее рассчитываем новую насыщенность, просто прибавляя к значению насыщенности цвета значение, переданное в метод и умноженное на 0.01. Полученный результат проверяем на предмет выхода из диапазона допустимых значений (от 0 до 1) и опять же при помощи класса CColors и его метода HSLtoRGB преобразуем составляющие цвета H, новое S и L в формат RGB.
Возвращаем полученный RGB-цвет с добавлением альфа-канала исходного цвета
.

Для чего мы умножаем на 0.01 значение, на которое нужно изменить насыщенность, передаваемое в метод? Просто для удобства. Так как в цветовой модели HSL значения составляющих меняются в диапазоне от 0 до 1, то во-первых — удобнее передавать эти значения в величинах, кратным 100 (1 вместо 0.01, 10 вместо 0.1, 100 вместо 1), а во-вторых — у нас в стилях форм, где могут быть значения изменения насыщенности цвета для каких-либо форм или текстов, все значения записываются целочисленными величинами, и эта причина куда весомее первой.

Метод, изменяющий насыщенность COLOR-цвета на указанную величину:

//+------------------------------------------------------------------+
//| Изменяет насыщенность COLOR-цвета на указанную величину          |
//+------------------------------------------------------------------+
color CGCnvElement::ChangeColorSaturation(const color colour,const double change_value)
  {
   if(change_value==0.0)
      return colour;
   uint clr=::ColorToARGB(colour,0);
   double r=GETRGBR(clr);
   double g=GETRGBG(clr);
   double b=GETRGBB(clr);
   double h=0,s=0,l=0;
   CColors::RGBtoHSL(r,g,b,h,s,l);
   double ns=s+change_value*0.01;
   if(ns>1.0) ns=1.0;
   if(ns<0.0) ns=0.0;
   CColors::HSLtoRGB(h,ns,l,r,g,b);
   return CColors::RGBToColor(r,g,b);
  }
//+------------------------------------------------------------------+

Метод по логике похож на вышерассмотренный. Разница лишь в том, что здесь нам параметр непрозрачности нужен лишь для преобразования цвета и его непрозрачности в ARGB-цвет и более нигде альфа-канал не используется. Поэтому при преобразовании мы его можем не учитывать и передать ноль. Далее из ARGB-цвета извлекаем составляющие R, G и B, преобразуем их в цветовую модель HSL, компоненту S корректируем на переданную в метод величину, преобразуем HSL модель обратно в RGB и возвращаем RGB-модель цвета, преобразованную в цвет в формате color.

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

//+------------------------------------------------------------------+
//| Изменяет яркость COLOR-цвета на указанную величину               |
//+------------------------------------------------------------------+
color CGCnvElement::ChangeColorLightness(const color colour,const double change_value)
  {
   if(change_value==0.0)
      return colour;
   uint clr=::ColorToARGB(colour,0);
   double r=GETRGBR(clr);
   double g=GETRGBG(clr);
   double b=GETRGBB(clr);
   double h=0,s=0,l=0;
   CColors::RGBtoHSL(r,g,b,h,s,l);
   double nl=l+change_value*0.01;
   if(nl>1.0) nl=1.0;
   if(nl<0.0) nl=0.0;
   CColors::HSLtoRGB(h,s,nl,r,g,b);
   return CColors::RGBToColor(r,g,b);
  }
//+------------------------------------------------------------------+

Метод идентичен вышерассмотренному за исключением того, что здесь мы меняем компоненту L цветовой модели HSL.

И, так как во всех рассмотренных методах мы умножаем величину, на которую нужно изменить составляющую цвета, на 0.01, то нам нужно внести правку в метод, изменяющий яркость ARGB-цвета на указанную величину, написанный нами ранее:

//+------------------------------------------------------------------+
//| Изменяет яркость ARGB-цвета на указанную величину                |
//+------------------------------------------------------------------+
uint CGCnvElement::ChangeColorLightness(const uint clr,const double change_value)
  {
   if(change_value==0.0)
      return clr;
   double a=GETRGBA(clr);
   double r=GETRGBR(clr);
   double g=GETRGBG(clr);
   double b=GETRGBB(clr);
   double h=0,s=0,l=0;
   CColors::RGBtoHSL(r,g,b,h,s,l);
   double nl=l+change_value*0.01;
   if(nl>1.0) nl=1.0;
   if(nl<0.0) nl=0.0;
   CColors::HSLtoRGB(h,s,nl,r,g,b);
   return ARGB(a,r,g,b);
  }
//+------------------------------------------------------------------+

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

//--- Устанавливает флаг (1) перемещаемости, (2) активности объекта, (3) идентификатор элемента, (4) номер элемента в списке, (5) наличие тени
   void              SetMovable(const bool flag)               { this.SetProperty(CANV_ELEMENT_PROP_MOVABLE,flag);                     }
   void              SetActive(const bool flag)                { this.SetProperty(CANV_ELEMENT_PROP_ACTIVE,flag);                      }
   void              SetID(const int id)                       { this.SetProperty(CANV_ELEMENT_PROP_ID,id);                            }
   void              SetNumber(const int number)               { this.SetProperty(CANV_ELEMENT_PROP_NUM,number);                       }
   void              SetShadow(const bool flag)                { this.m_shadow=flag;                                                   }
   
//--- Возвращает смещение (1) левого, (2) правого, (3) верхнего, (4) нижнего края активной зоны элемента

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


У нас уже реализованы ранее два метода очистки формы и заполнения её цветом. Для заливки фона градиентным цветом объявим ещё один метод Erase():

//+------------------------------------------------------------------+
//| Методы заполнения, очистки и обновления растровых данных         |
//+------------------------------------------------------------------+
//--- Очищает элемент с заполнением его цветом и непрозрачностью
   void              Erase(const color colour,const uchar opacity,const bool redraw=false);
//--- Очищает элемент заливкой градиентом
   void              Erase(color &colors[],const uchar opacity,const bool vgradient=true,const bool cycle=false,const bool redraw=false);
//--- Полностью очищает элемент
   void              Erase(const bool redraw=false);
//--- Обновляет элемент
   void              Update(const bool redraw=false)           { this.m_canvas.Update(redraw);                                         }

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

//+------------------------------------------------------------------+
//| Очищает элемент заливкой градиентом                              |
//+------------------------------------------------------------------+
void CGCnvElement::Erase(color &colors[],const uchar opacity,const bool vgradient=true,const bool cycle=false,const bool redraw=false)
  {
//--- Проверяем размер массива цветов
   int size=::ArraySize(colors);
//--- Если цветов в массиве меньше двух
   if(size<2)
     {
      //--- если пустой массив, то полностью стираем фон и уходим
      if(size==0)
        {
         this.Erase(redraw);
         return;
        }
      //--- если один цвет, то заливаем фон этим цветом и непрозрачностью и уходим
      this.Erase(colors[0],opacity,redraw);
      return;
     }
//--- Объявляем массив-приёмник
   color out[];
//--- В зависимости от направления заливки (вертикально/горизонтально) устанавливаем размер градиента
   int total=(vgradient ? this.Height() : this.Width());
//--- и получаем м массив-приёмник набор цветов
   CColors::Gradient(colors,out,total,cycle);
   total=::ArraySize(out);
//--- В цикле по количеству цветов в массиве
   for(int i=0;i<total;i++)
     {
      //--- в зависимости от направления заливки
      switch(vgradient)
        {
         //--- Горизонтальный градиент - рисуем вертикальные отрезки прямых слева-направо с цветом из массива
         case false :
            DrawLineVertical(i,0,this.Height()-1,out[i],opacity);
           break;
         //--- Вертикальный градиент - рисуем горизонтальные отрезки прямых сверху-вниз с цветом из массива
         default:
            DrawLineHorizontal(0,this.Width()-1,i,out[i],opacity);
           break;
        }
     }
//--- Если указано - обновляем канвас
   this.Update(redraw);
  }
//+------------------------------------------------------------------+

Вся логика метода описана в комментариях в листинге. В метод передаётся заполненный массив цветов, значение непрозрачности, флаг вертикального градиента (если true, то заливка будет осуществляться сверху-вниз, если false — то слева-направо), флаг зацикливания (если установлен, то заливка завершится тем же цветом, с которого началась) и флаг необходимости перерисовать канвас после заливки. Для получения массива цветов используется метод Gradient() класса CColors.

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


Класс объекта тени

В каталоге графических объектов библиотеки \MQL5\Include\DoEasy\Objects\Graph\ создадим новый файл ShadowObj.mqh класса CShadowObj.

К файлу должен быть подключен файл класса графического элемента и файл библиотеки численного анализа 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
  {
  }

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

//+------------------------------------------------------------------+
//| Класс объекта Тени                                               |
//+------------------------------------------------------------------+
class CShadowObj : public CGCnvElement
  {
private:
   color             m_color_shadow;                  // Цвет тени
   uchar             m_opacity_shadow;                // Непрозрачность тени
   
//--- Размытие по-Гауссу
   bool              GaussianBlur(const uint radius);
//--- Возвращает массив весовых коэффициентов
   bool              GetQuadratureWeights(const double mu0,const int n,double &weights[]);
//--- Рисует форму тени объекта
   void              DrawShadowFigureRect(const int w,const int h);

public:

Здесь метод DrawShadowFigureRect() рисует неразмытую фигуру по размерам объекта-формы, отбрасывающего тень, рисуемую данным объектом.
Метод GetQuadratureWeights() при помощи библиотеки ALGLIB рассчитывает и возвращает массив весовых коэффициентов, использующихся для размытия фигуры, нарисованной методом DrawShadowFigureRect().
Размытие же этой фигуры осуществляется методом GaussianBlur().
Все методы будем рассматривать далее.

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

public:
                     CShadowObj(const long chart_id,
                                const int subwindow,
                                const string name,
                                const int x,
                                const int y,
                                const int w,
                                const int h);

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

//--- Рисует тень объекта
   void              DrawShadow(const int shift_x,const int shift_y,const uchar blur_value);
   
//+------------------------------------------------------------------+
//| Методы упрощённого доступа к свойствам объекта                   |
//+------------------------------------------------------------------+
//--- (1) Устанавливает, (2) возвращает цвет тени
   void              SetColorShadow(const color colour)                       { this.m_color_shadow=colour;    }
   color             ColorShadow(void)                                  const { return this.m_color_shadow;    }
//--- (1) Устанавливает, (2) возвращает непрозрачность тени
   void              SetOpacityShadow(const uchar opacity)                    { this.m_opacity_shadow=opacity; }
   uchar             OpacityShadow(void)                                const { return this.m_opacity_shadow;  }
  };
//+------------------------------------------------------------------+


Рассмотрим подробнее устройство методов класса.

Параметрический конструктор:

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CShadowObj::CShadowObj(const long chart_id,
                       const int subwindow,
                       const string name,
                       const int x,
                       const int y,
                       const int w,
                       const int h) : CGCnvElement(GRAPH_ELEMENT_TYPE_SHADOW_OBJ,chart_id,subwindow,name,x,y,w,h)
  {
   CGCnvElement::SetColorBackground(clrNONE);
   CGCnvElement::SetOpacity(0);
   CGCnvElement::SetActive(false);
   this.m_opacity_shadow=127;
   color gray=CGCnvElement::ChangeColorSaturation(ChartColorBackground(),-100);
   this.m_color_shadow=CGCnvElement::ChangeColorLightness(gray,255,-50);
   this.m_shadow=false;
   this.m_visible=true;
   CGCnvElement::Erase();
  }
//+------------------------------------------------------------------+

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

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

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

//+------------------------------------------------------------------+
//| Рисует тень объекта                                              |
//+------------------------------------------------------------------+
void CShadowObj::DrawShadow(const int shift_x,const int shift_y,const uchar blur_value)
  {
//--- Рассчитываем ширину и высоту рисуемого прямоугольника
   int w=this.Width()-OUTER_AREA_SIZE*2;
   int h=this.Height()-OUTER_AREA_SIZE*2;
//--- Рисуем закрашенный прямоугольник с рассчитанными размерами
   this.DrawShadowFigureRect(w,h);
//--- Рассчитываем радиус размытия, который не может быть больше четверти размера константы OUTER_AREA_SIZE
   int radius=(blur_value>OUTER_AREA_SIZE/4 ? OUTER_AREA_SIZE/4 : blur_value);
//--- Если размыть фигуру не удалось - уходим из метода (ошибку в журнал выведет GaussianBlur())
   if(!this.GaussianBlur(radius))
      return;
//--- Смещаем объект тени на указанные в аргументах метода смещения по X и Y и обновляем канвас
   CGCnvElement::Move(this.CoordX()+shift_x,this.CoordY()+shift_y);
   CGCnvElement::Update();
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Рисует форму тени объекта                                        |
//+------------------------------------------------------------------+
void CShadowObj::DrawShadowFigureRect(const int w,const int h)
  {
   CGCnvElement::DrawRectangleFill(OUTER_AREA_SIZE,OUTER_AREA_SIZE,OUTER_AREA_SIZE+w-1,OUTER_AREA_SIZE+h-1,this.m_color_shadow,this.m_opacity_shadow);
   CGCnvElement::Update();
  }
//+------------------------------------------------------------------+

Здесь: рисуем прямоугольник в координатах X и Y, равных значению константы OUTER_AREA_SIZE. Вторая координата X и Y рассчитывается как отступ от первой координаты + ширина (высота) минус 1. После рисования фигуры канвас обновляется.

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

//+------------------------------------------------------------------+
//| Размытие по-Гауссу                                               |
//| https://www.mql5.com/ru/articles/1612#chapter4                   |
//+------------------------------------------------------------------+
bool CShadowObj::GaussianBlur(const uint radius)
  {
//---
   int n_nodes=(int)radius*2+1;
   uint res_data[];              // Массив для хранения данных графического ресурса
   uint res_w=this.Width();      // Ширина графического ресурса
   uint res_h=this.Height();     // Высота графического ресурса
   
//--- Читаем данные графического ресурса, и если не получилось - возвращаем false
   ::ResetLastError();
   if(!::ResourceReadImage(this.NameRes(),res_data,res_w,res_h))
     {
      CMessage::OutByID(MSG_LIB_SYS_FAILED_GET_DATA_GRAPH_RES);
      return false;
     }
//--- Проверяем величину размытия, и если радиус размытия больше половины высоты или ширины - возвращаем false
   if(radius>=res_w/2 || radius>=res_h/2)
     {
      ::Print(DFUN,CMessage::Text(MSG_SHADOW_OBJ_IMG_SMALL_BLUR_LARGE));
      return false;
     }
     
//--- Раскладываем данные изображения из ресурса на компоненты цвета a, r, g, b
   int  size=::ArraySize(res_data);
//--- массивы для хранения компонент цвета 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::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"a_h_data\"");
      return false;
     }
   if(::ArrayResize(r_h_data,size)==-1)
     {
      CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"r_h_data\"");
      return false;
     }
   if(::ArrayResize(g_h_data,size)==-1)
     {
      CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"g_h_data\"");
      return false;
     }
   if(ArrayResize(b_h_data,size)==-1)
     {
      CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"b_h_data\"");
      return false;
     }
   if(::ArrayResize(a_v_data,size)==-1)
     {
      CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"a_v_data\"");
      return false;
     }
   if(::ArrayResize(r_v_data,size)==-1)
     {
      CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"r_v_data\"");
      return false;
     }
   if(::ArrayResize(g_v_data,size)==-1)
     {
      CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"g_v_data\"");
      return false;
     }
   if(::ArrayResize(b_v_data,size)==-1)
     {
      CMessage::OutByID(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(res_data[i]);
      r_h_data[i]=GETRGBR(res_data[i]);
      g_h_data[i]=GETRGBG(res_data[i]);
      b_h_data[i]=GETRGBB(res_data[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(uint Y=0;Y<res_h;Y++)
     {
      //--- Цикл по высоте изображения
      for(uint X=radius;X<res_w-radius;X++)
        {
         XY=Y*res_w+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*res_w+x;
         a_h_data[XY]=a_h_data[Y*res_w+radius];
         r_h_data[XY]=r_h_data[Y*res_w+radius];
         g_h_data[XY]=g_h_data[Y*res_w+radius];
         b_h_data[XY]=b_h_data[Y*res_w+radius];
        }
      //--- Удаляем артефакты размытия справа методом копирования соседних пикселей
      for(uint x=res_w-radius;x<res_w;x++)
        {
         XY=Y*res_w+x;
         a_h_data[XY]=a_h_data[(Y+1)*res_w-radius-1];
         r_h_data[XY]=r_h_data[(Y+1)*res_w-radius-1];
         g_h_data[XY]=g_h_data[(Y+1)*res_w-radius-1];
         b_h_data[XY]=b_h_data[(Y+1)*res_w-radius-1];
        }
     }

//--- Размываем вертикально (по оси Y) уже размытое горизонтально изображение
   int dxdy=0;
   //--- Цикл по высоте изображения
   for(uint X=0;X<res_w;X++)
     {
      //--- Цикл по ширине изображения
      for(uint Y=radius;Y<res_h-radius;Y++)
        {
         XY=Y*res_w+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)res_w;
            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*res_w+X;
         a_v_data[XY]=a_v_data[X+radius*res_w];
         r_v_data[XY]=r_v_data[X+radius*res_w];
         g_v_data[XY]=g_v_data[X+radius*res_w];
         b_v_data[XY]=b_v_data[X+radius*res_w];
        }
      //--- Удаляем артефакты размытия снизу методом копирования соседних пикселей
      for(uint y=res_h-radius;y<res_h;y++)
        {
         XY=y*res_w+X;
         a_v_data[XY]=a_v_data[X+(res_h-1-radius)*res_w];
         r_v_data[XY]=r_v_data[X+(res_h-1-radius)*res_w];
         g_v_data[XY]=g_v_data[X+(res_h-1-radius)*res_w];
         b_v_data[XY]=b_v_data[X+(res_h-1-radius)*res_w];
        }
     }
     
//--- Записываем в массив данных графического ресурса дважды размытые (горизонтально и вертикально) пиксели изображения
   for(int i=0;i<size;i++)
      res_data[i]=ARGB(a_v_data[i],r_v_data[i],g_v_data[i],b_v_data[i]);
//--- Выводим пиксели изображения на канвас в цикле по высоте и ширине изображения из массива данных графического ресурса
   for(uint X=0;X<res_w;X++)
     {
      for(uint Y=radius;Y<res_h-radius;Y++)
        {
         XY=Y*res_w+X;
         CGCnvElement::GetCanvasObj().PixelSet(X,Y,res_data[XY]);
        }
     }
//--- Готово
   return true;
  }
//+------------------------------------------------------------------+

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

Метод, возвращающий массив весовых коэффициентов:

//+------------------------------------------------------------------+
//| Возвращает массив весовых коэффициентов                          |
//| https://www.mql5.com/ru/articles/1612#chapter3_2                 |
//+------------------------------------------------------------------+
bool CShadowObj::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;
  }
//+------------------------------------------------------------------+

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

На этом создание первой версии класса объекта тени завершено.

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

Откроем файл \MQL5\Include\DoEasy\Objects\Graph\Form.mqh класса объекта-формы и внесём в него необходимые доработки.

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

//+------------------------------------------------------------------+
//|                                                         Form.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 "ShadowObj.mqh"
//+------------------------------------------------------------------+
//| Класс объекта "форма"                                            |
//+------------------------------------------------------------------+

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

   color             m_color_shadow;                           // Цвет тени формы

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

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

//--- Инициализирует переменные
   void              Initialize(void);
//--- Возвращает имя зависимого объекта
   string            CreateNameDependentObject(const string base_name)  const
                       { return ::StringSubstr(this.NameObj(),::StringLen(::MQLInfoString(MQL_PROGRAM_NAME))+1)+"_"+base_name;   }
   
//--- Создаёт новый графический объект

В прошлой статье, при описании объекта-формы, мы уже делали такое создание имени для объекта:

... извлекаем из имени объекта его окончание (имя состоит из имени программы и имени объекта при его создании). Нам нужно извлечь имя объекта при его создании и добавить к нему имя, переданное в метод.
Таким образом из, например, такого имени "Имя_программы_Форма01" мы извлекаем подстроку "Форма01" и добавляем к этой строке имя, переданное в метод. Если мы создаём объект тени и передаём имя "Тень", то имя объекта получится "Форма01_ Тень ", а окончательное имя созданного объекта будет таким: "Имя_программы_Форма01_Тень" ...

Теперь же это будет делаться в отдельном методе, так как потребуется не единожды.

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

//--- Создаёт новый графический объект
   CGCnvElement     *CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                      const int element_num,
                                      const string name,
                                      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              CreateShadowObj(const color colour,const uchar opacity);
   
public:

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

//--- Создаёт новый присоединённый элемент
   bool              CreateNewElement(const int element_num,
                                      const string name,
                                      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              CreateShadow(const uchar opacity);
//--- Рисует тень объекта

Теперь этот метод не будет доступен публично, и в него дополнительно будет передаваться цвет тени наряду с её непрозрачностью.

Публичный метод, рисующий тень объекта теперь тоже будет иметь больше аргументов:

//--- Создаёт новый присоединённый элемент
   bool              CreateNewElement(const int element_num,
                                      const string name,
                                      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              DrawShadow(const int shift_x,const int shift_y,const color colour,const uchar opacity=127,const uchar blur=4);

//--- Рисует рамку формы

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

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

//+------------------------------------------------------------------+
//| Методы упрощённого доступа к свойствам объекта                   |
//+------------------------------------------------------------------+
//--- (1) Устанавливает, (2) возвращает цвет рамки формы
   void              SetColorFrame(const color colour)                        { this.m_color_frame=colour;  }
   color             ColorFrame(void)                                   const { return this.m_color_frame;  }
//--- (1) Устанавливает, (2) возвращает цвет тени формы
   void              SetColorShadow(const color colour)                       { this.m_color_shadow=colour; }
   color             ColorShadow(void)                                  const { return this.m_color_shadow; }

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

//+------------------------------------------------------------------+
//| Методы упрощённого доступа к свойствам объекта                   |
//+------------------------------------------------------------------+
//--- (1) Устанавливает, (2) возвращает цвет рамки формы
   void              SetColorFrame(const color colour)                        { this.m_color_frame=colour;     }
   color             ColorFrame(void)                                   const { return this.m_color_frame;     }
//--- (1) Устанавливает, (2) возвращает цвет тени формы
   void              SetColorShadow(const color colour);
   color             ColorShadow(void) const;
//--- (1) Устанавливает, (2) возвращает непрозрачность тени формы
   void              SetOpacityShadow(const uchar opacity);
   uchar             OpacityShadow(void) const;

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

В методе создания нового графического элемента заменим эти строки

   int pos=::StringLen(::MQLInfoString(MQL_PROGRAM_NAME));
   string pref=::StringSubstr(NameObj(),pos+1);
   string name=pref+"_"+obj_name;

вызовом метода создания имени зависимого объекта:

//+------------------------------------------------------------------+
//| Создаёт новый графический объект                                 |
//+------------------------------------------------------------------+
CGCnvElement *CForm::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                      const int obj_num,
                                      const string obj_name,
                                      const int x,
                                      const int y,
                                      const int w,
                                      const int h,
                                      const color colour,
                                      const uchar opacity,
                                      const bool movable,
                                      const bool activity)
  {
   string name=this.CreateNameDependentObject(obj_name);
   CGCnvElement *element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity);
   if(element==NULL)
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name);
   return element;
  }
//+------------------------------------------------------------------+

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

Для этого немного доработаем метод создания объекта-тени:

//+------------------------------------------------------------------+
//| Создаёт объект тени                                              |
//+------------------------------------------------------------------+
void CForm::CreateShadowObj(const color colour,const uchar opacity)
  {
//--- Если флаг тени выключен, или объект тени уже существует - уходим
   if(!this.m_shadow || this.m_shadow_obj!=NULL)
      return;
//--- Рассчитываем координаты объекта тени в соответствии с отступом сверху и слева
   int x=this.CoordX()-OUTER_AREA_SIZE;
   int y=this.CoordY()-OUTER_AREA_SIZE;
//--- Рассчитываем ширину и высоту в соответствии с отступом сверху, снизу, слева и справа
   int w=this.Width()+OUTER_AREA_SIZE*2;
   int h=this.Height()+OUTER_AREA_SIZE*2;
//--- Создаём новый объект тени и записываем указатель на него в переменную
   this.m_shadow_obj=new CShadowObj(this.ChartID(),this.SubWindow(),this.CreateNameDependentObject("Shadow"),x,y,w,h);
   if(this.m_shadow_obj==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_FAILED_CREATE_SHADOW_OBJ));
      return;
     }
//--- Устанавливаем свойства созданному объекту-тени
   this.m_shadow_obj.SetID(this.ID());
   this.m_shadow_obj.SetNumber(-1);
   this.m_shadow_obj.SetOpacityShadow(opacity);
   this.m_shadow_obj.SetColorShadow(colour);
   this.m_shadow_obj.SetMovable(true);
   this.m_shadow_obj.SetActive(false);
   this.m_shadow_obj.SetVisible(false);
//--- Объект-форму перемещаем на передний план
   this.BringToTop();
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Рисует тень                                                      |
//+------------------------------------------------------------------+
void CForm::DrawShadow(const int shift_x,const int shift_y,const color colour,const uchar opacity=127,const uchar blur=4)
  {
//--- Если флаг тени выключен - уходим
   if(!this.m_shadow)
      return;
//--- Если объект тени не создан - создадим его
   if(this.m_shadow_obj==NULL)
      this.CreateShadowObj(colour,opacity);
//--- Если объект тени существует, рисуем на нём тень,
//--- устанавливаем флаг видимости объекта-тени и
//--- перемещаем объект-форму на передний план
   if(this.m_shadow_obj!=NULL)
     {
      this.m_shadow_obj.DrawShadow(shift_x,shift_y,blur);
      this.m_shadow_obj.SetVisible(true);
      this.BringToTop();
     }
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Устанавливает цветовую схему                                     |
//+------------------------------------------------------------------+
void CForm::SetColorTheme(const ENUM_COLOR_THEMES theme,const uchar opacity)
  {
   this.SetOpacity(opacity);
   this.SetColorBackground(array_color_themes[theme][COLOR_THEME_COLOR_FORM_BG]);
   this.SetColorFrame(array_color_themes[theme][COLOR_THEME_COLOR_FORM_FRAME]);
   if(this.m_shadow && this.m_shadow_obj!=NULL)
      this.SetColorShadow(array_color_themes[theme][COLOR_THEME_COLOR_FORM_SHADOW]);
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Устанавливает стиль формы                                        |
//+------------------------------------------------------------------+
void CForm::SetFormStyle(const ENUM_FORM_STYLE style,
                         const ENUM_COLOR_THEMES theme,
                         const uchar opacity,
                         const bool shadow=false,
                         const bool use_bg_color=true,
                         const bool redraw=false)
  {
//--- Устанавливаем параметры непрозрачности и размера сторон рамки формы
   this.m_shadow=shadow;
   this.m_frame_width_top=array_form_style[style][FORM_STYLE_FRAME_WIDTH_TOP];
   this.m_frame_width_bottom=array_form_style[style][FORM_STYLE_FRAME_WIDTH_BOTTOM];
   this.m_frame_width_left=array_form_style[style][FORM_STYLE_FRAME_WIDTH_LEFT];
   this.m_frame_width_right=array_form_style[style][FORM_STYLE_FRAME_WIDTH_RIGHT];
   
//--- Создаём объект тени
   this.CreateShadowObj(clrNONE,(uchar)array_form_style[style][FORM_STYLE_FRAME_SHADOW_OPACITY]);
   
//--- Устанавливаем цветовую схему
   this.SetColorTheme(theme,opacity);
//--- Рассчитываем цвет тени с затемнением цвета
   color clr=array_color_themes[theme][COLOR_THEME_COLOR_FORM_SHADOW];
   color gray=CGCnvElement::ChangeColorSaturation(ChartColorBackground(),-100);
   color color_shadow=CGCnvElement::ChangeColorLightness((use_bg_color ? gray : clr),-fabs(array_form_style[style][FORM_STYLE_DARKENING_COLOR_FOR_SHADOW]));
   this.SetColorShadow(color_shadow);
   
//--- Рисуем прямоугольную тень
   int shift_x=array_form_style[style][FORM_STYLE_FRAME_SHADOW_X_SHIFT];
   int shift_y=array_form_style[style][FORM_STYLE_FRAME_SHADOW_Y_SHIFT];
   this.DrawShadow(shift_x,shift_y,color_shadow,this.OpacityShadow(),(uchar)array_form_style[style][FORM_STYLE_FRAME_SHADOW_BLUR]);
   
//--- Заполняем фон формы цветом и непрозрачностью
   this.Erase(this.ColorBackground(),this.Opacity());
//--- В зависимости от выбранного стиля формы рисуем соответствующую рамку формы и внешнюю очерчивающую рамку
   switch(style)
     {
      case FORM_STYLE_BEVEL   :
        this.DrawFormFrame(this.m_frame_width_top,this.m_frame_width_bottom,this.m_frame_width_left,this.m_frame_width_right,this.ColorFrame(),this.Opacity(),FRAME_STYLE_BEVEL);

        break;
      //---FORM_STYLE_FLAT
      default:
        this.DrawFormFrame(this.m_frame_width_top,this.m_frame_width_bottom,this.m_frame_width_left,this.m_frame_width_right,this.ColorFrame(),this.Opacity(),FRAME_STYLE_FLAT);

        break;
     }
   this.DrawRectangle(0,0,Width()-1,Height()-1,array_color_themes[theme][COLOR_THEME_COLOR_FORM_RECT_OUTER],this.Opacity());
  }
//+------------------------------------------------------------------+

Логика метода расписана в комментариях. Вкратце: сначала создаём объект тени. После установки цветовой палитры рассчитываем нужный цвет для рисования тени. Если флаг использования цвета фона установлен, то для рисования тени будем использовать цвет фона графика, преобразованный в монохромный и затемнённый на величину параметра затемнения, записанную в стиле формы в файле GraphINI.mqh. Если флаг не установлен, то используем затемнённый тем же способом цвет, установленный в цветовых схемах форм в файле GraphINI.mqh. Далее вызываем метод рисования тени, который нарисует тень только в том случае, если установлен флаг использования тени у объекта-формы.

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

      //--- Затемняем горизонтальные стороны рамки
      for(int i=0;i<width;i++)
        {
         this.m_canvas.PixelSet(x+i,y,CGCnvElement::ChangeColorLightness(this.GetPixel(x+i,y),-0.05));
         this.m_canvas.PixelSet(x+i,y+height-1,CGCnvElement::ChangeColorLightness(this.GetPixel(x+i,y+height-1),-0.07));
        }

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

      //--- Затемняем горизонтальные стороны рамки
      for(int i=0;i<width;i++)
        {
         this.m_canvas.PixelSet(x+i,y,CGCnvElement::ChangeColorLightness(this.GetPixel(x+i,y),-5));
         this.m_canvas.PixelSet(x+i,y+height-1,CGCnvElement::ChangeColorLightness(this.GetPixel(x+i,y+height-1),-7));
        }

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

Метод, устанавливающий цвет тени формы:

//+------------------------------------------------------------------+
//| Устанавливает цвет тени формы                                    |
//+------------------------------------------------------------------+
void CForm::SetColorShadow(const color colour)
  {
   if(this.m_shadow_obj==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT));
      return;
     }
   this.m_shadow_obj.SetColorShadow(colour);
  }
//+------------------------------------------------------------------+

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

Метод, возвращающий цвет тени формы:

//+------------------------------------------------------------------+
//| Возвращает цвет тени формы                                       |
//+------------------------------------------------------------------+
color CForm::ColorShadow(void) const
  {
   if(this.m_shadow_obj==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT));
      return clrNONE;
     }
   return this.m_shadow_obj.ColorShadow();
  }
//+------------------------------------------------------------------+

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

Методы установки и возврата непрозрачности тени:

//+------------------------------------------------------------------+
//| Устанавливает непрозрачность тени формы                          |
//+------------------------------------------------------------------+
void CForm::SetOpacityShadow(const uchar opacity)
  {
   if(this.m_shadow_obj==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT));
      return;
     }
   this.m_shadow_obj.SetOpacityShadow(opacity);
  }
//+------------------------------------------------------------------+
//| Возвращает непрозрачность тени формы                             |
//+------------------------------------------------------------------+
uchar CForm::OpacityShadow(void) const
  {
   if(this.m_shadow_obj==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT));
      return 0;
     }
   return this.m_shadow_obj.OpacityShadow();
  }
//+------------------------------------------------------------------+

Эти методы идентичны по своей логике двум вышерассмотренным.

У нас всё готово для тестирования создания объекта-тени для форм.


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

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

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

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

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart77.mq5 |
//|                                  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"
//--- includes
#include <Arrays\ArrayObj.mqh>
#include <DoEasy\Services\Select.mqh>
#include <DoEasy\Objects\Graph\Form.mqh>
//--- defines
#define        FORMS_TOTAL (3)   // Количество создаваемых форм
//--- input parameters
sinput   bool              InpMovable     =  true;          // Movable forms flag
sinput   ENUM_INPUT_YES_NO InpUseColorBG  =  INPUT_YES;     // Use chart background color to calculate shadow color
sinput   color             InpColorForm3  =  clrCadetBlue;  // Third form shadow color (if not background color) 
//--- global variables
CArrayObj      list_forms;
color          array_clr[];
//+------------------------------------------------------------------+

В обработчике OnInit() допишем создание третьего объекта-формы:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Установка разрешений на отсылку событий перемещения курсора и прокрутки колёсика мышки
   ChartSetInteger(ChartID(),CHART_EVENT_MOUSE_MOVE,true);
   ChartSetInteger(ChartID(),CHART_EVENT_MOUSE_WHEEL,true);
//--- Установка глобальных переменных советника
   ArrayResize(array_clr,2);
   array_clr[0]=C'26,100,128';      // Исходный ≈Тёмно-лазурный цвет
   array_clr[1]=C'35,133,169';      // Осветлённый исходный цвет
//--- Создадим заданное количество объектов-форм
   list_forms.Clear();
   int total=FORMS_TOTAL;
   for(int i=0;i<total;i++)
     {
      //--- При создании объекта передаём в него все требуемые параметры
      CForm *form=new CForm("Form_0"+(string)(i+1),300,40+(i*80),100,(i<2 ? 70 : 30));
      if(form==NULL)
         continue;
      //--- Установим форме флаги активности, и перемещаемости
      form.SetActive(true);
      form.SetMovable(false);
      //--- Установим форме её идентификатор, равный индексу цикла и номер в списке объектов
      form.SetID(i);
      form.SetNumber(0);   // (0 - означает главный объект-форма) К главному объекту могут прикрепляться второстепенные, которыми он будет управлять
      //--- Установим частичную непрозрачность для средней формы и полную - для остальных
      uchar opacity=(i==1 ? 250 : 255);
      //--- Указываем стиль формы и её цветовую тему в зависимости от индекса цикла
      if(i<2)
        {
         ENUM_FORM_STYLE style=(ENUM_FORM_STYLE)i;
         ENUM_COLOR_THEMES theme=(ENUM_COLOR_THEMES)i;
         //--- Устанавливаем форме её стиль и тему
         form.SetFormStyle(style,theme,opacity,true,false);
        }
      //--- Если это первая (верхняя) форма
      if(i==0)
        {
         //--- Нарисуем на ней вдавленное поле, немного смещённое от центра формы книзу
         form.DrawFieldStamp(3,10,form.Width()-6,form.Height()-13,form.ColorBackground(),form.Opacity());
         form.Update(true);
        }
      //--- Если это вторая (средняя) форма
      if(i==1)
        {
         //--- Нарисуем на ней вдавленное полупрозрачное поле по центру в виде "затемнённого стекла"
         form.DrawFieldStamp(10,10,form.Width()-20,form.Height()-20,clrWheat,200);
         form.Update(true);
        }
      //--- Если это третья (нижняя) форма
      if(i==2)
        {
         //--- Установим непрозрачность 200
         form.SetOpacity(200);
         //--- Цвет фона формы зададим как первый цвет из массива цветов
         form.SetColorBackground(array_clr[0]);
         //--- Цвет очерчивающей рамки формы
         form.SetColorFrame(clrDarkBlue);
         //--- Установим флаг рисования тени
         form.SetShadow(true);
         //--- Рассчитаем цвет тени как цвет фона графика, преобразованный в монохромный
         color clrS=form.ChangeColorSaturation(form.ColorBackground(),-100);
         //--- Если в настройках задано использовать цвет фона графика, то заемним монохромный цвет на 20 единиц
         //--- Иначе - будем использовать для рисования тени заданный в настройках цвет
         color clr=(InpUseColorBG ? form.ChangeColorLightness(clrS,255,-20) : InpColorForm3);
         //--- Нарисуем тень формы со смещением от формы вправо-вниз на три пикселя по всем осям
         //--- Непрозрачность тени при этом установим равной 200, а радиус размытия равный 4
         form.DrawShadow(3,3,clr,200,4);
         //--- Зальём фон формы вертикальным градиентом
         form.Erase(array_clr,form.Opacity());
         //--- Нарисуем очерчивающий прямоугольник по краям формы
         form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,form.ColorFrame(),form.Opacity());
         //--- Выведем текст с описанием типа градиента и обновим форму
         form.Text(form.Width()/2,form.Height()/2,TextByLanguage("V-Градиент","V-Gradient"),C'211,233,149',255,TEXT_ANCHOR_CENTER);
         form.Update(true);
        }
      //--- Добавим объекты в список
      if(!list_forms.Add(form))
        {
         delete form;
         continue;
        }
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

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

В обработчике OnChartEvent() впишем вывод в журнал имени графического объекта при щелчке по нему:

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Если щелчок по объекту
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      Print(sparam);
     }
  }
//+------------------------------------------------------------------+

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


К сожалению, GIF-изображение не позволяет увидеть всю палитру цветов.

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


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

TestDoEasyPart77_Form_01
TestDoEasyPart77_Form_02
TestDoEasyPart77_Form_03

Это говорит нам о том, что объект-тень после его создания из объекта-формы всё равно перемещается на задний план, чтобы "не мешать" работе с формой, его создавшей.

Что дальше

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

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

К содержанию

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

Графика в библиотеке DoEasy (Часть 73): Объект-форма графического элемента
Графика в библиотеке DoEasy (Часть 74): Базовый графический элемент на основе класса CCanvas
Графика в библиотеке DoEasy (Часть 75): Методы работы с примитивами и текстом в базовом графическом элементе
Графика в библиотеке DoEasy (Часть 76): Объект Форма и предопределённые цветовые темы

Прикрепленные файлы |
MQL5.zip (4033.71 KB)
Графика в библиотеке DoEasy (Часть 78): Принципы анимации в библиотеке. Нарезка изображений Графика в библиотеке DoEasy (Часть 78): Принципы анимации в библиотеке. Нарезка изображений
В статье определим принципы анимации, которые будем использовать в некоторых частях библиотеки, разработаем класс для копирования части изображения и вставки его в указанное место объекта-формы с сохранением и восстановлением той части фона формы, на которую будет накладываться рисунок.
Комбинаторика и теория вероятностей для трейдинга (Часть II): Универсальный фрактал Комбинаторика и теория вероятностей для трейдинга (Часть II): Универсальный фрактал
В данной статье я продолжаю изучать фракталы и очень большое внимание будет уделено обобщению всего материала. А именно, я постараюсь свести все наработок в нечто более компактное и понятное для практического применения в трейдинге.
Кластерный анализ (Часть I): Использование наклона индикаторных линий Кластерный анализ (Часть I): Использование наклона индикаторных линий
Кластерный анализ — один из важнейших элементов искусственного интеллекта. В этой статье я пытаюсь применить кластерный анализ наклона индикатора, чтобы получить пороговые значения для определения флэтового или трендового характера рынка.
Графика в библиотеке DoEasy (Часть 76): Объект Форма и предопределённые цветовые темы Графика в библиотеке DoEasy (Часть 76): Объект Форма и предопределённые цветовые темы
В статье опишем концепцию построения различных тем оформления GUI в библиотеке, создадим объект "Форма", являющийся потомком объекта класса графического элемента, подготовим данные для создания теней графических объектов библиотеки и дальнейшего развития функционала.