English 中文 Español Deutsch 日本語 Português
Графика в библиотеке DoEasy (Часть 78): Принципы анимации в библиотеке. Нарезка изображений

Графика в библиотеке DoEasy (Часть 78): Принципы анимации в библиотеке. Нарезка изображений

MetaTrader 5Примеры | 2 июля 2021, 15:58
2 872 0
Artyom Trishkin
Artyom Trishkin

Содержание


Концепция

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

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


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

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

  1. Сохранение фона в нужных координатах
  2. Вывод изображения в эти координаты
  3. При перерисовке изображения — восстановление фона (который затрёт нарисованное изображение)

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

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

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

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


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

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

//--- 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,        // Изменён символ и таймфрейм графика

//--- CGCnvElement
   MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY,                  // Ошибка! Пустой массив
   
//--- CForm
   MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT,// Отсутствует объект тени. Необходимо сначала его создать при помощи метода CreateShadowObj()
   MSG_FORM_OBJECT_ERR_FAILED_CREATE_SHADOW_OBJ,      // Не удалось создать новый объект для тени
   MSG_FORM_OBJECT_ERR_FAILED_CREATE_PC_OBJ,          // Не удалось создать новый объект-копировщик пикселей
   MSG_FORM_OBJECT_PC_OBJ_ALREADY_IN_LIST,            // Уже есть в списке объект-копировщик пикселей с идентификатором 
   MSG_FORM_OBJECT_PC_OBJ_NOT_EXIST_LIST,             // Нет в списке объекта-копировщика пикселей с идентификатором 

//--- CShadowObj
   MSG_SHADOW_OBJ_IMG_SMALL_BLUR_LARGE,               // Ошибка! Размер изображения очень маленький или очень большое размытие
   
  };
//+------------------------------------------------------------------+

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

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

//--- CGCnvElement
   {"Ошибка! Пустой массив","Error! Empty array"},

//--- CForm
   {"Отсутствует объект тени. Необходимо сначала его создать при помощи метода CreateShadowObj()","There is no shadow object. You must first create it using the CreateShadowObj () method"},
   {"Не удалось создать новый объект для тени","Failed to create new object for shadow"},
   {"Не удалось создать новый объект-копировщик пикселей","Failed to create new pixel copier object"},
   {"В списке уже есть объект-копировщик пикселей с идентификатором ","There is already a pixel copier object in the list with ID "},
   {"В списке нет объекта-копировщика пикселей с идентификатором ","No pixel copier object with ID "},
   
//--- CShadowObj
   {"Ошибка! Размер изображения очень маленький или очень большое размытие","Error! Image size is very small or very large blur"},
      
  };
//+---------------------------------------------------------------------+


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

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

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

//+------------------------------------------------------------------+
//| Класс объекта графического элемента                              |
//+------------------------------------------------------------------+
class CGCnvElement : public CGBaseObj
  {
protected:
   CCanvas           m_canvas;                                 // Объект класса CCanvas
   CPause            m_pause;                                  // Объект класса "Пауза"
   bool              m_shadow;                                 // Наличие тени
   color             m_chart_color_bg;                         // Цвет фона графика
   uint              m_data_array[];                           // Массив для хранения копии данных ресурса
//--- Возвращает положение курсора относительно (1) всего элемента, (2) активной зоны элемента
   bool              CursorInsideElement(const int x,const int y);
   bool              CursorInsideActiveArea(const int x,const int y);
//--- Создаёт (1) структуру объекта, (2) объект из структуры
   virtual bool      ObjectToStruct(void);
   virtual void      StructToObject(void);
   
//--- Сохраняет графический ресурс в массив
   bool              ResourceCopy(const string source);

private:

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

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

   long              m_long_prop[ORDER_PROP_INTEGER_TOTAL];    // Целочисленные свойства
   double            m_double_prop[ORDER_PROP_DOUBLE_TOTAL];   // Вещественные свойства
   string            m_string_prop[ORDER_PROP_STRING_TOTAL];   // Строковые свойства
   
   ENUM_TEXT_ANCHOR  m_text_anchor;                            // Текущее выравнивание текста
   int               m_text_x;                                 // Последняя координата X текста
   int               m_text_y;                                 // Последняя координата Y текста
   color             m_color_bg;                               // Цвет фона элемента
   uchar             m_opacity;                                // Непрозрачность элемента
   
//--- Возвращает индекс массива, по которому фактически расположено (1) double-свойство и (2) string-свойство ордера


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

//--- Возвращает флаг поддержания объектом данного свойства
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER property)          { return true;    }
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_DOUBLE property)           { return false;   }
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING property)           { return true;    }

//--- Возвращает себя
   CGCnvElement     *GetObject(void)                                                   { return &this;   }

//--- Сравнивает объекты CGCnvElement между собой по всем возможным свойствам (для сортировки списков по указанному свойству объекта)
   virtual int       Compare(const CObject *node,const int mode=0) const;
//--- Сравнивает объекты CGCnvElement между собой по всем свойствам (для поиска равных объектов)
   bool              IsEqual(CGCnvElement* compared_obj) const;

//--- (1) Сохраняет объект в файл, (2), загружает объект из файла
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   
//--- Создаёт элемент
   bool              Create(const long chart_id,
                            const int wnd_num,
                            const string name,
                            const int x,
                            const int y,
                            const int w,
                            const int h,
                            const color colour,
                            const uchar opacity,
                            const bool redraw=false);
                                
//--- Возвращает указатель на объект-канвас
   CCanvas          *GetCanvasObj(void)                                                { return &this.m_canvas;                        }
//--- Устанавливает частоту обновления канваса
   void              SetFrequency(const ulong value)                                   { this.m_pause.SetWaitingMSC(value);            }
//--- Обновляет координаты (сдвигает канвас)
   bool              Move(const int x,const int y,const bool redraw=false);

//--- Сохраняет изображение в массив
   bool              ImageCopy(const string source,uint &array[]);
   

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

В блок кода методов для работы с текстом добавим два метода для возврата координат X и Y последнего нарисованного текста:

//+------------------------------------------------------------------+
//| Методы работы с текстом                                          |
//+------------------------------------------------------------------+
//--- Возвращает (1) тип выравнивания (способ привязки), последнюю координату (2) X, (3) Y текста
   ENUM_TEXT_ANCHOR  TextAnchor(void)                       const { return this.m_text_anchor;                                         }
   int               TextLastX(void)                        const { return this.m_text_x;                                              }
   int               TextLastY(void)                        const { return this.m_text_y;                                              }
//--- Устанавливает текущий шрифт

Методы просто возвращают значения соответствующих переменных.

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

//--- Выводит текст текущим шрифтом
   void              Text(int         x,                          // Координата X точки привязки текста
                          int         y,                          // Координата Y точки привязки текста
                          string      text,                       // Текст для отображения
                          const color clr,                        // Цвет
                          const uchar opacity=255,                // Непрозрачность
                          uint        alignment=0)                // Способ привязки текста
                       { 
                        this.m_text_anchor=(ENUM_TEXT_ANCHOR)alignment;
                        this.m_text_x=x;
                        this.m_text_y=y;
                        this.m_canvas.TextOut(x,y,text,::ColorToARGB(clr,opacity),alignment);
                       }


У рисуемого текста могут быть девять точек привязки:


Например, если точка привязки текста находится справа-снизу (Right|Bottom), то это и будет начальной координатой XY. У нас же в библиотеке все начальные координаты соответствуют верхнему левому углу прямоугольника (Left|Top). И если мы сохраним изображение с начальными координатами текста, то текст будет находиться справа-снизу от сохраняемого изображения, что не даст нам правильно сохранить тот участок фона, на который будет наложен текст.

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

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

//--- Возвращает смещения координат относительно точки привязки текста
   void              TextGetShiftXY(const string text,            // Текст для расчёта размера его очерчивающего прямоугольника
                                    const ENUM_TEXT_ANCHOR anchor,// Точка привязки текста, относительно которой будут рассчитаны смещения
                                    int &shift_x,                 // Сюда будет записана X-координата верхнего левого угла прямоугольника
                                    int &shift_y);                // Сюда будет записана Y-координата верхнего левого угла прямоугольника

  };
//+------------------------------------------------------------------+
//| Параметрический конструктор                                      |
//+------------------------------------------------------------------+

Метод будет рассмотрен далее.

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

//+------------------------------------------------------------------+
//| Параметрический конструктор                                      |
//+------------------------------------------------------------------+
CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                           const int      element_id,
                           const int      element_num,
                           const long     chart_id,
                           const int      wnd_num,
                           const string   name,
                           const int      x,
                           const int      y,
                           const int      w,
                           const int      h,
                           const color    colour,
                           const uchar    opacity,
                           const bool     movable=true,
                           const bool     activity=true,
                           const bool     redraw=false) : m_shadow(false)
  {
   this.m_chart_color_bg=(color)::ChartGetInteger(chart_id,CHART_COLOR_BACKGROUND);
   this.m_name=this.m_name_prefix+name;
   this.m_chart_id=chart_id;
   this.m_subwindow=wnd_num;
   this.m_type=element_type;
   this.SetFont("Calibri",8);
   this.m_text_anchor=0;
   this.m_text_x=0;
   this.m_text_y=0;
   this.m_color_bg=colour;
   this.m_opacity=opacity;
   if(this.Create(chart_id,wnd_num,this.m_name,x,y,w,h,colour,opacity,redraw))
     {
      this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,this.m_canvas.ResourceName()); // Имя графического ресурса
      this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,CGBaseObj::ChartID());         // Идентификатор графика
      this.SetProperty(CANV_ELEMENT_PROP_WND_NUM,CGBaseObj::SubWindow());        // Номер подокна графика
      this.SetProperty(CANV_ELEMENT_PROP_NAME_OBJ,CGBaseObj::Name());            // Имя объекта-элемента
      this.SetProperty(CANV_ELEMENT_PROP_TYPE,element_type);                     // Тип графического элемента
      this.SetProperty(CANV_ELEMENT_PROP_ID,element_id);                         // Идентификатор элемента
      this.SetProperty(CANV_ELEMENT_PROP_NUM,element_num);                       // Номер элемента в списке
      this.SetProperty(CANV_ELEMENT_PROP_COORD_X,x);                             // X-координата элемента на графике
      this.SetProperty(CANV_ELEMENT_PROP_COORD_Y,y);                             // Y-координата элемента на графике
      this.SetProperty(CANV_ELEMENT_PROP_WIDTH,w);                               // Ширина элемента
      this.SetProperty(CANV_ELEMENT_PROP_HEIGHT,h);                              // Высота элемента
      this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_LEFT,0);                      // Отступ активной зоны от левого края элемента
      this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_TOP,0);                       // Отступ активной зоны от верхнего края элемента
      this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT,0);                     // Отступ активной зоны от правого края элемента
      this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM,0);                    // Отступ активной зоны от нижнего края элемента
      this.SetProperty(CANV_ELEMENT_PROP_MOVABLE,movable);                       // Флаг перемещаемости элемента
      this.SetProperty(CANV_ELEMENT_PROP_ACTIVE,activity);                       // Флаг активности элемента
      this.SetProperty(CANV_ELEMENT_PROP_RIGHT,this.RightEdge());                // Правая граница элемента
      this.SetProperty(CANV_ELEMENT_PROP_BOTTOM,this.BottomEdge());              // Нижняя граница элемента
      this.SetProperty(CANV_ELEMENT_PROP_COORD_ACT_X,this.ActiveAreaLeft());     // X-координата активной зоны элемента
      this.SetProperty(CANV_ELEMENT_PROP_COORD_ACT_Y,this.ActiveAreaTop());      // Y-координата активной зоны элемента
      this.SetProperty(CANV_ELEMENT_PROP_ACT_RIGHT,this.ActiveAreaRight());      // Правая граница активной зоны элемента
      this.SetProperty(CANV_ELEMENT_PROP_ACT_BOTTOM,this.ActiveAreaBottom());    // Нижняя граница активной зоны элемента
     }
   else
     {
      ::Print(CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.m_name);
     }
  }
//+------------------------------------------------------------------+

В защищённом конструкторе класса точно так же инициализируем эти переменные:

//+------------------------------------------------------------------+
//| Защищённый конструктор                                           |
//+------------------------------------------------------------------+
CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                           const long    chart_id,
                           const int     wnd_num,
                           const string  name,
                           const int     x,
                           const int     y,
                           const int     w,
                           const int     h) : m_shadow(false)
  {
   this.m_chart_color_bg=(color)::ChartGetInteger(chart_id,CHART_COLOR_BACKGROUND);
   this.m_name=this.m_name_prefix+name;
   this.m_chart_id=chart_id;
   this.m_subwindow=wnd_num;
   this.m_type=element_type;
   this.SetFont("Calibri",8);
   this.m_text_anchor=0;
   this.m_text_x=0;
   this.m_text_y=0;
   this.m_color_bg=NULL_COLOR;
   this.m_opacity=0;
   if(this.Create(chart_id,wnd_num,this.m_name,x,y,w,h,this.m_color_bg,this.m_opacity,false))
     {
      ...

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

Реализация метода, сохраняющего изображение в массив:

//+------------------------------------------------------------------+
//| Сохраняет изображение в массив                                   |
//+------------------------------------------------------------------+
bool CGCnvElement::ImageCopy(const string source,uint &array[])
  {
   ::ResetLastError();
   int w=0,h=0;
   if(!::ResourceReadImage(this.NameRes(),array,w,h))
     {
      CMessage::ToLog(source,MSG_LIB_SYS_FAILED_GET_DATA_GRAPH_RES,true);
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

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

При помощи функции ResourceReadImage() считываем в массив данные созданного классом CCanvas графического ресурса, содержащего в себе изображение формы. Если произошла ошибка чтения ресурса, выводим об этом сообщение и возвращаем false. Если всё прошло успешно, возвращаем true, а в переданный в метод массив будут записаны все пиксели изображения, хранящегося в ресурсе.

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

//+------------------------------------------------------------------+
//| Сохраняет графический ресурс в массив                            |
//+------------------------------------------------------------------+
bool CGCnvElement::ResourceCopy(const string source)
  {
   return this.ImageCopy(DFUN,this.m_data_array);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Возвращает смещения координат относительно точки привязки текста |
//+------------------------------------------------------------------+
void CGCnvElement::TextGetShiftXY(const string text,const ENUM_TEXT_ANCHOR anchor,int &shift_x,int &shift_y)
  {
   int tw=0,th=0;
   this.TextSize(text,tw,th);
   switch(anchor)
     {
      case TEXT_ANCHOR_LEFT_TOP :
        shift_x=0; shift_y=0;
        break;
      case TEXT_ANCHOR_LEFT_CENTER :
        shift_x=0; shift_y=-th/2;
        break;
      case TEXT_ANCHOR_LEFT_BOTTOM :
        shift_x=0; shift_y=-th;
        break;
      case TEXT_ANCHOR_CENTER_TOP :
        shift_x=-tw/2; shift_y=0;
        break;
      case TEXT_ANCHOR_CENTER :
        shift_x=-tw/2; shift_y=-th/2;
        break;
      case TEXT_ANCHOR_CENTER_BOTTOM :
        shift_x=-tw/2; shift_y=-th;
        break;
      case TEXT_ANCHOR_RIGHT_TOP :
        shift_x=-tw; shift_y=0;
        break;
      case TEXT_ANCHOR_RIGHT_CENTER :
        shift_x=-tw; shift_y=-th/2;
        break;
      case TEXT_ANCHOR_RIGHT_BOTTOM :
        shift_x=-tw; shift_y=-th;
        break;
      default:
        shift_x=0; shift_y=0;
        break;
     }
  }
//+------------------------------------------------------------------+

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

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

В файл \MQL5\Include\DoEasy\Objects\Graph\ShadowObj.mqh внесём доработки.

Из метода размытия по Гауссу удалим массив и ненужные переменные:

//+------------------------------------------------------------------+
//| Размытие по-Гауссу                                               |
//| 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

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

//--- Читаем данные графического ресурса, и если не получилось - возвращаем 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
//--- Читаем данные графического ресурса, и если не получилось - возвращаем false
   if(!CGCnvElement::ResourceCopy(DFUN))
      return false;

Во всём коде вместо удалённых переменных res_w и res_h будем использовать методы класса объекта-графического элемента Width() и Height(), а вместо массива res_data будем использовать массив m_data_array, который как раз и служит теперь для хранения в нём копии графического ресурса.

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

//+------------------------------------------------------------------+
//| Размытие по-Гауссу                                               |
//| https://www.mql5.com/ru/articles/1612#chapter4                   |
//+------------------------------------------------------------------+
bool CShadowObj::GaussianBlur(const uint radius)
  {
//---
   int n_nodes=(int)radius*2+1;
//--- Читаем данные графического ресурса, и если не получилось - возвращаем false
   if(!CGCnvElement::ResourceCopy(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_data_array);
//--- массивы для хранения компонент цвета 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_data_array[i]);
      r_h_data[i]=GETRGBR(this.m_data_array[i]);
      g_h_data[i]=GETRGBG(this.m_data_array[i]);
      b_h_data[i]=GETRGBB(this.m_data_array[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_data_array[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_data_array[XY]);
        }
     }
//--- Готово
   return true;
  }
//+------------------------------------------------------------------+

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


Класс для копирования и вставки частей изображения

Минимальным объектом в иерархии наследования, в котором мы сможем работать с анимацией, будет класс объекта-формы.
А так как класс для сохранения и восстановления части изображения будет небольшим, то мы разместим его прямо в файле класса объекта-формы \MQL5\Include\DoEasy\Objects\Graph\Form.mqh. Назовём класс копировщиком пикселей, что явно описывает его суть.

Каждый объект класса-копировщика пикселей будет иметь свой идентификатор, по которому можно будет определить, с каким рисунком работает данный объект, и обращаться к нужному объекту класса можно будет по его идентификатору так, чтобы раздельно работать с каждым анимированным объектом. Например, если нам нужно одновременно управлять и изменять три рисунка, два из которых — текст, а один — изображение, то при создании объекта-копировщика для каждого рисунка, достаточно присвоить им разные идентификаторы — текст1 = ID0, текст2 = ID1, изображение = ID2, и тогда в каждом из объектов будут храниться все остальные параметры для работы с ним, а именно:

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

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

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

//+------------------------------------------------------------------+
//|                                                         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"
//+------------------------------------------------------------------+
//| Класс копировщика пикселей                                       |
//+------------------------------------------------------------------+
class CPixelCopier : public CObject
  {
private:
   CGCnvElement     *m_element;                             // Указатель на графический элемент
   uint              m_array[];                             // Массив пикселей
   int               m_id;                                  // Идентификатор
   int               m_x;                                   // Координата X верхнего левого угла
   int               m_y;                                   // Координата Y верхнего левого угла
   int               m_w;                                   // Ширина копируемого изображения
   int               m_h;                                   // Высота копируемого изображения
   int               m_wr;                                  // Рассчитываемая ширина копируемого изображения
   int               m_hr;                                  // Рассчитываемая высота копируемого изображения
public:

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

public:
//--- Сравнивает объекты CPixelCopier между собой по указанному свойству (для сортировки списка по свойству объекта)
   virtual int       Compare(const CObject *node,const int mode=0) const
                       {
                        const CPixelCopier *obj_compared=node;
                        return(mode==0 ? (this.ID()>obj_compared.ID() ? 1 : this.ID()<obj_compared.ID() ? -1 : 0) : WRONG_VALUE);
                       }
   
//--- Установка свойств
   void              SetID(const int id)                       { this.m_id=id;      }
   void              SetCoordX(const int value)                { this.m_x=value;    }
   void              SetCoordY(const int value)                { this.m_y=value;    }
   void              SetWidth(const int value)                 { this.m_w=value;    }
   void              SetHeight(const int value)                { this.m_h=value;    }
//--- Получение свойств
   int               ID(void)                            const { return this.m_id;  }
   int               CoordX(void)                        const { return this.m_x;   }
   int               CoordY(void)                        const { return this.m_y;   }
   int               Width(void)                         const { return this.m_w;   }
   int               Height(void)                        const { return this.m_h;   }
   int               WidthReal(void)                     const { return this.m_wr;  }
   int               HeightReal(void)                    const { return this.m_hr;  }

//--- Копирует часть или всё изображение в массив
   bool              CopyImgDataToArray(const uint x_coord,const uint y_coord,uint width,uint height);
//--- Копирует часть или всё изображение из массива на канвас
   bool              CopyImgDataToCanvas(const int x_coord,const int y_coord);

//--- Конструкторы
                     CPixelCopier (void){;}
                     CPixelCopier (const int id,
                                   const int x,
                                   const int y,
                                   const int w,
                                   const int h,
                                   CGCnvElement *element) : m_id(id), m_x(x),m_y(y),m_w(w),m_wr(w),m_h(h),m_hr(h) { this.m_element=element; }
                    ~CPixelCopier (void){;}
  };
//+------------------------------------------------------------------+

Рассмотрим методы подробнее.

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

//--- Сравнивает объекты CPixelCopier между собой по указанному свойству (для сортировки списка по свойству объекта)
   virtual int       Compare(const CObject *node,const int mode=0) const
                       {
                        const CPixelCopier *obj_compared=node;
                        return(mode==0 ? (this.ID()>obj_compared.ID() ? 1 : this.ID()<obj_compared.ID() ? -1 : 0) : WRONG_VALUE);
                       }

Здесь всё стандартно, как и в других классах библиотеки. Если режим сравнения (mode) равен 0 (по умолчанию), то сравниваются идентификаторы двух объектов — текущего и того, указатель на который передан в метод. Если идентификатор текущего объекта больше сравниваемого, то возвращается 1, если меньше — возвращается -1, при равенстве возвращается 0. Во всех иных случаях (если mode != 0) — возвращается -1. Т.е. данный метод на данный момент может сравнивать только идентификаторы объектов.

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

CPixelCopier (const int id,
              const int x,
              const int y,
              const int w,
              const int h,
              CGCnvElement *element) : m_id(id), m_x(x),m_y(y),m_w(w),m_wr(w),m_h(h),m_hr(h) { this.m_element=element; }

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

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

//+------------------------------------------------------------------+
//| Копирует часть или всё изображение в массив                      |
//+------------------------------------------------------------------+
bool CPixelCopier::CopyImgDataToArray(const uint x_coord,const uint y_coord,uint width,uint height)
  {
//--- Присваиваем переменным значения координат, переданных в метод
   int x1=(int)x_coord;
   int y1=(int)y_coord;
//--- Если X-координата выходит за пределы формы справа, или Y-координата выходит за пределы снизу,
//--- то копировать нечего - копируемая область за пределами формы - возвращаем false
   if(x1>this.m_element.Width()-1 || y1>this.m_element.Height()-1)
      return false;
//--- Присваиваем переменным значения ширины и высоты копируемой области
//--- Если переданные ширина и высота равны нулю - присваиваем им значения ширины и высоты формы
   this.m_wr=int(width==0  ? this.m_element.Width()  : width);
   this.m_hr=int(height==0 ? this.m_element.Height() : height);
//--- Если координаты X и Y равны нулю (это верхний левый угол формы) и ширина и высота равны ширине и высоте формы,
//--- Значит копируемая область равна всей площади формы - копируем (с возвратом из метода) всю форму целиком при помощи метода ImageCopy()
   if(x1==0 && y1==0 && this.m_wr==this.m_element.Width() && this.m_hr==this.m_element.Height())
      return this.m_element.ImageCopy(DFUN,this.m_array);

//--- Рассчитываем правую координату X и нижнюю координату Y прямоугольной области
   int x2=int(x1+this.m_wr-1);
   int y2=int(y1+this.m_hr-1);
//--- Если рассчитанная координата X выхоит за пределы формы, то этой координатой будет правый край формы
   if(x2>=this.m_element.Width()-1)
      x2=this.m_element.Width()-1;
//--- Если рассчитанная координата Y выхоит за пределы формы, то этой координатой будет нижний край формы
   if(y2>=this.m_element.Height()-1)
      y2=this.m_element.Height()-1;
//--- Рассчитываем копируемые ширину и высоту
   this.m_wr=x2-x1+1;
   this.m_hr=y2-y1+1;
//--- Определяем необходимый размер массива, в который должны поместиться все пиксели изображения с рассчитанными шириной и высотой
   int size=this.m_wr*this.m_hr;
//--- Если размер массива установить не удалось - сообщаем об этом и возвращаем false
   if(::ArrayResize(this.m_array,size)!=size)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE,true);
      return false;
     }
//--- Устанавливаем индекс в массиве для записи пикселя изображения
   int n=0;
//--- В цикле по рассчитанной высоте копируемой области, начиная от указанной координаты Y
   for(int y=y1;y<y1+this.m_hr;y++)
     {
      //--- в цикле по рассчитанной ширине копируемой области, начиная от указанной координаты X
      for(int x=x1;x<x1+this.m_wr;x++)
        {
         //--- Копируем очередной пиксель изображения в массив и увеличиваем индекс массива
         this.m_array[n]=this.m_element.GetCanvasObj().PixelGet(x,y);
         n++;
        }
     }
//--- Успешно - возвращаем true
   return true;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Копирует часть или всё изображение из массива на канвас          |
//+------------------------------------------------------------------+
bool CPixelCopier::CopyImgDataToCanvas(const int x_coord,const int y_coord)
  {
//--- Если массив сохранённых пикселей пустой - сообщаем об этом и возвращаем false
   int size=::ArraySize(this.m_array);
   if(size==0)
     {
      CMessage::ToLog(DFUN,MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY,true);
      return false;
     }
//--- Устанавливаем индекс массива для чтения пикселя изображения
   int n=0;
//--- В цикле по ранее рассчитанной высоте скопированной области, начиная от указанной координаты Y
   for(int y=y_coord;y<y_coord+this.m_hr;y++)
     {
      //--- в цикле по ранее рассчитанной ширине скопированной области, начиная от указанной координаты X
      for(int x=x_coord;x<x_coord+this.m_wr;x++)
        {
         //--- Восстанавливаем очередной пиксель изображения из массива и увеличиваем индекс массива
         this.m_element.GetCanvasObj().PixelSet(x,y,this.m_array[n]);
         n++;
        }
     }
   return true;
  }
//+------------------------------------------------------------------+

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

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

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

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

//+------------------------------------------------------------------+
//| Класс объекта "форма"                                            |
//+------------------------------------------------------------------+
class CForm : public CGCnvElement
  {
private:
   CArrayObj         m_list_elements;                          // Список присоединённых элементов
   CArrayObj         m_list_pc_obj;                            // Список объектов-копировщиков пикселей
   CShadowObj       *m_shadow_obj;                             // Указатель на объект тени
   color             m_color_frame;                            // Цвет рамки формы
   int               m_frame_width_left;                       // Ширина рамки формы слева
   int               m_frame_width_right;                      // Ширина рамки формы справа
   int               m_frame_width_top;                        // Ширина рамки формы сверху
   int               m_frame_width_bottom;                     // Ширина рамки формы снизу

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

//--- Создаёт объект для тени
   void              CreateShadowObj(const color colour,const uchar opacity);
   
//--- Возвращает флаг присутствия в списке объекта-копировщика с указанным идентификатором
   bool              IsPresentPC(const int id);

public:

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

public:
   //--- Конструкторы
                     CForm(const long chart_id,
                           const int subwindow,
                           const string name,
                           const int x,
                           const int y,
                           const int w,
                           const int h);
                     CForm(const int subwindow,
                           const string name,
                           const int x,
                           const int y,
                           const int w,
                           const int h);
                     CForm(const string name,
                           const int x,
                           const int y,
                           const int w,
                           const int h);
                     CForm() { this.Initialize(); }
//--- Деструктор
                    ~CForm();
                           
//--- Поддерживаемые свойства формы (1) целочисленные, (2) строковые
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER property) { return true;                   }
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING property)  { return true;                   }
   
//--- Возвращает (1) себя, список (2) присоединённых объектов, (3) объектов-копировщиков пикселей, (4) объект тени
   CForm            *GetObject(void)                                          { return &this;                  }
   CArrayObj        *GetList(void)                                            { return &this.m_list_elements;  }
   CArrayObj        *GetListPC(void)                                          { return &this.m_list_pc_obj;    }
   CGCnvElement     *GetShadowObj(void)                                       { return this.m_shadow_obj;      }

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

//--- Создаёт новый объект-копировщик пикселей
   CPixelCopier     *CreateNewPixelCopier(const int id,const int x_coord,const int y_coord,const int width,const int height);

//--- Рисует тень объекта
   void              DrawShadow(const int shift_x,const int shift_y,const color colour,const uchar opacity=127,const uchar blur=4);

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

//+------------------------------------------------------------------+
//| Методы работы с пикселями изображения                            |
//+------------------------------------------------------------------+
//--- Возвращает объект-копировщик пикселей по идентификатору
   CPixelCopier     *GetPixelCopier(const int id);
//--- Копирует часть или всё изображение в массив
   bool              ImageCopy(const int id,const uint x_coord,const uint y_coord,uint &width,uint &height);
//--- Копирует часть или всё изображение из массива на канвас
   bool              ImagePaste(const int id,const uint x_coord,const uint y_coord);
   
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Возвращает флаг присутствия в списке                             |
//| объекта-копировщика с указанным идентификатором                  |
//+------------------------------------------------------------------+
bool CForm::IsPresentPC(const int id)
  {
   for(int i=0;i<this.m_list_pc_obj.Total();i++)
     {
      CPixelCopier *pc=this.m_list_pc_obj.At(i);
      if(pc==NULL)
         continue;
      if(pc.ID()==id)
         return true;
     }
   return false;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Создаёт новый объект-копировщик пикселей                         |
//+------------------------------------------------------------------+
CPixelCopier *CForm::CreateNewPixelCopier(const int id,const int x_coord,const int y_coord,const int width,const int height)
  {
//--- Если объект с таким идентификатором уже есть - сообщаем об этом в журнал и возвращаем NULL
   if(this.IsPresentPC(id))
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_PC_OBJ_ALREADY_IN_LIST),(string)id);
      return NULL;
     }
//--- Создаём новый объект-копировщик с указанными параметрами
   CPixelCopier *pc=new CPixelCopier(id,x_coord,y_coord,width,height,CGCnvElement::GetObject());
//--- Если объект создать не удалось - сообщаем об этом и возвращаем NULL
   if(pc==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_FAILED_CREATE_PC_OBJ));
      return NULL;
     }
//--- Если успешно созданный объект не удалось добавить в список - сообщаем об этом, удаляем объект и возвращаем NULL
   if(!this.m_list_pc_obj.Add(pc))
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST)," ID: ",id);
      delete pc;
      return NULL;
     }
//--- Возвращаем указатель на вновь созданный объект
   return pc;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Возвращает объект-копировщик пикселей по идентификатору          |
//+------------------------------------------------------------------+
CPixelCopier *CForm::GetPixelCopier(const int id)
  {
   for(int i=0;i<this.m_list_pc_obj.Total();i++)
     {
      CPixelCopier *pc=m_list_pc_obj.At(i);
      if(pc==NULL)
         continue;
      if(pc.ID()==id)
         return pc;
     }
   return NULL;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Копирует часть или всё изображение в массив                      |
//+------------------------------------------------------------------+
bool CForm::ImageCopy(const int id,const uint x_coord,const uint y_coord,uint &width,uint &height)
  {
   CPixelCopier *pc=this.GetPixelCopier(id);
   if(pc==NULL)
     {
      pc=this.CreateNewPixelCopier(id,x_coord,y_coord,width,height);
      if(pc==NULL)
         return false;
     }
   return pc.CopyImgDataToArray(x_coord,y_coord,width,height);
  }
//+------------------------------------------------------------------+

Здесь: получаем указатель на объект-копировщик по идентификатору. Если объект не найден — сообщаем об этом и возвращаем false. Если указатель на объект получен успешно, возвращаем результат работы метода CopyImgDataToArray() класса объекта-копировщика, рассмотренного нами выше.

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

//+------------------------------------------------------------------+
//| Копирует часть или всё изображение из массива на канвас          |
//+------------------------------------------------------------------+
bool CForm::ImagePaste(const int id,const uint x_coord,const uint y_coord)
  {
   CPixelCopier *pc=this.GetPixelCopier(id);
   if(pc==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_PC_OBJ_NOT_EXIST_LIST),(string)id);
      return false;
     }
   return pc.CopyImgDataToCanvas(x_coord,y_coord);
  }
//+------------------------------------------------------------------+

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

Для тестирования работы объекта-копировщика пикселей изображения у нас всё готово.


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

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

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

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

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

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart78.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 (4)   // Количество создаваемых форм
//--- 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() при создании форм будем рассчитывать координаты новой формы в зависимости от координат предыдущей. После создания каждой последующей формы вовсе не обязательно перерисовывать весь график целиком, поэтому удалим в указанных строчках передачу в методы обновления форм (ранее мы явно передавали значение true). Теперь это значение будем передавать в самом конце — после полного создания последней формыв новом блоке кода для создания четвёртой формы:

//+------------------------------------------------------------------+
//| 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++)
     {
      int y=40;
      if(i>0)
        {
         CForm *form_prev=list_forms.At(i-1);
         if(form_prev==NULL)
            continue;
         y=form_prev.BottomEdge()+10;
        }
      //--- При создании объекта передаём в него все требуемые параметры
      CForm *form=new CForm("Form_0"+(string)(i+1),300,y,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();
        }
      //--- Если это вторая форма
      if(i==1)
        {
         //--- Нарисуем на ней вдавленное полупрозрачное поле по центру в виде "затемнённого стекла"
         form.DrawFieldStamp(10,10,form.Width()-20,form.Height()-20,clrWheat,200);
         form.Update();
        }
      //--- Если это третья форма
      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,-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();
        }
      //--- Если это четвёртая (нижняя - тестируемая) форма
      if(i==3)
        {
         //--- Установим непрозрачность 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,-20) : InpColorForm3);
         //--- Нарисуем тень формы со смещением от формы вправо-вниз на три пикселя по всем осям
         //--- Непрозрачность тени при этом установим равной 200, а радиус размытия равный 4
         form.DrawShadow(3,3,clr,200,4);
         //--- Зальём фон формы горизонтальным градиентом
         form.Erase(array_clr,form.Opacity(),false);
         //--- Нарисуем очерчивающий прямоугольник по краям формы
         form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,form.ColorFrame(),form.Opacity());
         
         //--- Выведем текст с описанием типа градиента и обновим форму        
         //--- Зададим параметры текста - координаты текста в центре формы и точка привязки - тоже по центру
         string text=TextByLanguage("H-Градиент","H-Gradient");
         int text_x=form.Width()/2;
         int text_y=form.Height()/2;
         ENUM_TEXT_ANCHOR anchor=TEXT_ANCHOR_CENTER;
         
         //--- Узнаем ширину и высоту очерчивающего прямоугольника текста (это и будет размером сохраняемой области)
         int text_w=0,text_h=0;
         form.TextSize(text,text_w,text_h);
         //--- Рассчитаем смещения координат для сохраняемой области в зависимости от точки привязки текста
         int shift_x=0,shift_y=0;
         form.TextGetShiftXY(text,anchor,shift_x,shift_y);
         
         //--- Если область фона с рассчитанными координатами и размерами под будущим текстом успешно сохранена
         if(form.ImageCopy(0,text_x+shift_x,text_y+shift_y,text_w,text_h))
           {
            //--- Нарисуем текст и обновим форму вместе с перерисовкой чарта
            form.Text(text_x,text_y,text,C'211,233,149',255,anchor);
            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)
     {
      //--- Если объект, по которому был щелчок, принадлежит советнику
      if(StringFind(sparam,MQLInfoString(MQL_PROGRAM_NAME))==0)
        {
         //--- Получим из его имени идентификатор объекта
         int form_id=(int)StringToInteger(StringSubstr(sparam,StringLen(sparam)-1))-1;
         //--- Найдём этот объект-форму в цикле по всем созданным в советнике формам
         for(int i=0;i<list_forms.Total();i++)
           {
            CForm *form=list_forms.At(i);
            if(form==NULL)
               continue;
            //--- Если объект, по которому был щелчок, имеет идентификатор 3 и форма имеет такой же идентификатор
            if(form_id==3 && form.ID()==3)
              {
               //--- Зададим параметры текста 
               string text=TextByLanguage("H-Градиент","H-Gradient");
               //--- Получим размеры будущего текста
               int text_w=0,text_h=0;
               form.TextSize(text,text_w,text_h);
               //--- Возьмём точку привязки у последнего нарисованного текста (а в этой форме и был последний нарисованный текст)
               ENUM_TEXT_ANCHOR anchor=form.TextAnchor();
               //--- Получим координаты последнего нарисованного текста
               int text_x=form.TextLastX();
               int text_y=form.TextLastY();
               //--- Рассчитаем смещение координат сохраняемой прямоугольной области в зависимости от точки привязки текста
               int shift_x=0,shift_y=0;
               form.TextGetShiftXY(text,anchor,shift_x,shift_y);
               //--- Установим стартовую точку привязки текста (0 = LEFT_TOP) из девяти возможных
               static int n=0;
               //--- Если ранее скопированное изображение фона формы при создании объекта-формы в OnInit() успешно восстановлено
               if(form.ImagePaste(0,text_x+shift_x,text_y+shift_y))
                 {
                  //--- В зависимости от переменной n установим новую точку привязки текста
                  switch(n)
                    {
                     case 0 : anchor=TEXT_ANCHOR_LEFT_TOP;     text_x=1;               text_y=1;               break;
                     case 1 : anchor=TEXT_ANCHOR_CENTER_TOP;   text_x=form.Width()/2;  text_y=1;               break;
                     case 2 : anchor=TEXT_ANCHOR_RIGHT_TOP;    text_x=form.Width()-2;  text_y=1;               break;
                     case 3 : anchor=TEXT_ANCHOR_LEFT_CENTER;  text_x=1;               text_y=form.Height()/2; break;
                     case 4 : anchor=TEXT_ANCHOR_CENTER;       text_x=form.Width()/2;  text_y=form.Height()/2; break;
                     case 5 : anchor=TEXT_ANCHOR_RIGHT_CENTER; text_x=form.Width()-2;  text_y=form.Height()/2; break;
                     case 6 : anchor=TEXT_ANCHOR_LEFT_BOTTOM;  text_x=1;               text_y=form.Height()-2; break;
                     case 7 : anchor=TEXT_ANCHOR_CENTER_BOTTOM;text_x=form.Width()/2;  text_y=form.Height()-2; break;
                     case 8 : anchor=TEXT_ANCHOR_RIGHT_BOTTOM; text_x=form.Width()-2;  text_y=form.Height()-2; break;
                     default: anchor=TEXT_ANCHOR_CENTER;       text_x=form.Width()/2;  text_y=form.Height()/2; break;
                    }
                  //--- В соответствии с новой точкой привязки получим новые смещения координат сохраняемой области
                  form.TextGetShiftXY(text,anchor,shift_x,shift_y);
                  //--- Если область фона по новым координатам успешно сохранена
                  if(form.ImageCopy(0,text_x+shift_x,text_y+shift_y,text_w,text_h))
                    {
                     //--- Нарисуем текст в новых координатах и обновим форму
                     form.Text(text_x,text_y,text,C'211,233,149',255,anchor);
                     form.Update();
                    }
                  //--- Увеличим счётчик щелчков по объекту (и по совместительству - указатель на точку привязки текста),
                  //--- и если значение больше 8, то сбросим значение в ноль (от 0 до 8 = девять точек привязки)
                  n++;
                  if(n>8) n=0;
                 }
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Здесь всё достаточно подробно расписано в комментариях к коду. Любые вопросы можно задать в обсуждении к статье.

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

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



Что дальше

В следующей статье продолжим развитие концепции анимации в библиотеке и начнём работать над спрайтовой анимацией.

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

К содержанию

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

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

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