Графика в библиотеке DoEasy (Часть 81): Интегрируем графику в объекты библиотеки

Artyom Trishkin | 13 августа, 2021

Содержание

Концепция

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

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

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

Концепция на сегодня у нас будет такова:

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

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


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

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

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

Плюс, нам необходимо будет сделать методы для вывода описания объектов однотипными для всех ранее созданных объектов библиотеки. Это методы Print() и PrintShort(), выводящие полное и краткое описание свойств объекта. Сделаем эти методы виртуальными и объявим их в родительском классе всех объектов библиотеки CBaseObj. Чтобы виртуализация работала, нам необходимо во всех классах сделать абсолютно одинаковыми аргументы этих методов. А у нас на данный момент есть различные параметры у этих методов в разных классах. Нужно их привести к одному виду и подправить вызовы методов в соответствии с изменёнными параметрами в аргументах методов.

В классе CBaseObj в файле \MQL5\Include\DoEasy\Objects\BaseObj.mqh объявим два этих виртуальных метода с уже нужными параметрами:

//--- Возвращает тип объекта
   virtual int       Type(void)                                const { return this.m_type;                     }
//--- Выводит в журнал описание свойств объекта (full_prop=true - все свойства, false - только поддерживаемые - реализуется в наследниках класса)
   virtual void      Print(const bool full_prop=false,const bool dash=false)  { return;                        }
//--- Выводит в журнал краткое описание объекта
   virtual void      PrintShort(const bool dash=false,const bool symbol=false){ return;                        }
   
//--- Конструктор

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

Например, в классе COrder — базовом классе всей ордерной системы библиотеки — уже были внесены такие изменения:

//--- Возвращает тип по направлению ордера/позиции
   string            DirectionDescription(void) const;
//--- Выводит в журнал описание свойств объекта (full_prop=true - все свойства, false - только поддерживаемые - реализуется в наследниках класса)
   virtual void      Print(const bool full_prop=false,const bool dash=false);
//--- Выводит в журнал краткое описание объекта
   virtual void      PrintShort(const bool dash=false,const bool symbol=false);
//---
  };
//+------------------------------------------------------------------+

Здесь мы добавили ещё один аргумент в метод Print() и объявили метод PrintShort().

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

//+------------------------------------------------------------------+
//| Выводит в журнал свойства ордера                                 |
//+------------------------------------------------------------------+
void COrder::Print(const bool full_prop=false,const bool dash=false)
  {
   ::Print("============= ",CMessage::Text(MSG_LIB_PARAMS_LIST_BEG),": \"",this.StatusDescription(),"\" =============");
   int beg=0, end=ORDER_PROP_INTEGER_TOTAL;
   for(int i=beg; i<end; i++)
     {
      ENUM_ORDER_PROP_INTEGER prop=(ENUM_ORDER_PROP_INTEGER)i;
      if(!full_prop && !this.SupportProperty(prop)) continue;
      ::Print(this.GetPropertyDescription(prop));
     }
   ::Print("------");
   beg=end; end+=ORDER_PROP_DOUBLE_TOTAL;
   for(int i=beg; i<end; i++)
     {
      ENUM_ORDER_PROP_DOUBLE prop=(ENUM_ORDER_PROP_DOUBLE)i;
      if(!full_prop && !this.SupportProperty(prop)) continue;
      ::Print(this.GetPropertyDescription(prop));
     }
   ::Print("------");
   beg=end; end+=ORDER_PROP_STRING_TOTAL;
   for(int i=beg; i<end; i++)
     {
      ENUM_ORDER_PROP_STRING prop=(ENUM_ORDER_PROP_STRING)i;
      if(!full_prop && !this.SupportProperty(prop)) continue;
      ::Print(this.GetPropertyDescription(prop));
     }
   ::Print("================== ",CMessage::Text(MSG_LIB_PARAMS_LIST_END),": \"",this.StatusDescription(),"\" ==================\n");
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Выводит в журнал полное описание коллекции                       |
//+------------------------------------------------------------------+
void CMBookSeriesCollection::Print(const bool full_prop=false,const bool dash=false)
  {
   ::Print(CMessage::Text(MSG_MB_COLLECTION_TEXT_MBCOLLECTION),":");
   for(int i=0;i<this.m_list.Total();i++)
     {
      CMBookSeries *bookseries=this.m_list.At(i);
      if(bookseries==NULL)
         continue;
      bookseries.Print(false,true);
     }
  }
//+------------------------------------------------------------------+
//| Выводит в журнал краткое описание коллекции                      |
//+------------------------------------------------------------------+
void CMBookSeriesCollection::PrintShort(const bool dash=false,const bool symbol=false)
  {
   ::Print(CMessage::Text(MSG_MB_COLLECTION_TEXT_MBCOLLECTION),":");
   for(int i=0;i<this.m_list.Total();i++)
     {
      CMBookSeries *bookseries=this.m_list.At(i);
      if(bookseries==NULL)
         continue;
      bookseries.PrintShort(true);
     }
  }
//+------------------------------------------------------------------+

Ранее здесь был один параметр, и метод вызывался как bookseries.Print(true); Сейчас же у нас в методе Print() класса CMBookSeries добавился ещё один параметр перед нужным нам, поэтому сначала передаём false для добавленного параметра, а уже затем передаём true для необходимого — того, что ранее был при вызове метода.

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

BookSeriesCollection.mqh, ChartObjCollection.mqh, MQLSignalsCollection.mqh, TickSeriesCollection.mqh, TimeSeriesCollection.mqh.

Account.mqh, MarketBookOrd.mqh, MarketBookSnapshot.mqh, MBookSeries.mqh, ChartObj.mqh, ChartWnd.mqh, MQLSignal.mqh, Order.mqh.

Buffer.mqh, BufferArrow.mqh, BufferBars.mqh, BufferCalculate.mqh, BufferCandles.mqh, BufferFilling.mqh, BufferHistogram.mqh, BufferHistogram2.mqh, BufferLine.mqh, BufferSection.mqh, BufferZigZag.mqh, DataInd.mqh, IndicatorDE.mqh.

PendReqClose.mqh, PendReqModify.mqh, PendReqOpen.mqh, PendReqPlace.mqh, PendReqRemove.mqh, PendReqSLTP.mqh, PendRequest.mqh.

Bar.mqh, SeriesDE.mqh, TimeSeriesDE.mqh, DataTick.mqh, TickSeries.mqh.

Symbol.mqh, SymbolBonds.mqh, SymbolCFD.mqh, SymbolCollateral.mqh, SymbolCommodity.mqh, SymbolCommon.mqh, SymbolCrypto.mqh, SymbolCustom.mqh, SymbolExchange.mqh, SymbolFutures.mqh, SymbolFX.mqh, SymbolFXExotic.mqh, SymbolFXMajor.mqh, SymbolFXMinor.mqh, SymbolFXRub.mqh, SymbolIndex.mqh, SymbolIndicative.mqh, SymbolMetall.mqh, SymbolOption.mqh, SymbolStocks.mqh

BaseObj.mqh.

В некоторых классах библиотеки были заменены выводы сообщений посредством стандартной функции Print() на вывод сообщений методом ToLog() класса CMessage как, например, в этом методе класса коллекции событий:

//+------------------------------------------------------------------+
//| Выбирает из списка только рыночные отложенные ордера             |
//+------------------------------------------------------------------+
CArrayObj* CEventsCollection::GetListMarketPendings(CArrayObj* list)
  {
   if(list.Type()!=COLLECTION_MARKET_ID)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_ERROR_NOT_MARKET_LIST);
      return NULL;
     }
   CArrayObj* list_orders=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_MARKET_PENDING,EQUAL);
   return list_orders;
  }
//+------------------------------------------------------------------+

Ранее для вывода сообщения использовалась такая строка:

Print(DFUN,CMessage::Text(MSG_LIB_SYS_ERROR_NOT_MARKET_LIST));

Список файлов, в которых были внесены такие правки:

EventsCollection.mqh, HistoryCollection.mqh, TimeSeriesCollection.mqh.

Со всеми изменениями в этих классах можно ознакомиться в прикреплённых к статье файлах.

Если на графике есть уже созданный объект-форма, то его можно скрыть или показать, указав для него флаги отображения на указанных таймфреймах. Мы такую возможность используем для "перемещения" объекта на передний план — поверх всех остальных методом BringToTop().
Но у нас нет "говорящих" методов для показа/скрытия графических объектов.
Создадим для этого два виртуальных метода в классе графического элемента CGCnvElement в файле \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh:

//--- Устанавливает объект выше всех
   void              BringToTop(void)                          { CGBaseObj::SetVisible(false); CGBaseObj::SetVisible(true);            }
//--- (1) Показывает, (2) скрывает элемент
   virtual void      Show(void)                                { CGBaseObj::SetVisible(true);                                          }
   virtual void      Hide(void)                                { CGBaseObj::SetVisible(false);                                         }

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

Класс объекта-формы CForm является наследником объекта-графического элемента, и объект-форма может быть составным — состоять из нескольких объектов-графических элементов. Поэтому для него необходимо прописать свою реализацию этих методов.
Откроем файл \MQL5\Include\DoEasy\Objects\Graph\Form.mqh и в публичной секции объявим два виртуальных метода:

//+------------------------------------------------------------------+
//| Методы визуального оформления                                    |
//+------------------------------------------------------------------+
//--- (1) Показывает, (2) скрывает форму
   virtual void      Show(void);
   virtual void      Hide(void);

//+------------------------------------------------------------------+
//| Методы упрощённого доступа к свойствам объекта                   |
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Показывает форму                                                 |
//+------------------------------------------------------------------+
void CForm::Show(void)
  {
//--- Если у объекта есть тень - отобразим её
   if(this.m_shadow_obj!=NULL)
      this.m_shadow_obj.Show();
//--- Отобразим главную форму
   CGCnvElement::Show();
//--- В цикле по всем привязанным графическим объектам
   for(int i=0;i<this.m_list_elements.Total();i++)
     {
      //--- получим очередной графический элемент
      CGCnvElement *elment=this.m_list_elements.At(i);
      if(elment==NULL)
         continue;
      //--- и отобразим его
      elment.Show();
     }
//--- Обновим форму
   CGCnvElement::Update();
  }
//+------------------------------------------------------------------+
//| Скрывает форму                                                   |
//+------------------------------------------------------------------+
void CForm::Hide(void)
  {
//--- Если у объекта есть тень - скроем её
   if(this.m_shadow_obj!=NULL)
      this.m_shadow_obj.Hide();
//--- В цикле по всем привязанным графическим объектам
   for(int i=0;i<this.m_list_elements.Total();i++)
     {
      //--- получим очередной графический элемент
      CGCnvElement *elment=this.m_list_elements.At(i);
      if(elment==NULL)
         continue;
      //--- и скроем его
      elment.Hide();
     }
//--- Скроем главную форму и обновим объект
   CGCnvElement::Hide();
   CGCnvElement::Update();
  }
//+------------------------------------------------------------------+

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

Теперь можно приступать к интеграции графических объектов в объекты библиотеки.

Класс управления графическими объектами

Каким же образом мы можем наделить каждый объект библиотеки возможностью создавать для себя графические объекты?

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

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

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

//+------------------------------------------------------------------+
//|                                              GraphElmControl.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 <Arrays\ArrayObj.mqh>
#include "..\..\Services\DELib.mqh"
#include "Form.mqh"
//+------------------------------------------------------------------+
//| Класс управления графическими элементами                         |
//+------------------------------------------------------------------+
class CGraphElmControl : public CObject
  {
private:
   int               m_type_node;                     // Тип объекта, для которого строится графика
public:
//--- Возвращает себя
   CGraphElmControl *GetObject(void)                  { return &this;               }
//--- Устанавливает тип объекта, для которого строится графика
   void              SetTypeNode(const int type_node) { this.m_type_node=type_node; }
   
//--- Создаёт объект-форму
   CForm            *CreateForm(const int form_id,const long chart_id,const int wnd,const string name,const int x,const int y,const int w,const int h);
   CForm            *CreateForm(const int form_id,const int wnd,const string name,const int x,const int y,const int w,const int h);
   CForm            *CreateForm(const int form_id,const string name,const int x,const int y,const int w,const int h);

//--- Конструкторы
                     CGraphElmControl(){;}
                     CGraphElmControl(int type_node);
  };
//+------------------------------------------------------------------+

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

Рассмотрим методы класса.

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

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CGraphElmControl::CGraphElmControl(int type_node)
  {
   this.m_type_node=m_type_node;
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Создаёт объект-форму на указанном чарте в заданном подокне       |
//+------------------------------------------------------------------+
CForm *CGraphElmControl::CreateForm(const int form_id,const long chart_id,const int wnd,const string name,const int x,const int y,const int w,const int h)
  {
   CForm *form=new CForm(chart_id,wnd,name,x,y,w,h);
   if(form==NULL)
      return NULL;
   form.SetID(form_id);
   form.SetNumber(0);
   return form;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Создаёт объект-форму на текущем чарте в заданном подокне         |
//+------------------------------------------------------------------+
CForm *CGraphElmControl::CreateForm(const int form_id,const int wnd,const string name,const int x,const int y,const int w,const int h)
  {
   return this.CreateForm(form_id,::ChartID(),wnd,name,x,y,w,h);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Создаёт объект-форму на текущем чарте в главном окне графика     |
//+------------------------------------------------------------------+
CForm *CGraphElmControl::CreateForm(const int form_id,const string name,const int x,const int y,const int w,const int h)
  {
   return this.CreateForm(form_id,::ChartID(),0,name,x,y,w,h);
  }
//+------------------------------------------------------------------+

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

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

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


Интегрируем графику в библиотеку

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

Откроем файл \MQL5\Include\DoEasy\Objects\BaseObj.mqh и подключим к нему файл класса управления графическими объектами:

//+------------------------------------------------------------------+
//|                                                      BaseObj.mqh |
//|                        Copyright 2019, MetaQuotes Software Corp. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019, MetaQuotes Software Corp."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include <Arrays\ArrayObj.mqh>
#include "..\Services\DELib.mqh"
#include "..\Objects\Graph\GraphElmControl.mqh"
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Класс базового объекта для всех объектов библиотеки              |
//+------------------------------------------------------------------+
class CBaseObj : public CObject
  {
protected:
   CGraphElmControl  m_graph_elm;                              // Экземпляр класса управления графическими элементами
   ENUM_LOG_LEVEL    m_log_level;                              // Уровень логирования
   ENUM_PROGRAM_TYPE m_program;                                // Тип программы
   bool              m_first_start;                            // Флаг первого запуска
   bool              m_use_sound;                              // Флаг проигрывания установленного объекту звука
   bool              m_available;                              // Флаг использования объекта-наследника в программе
   int               m_global_error;                           // Код глобальной ошибки
   long              m_chart_id_main;                          // Идентификатор графика управляющей программы
   long              m_chart_id;                               // Идентификатор графика
   string            m_name;                                   // Наименование объекта
   string            m_folder_name;                            // Имя папки хранения объектов-наследников CBaseObj
   string            m_sound_name;                             // Имя звукового файла объекта
   int               m_type;                                   // Тип объекта (соответствует идентификаторам коллекций)

public:

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

//--- Возвращает тип объекта
   virtual int       Type(void)                                const { return this.m_type;                     }
//--- Выводит в журнал описание свойств объекта (full_prop=true - все свойства, false - только поддерживаемые - реализуется в наследниках класса)
   virtual void      Print(const bool full_prop=false,const bool dash=false)  { return;                        }
//--- Выводит в журнал краткое описание объекта
   virtual void      PrintShort(const bool dash=false,const bool symbol=false){ return;                        }
//--- Создаёт объект-форму на указанном графике в указанном подокне
   CForm            *CreateForm(const int form_id,const long chart_id,const int wnd,const string name,const int x,const int y,const int w,const int h)
                       { return this.m_graph_elm.CreateForm(form_id,chart_id,wnd,name,x,y,w,h);                }
//--- Создаёт объект-форму на текущем графике в указанном подокне
   CForm            *CreateForm(const int form_id,const int wnd,const string name,const int x,const int y,const int w,const int h)
                       { return this.m_graph_elm.CreateForm(form_id,wnd,name,x,y,w,h);                         }
//--- Создаёт объект-форму на текущем графике в главном окне
   CForm            *CreateForm(const int form_id,const string name,const int x,const int y,const int w,const int h)
                       { return this.m_graph_elm.CreateForm(form_id,name,x,y,w,h);                             }
   
//--- Конструктор

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

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

Сегодня будем проверять работу с графическими объектами при помощи класса объекта "Бар".
Откроем файл этого класса \MQL5\Include\DoEasy\Objects\Series\Bar.mqh и впишем в метод установки параметров SetProperties() передачу в класс управления графическими объектами типа объекта-бар:

//+------------------------------------------------------------------+
//| Устанавливает параметры объекта-бар                              |
//+------------------------------------------------------------------+
void CBar::SetProperties(const MqlRates &rates)
  {
   this.SetProperty(BAR_PROP_SPREAD,rates.spread);
   this.SetProperty(BAR_PROP_VOLUME_TICK,rates.tick_volume);
   this.SetProperty(BAR_PROP_VOLUME_REAL,rates.real_volume);
   this.SetProperty(BAR_PROP_TIME,rates.time);
   this.SetProperty(BAR_PROP_TIME_YEAR,this.TimeYear());
   this.SetProperty(BAR_PROP_TIME_MONTH,this.TimeMonth());
   this.SetProperty(BAR_PROP_TIME_DAY_OF_YEAR,this.TimeDayOfYear());
   this.SetProperty(BAR_PROP_TIME_DAY_OF_WEEK,this.TimeDayOfWeek());
   this.SetProperty(BAR_PROP_TIME_DAY,this.TimeDay());
   this.SetProperty(BAR_PROP_TIME_HOUR,this.TimeHour());
   this.SetProperty(BAR_PROP_TIME_MINUTE,this.TimeMinute());
//---
   this.SetProperty(BAR_PROP_OPEN,rates.open);
   this.SetProperty(BAR_PROP_HIGH,rates.high);
   this.SetProperty(BAR_PROP_LOW,rates.low);
   this.SetProperty(BAR_PROP_CLOSE,rates.close);
   this.SetProperty(BAR_PROP_CANDLE_SIZE,this.CandleSize());
   this.SetProperty(BAR_PROP_CANDLE_SIZE_BODY,this.BodySize());
   this.SetProperty(BAR_PROP_CANDLE_BODY_TOP,this.BodyHigh());
   this.SetProperty(BAR_PROP_CANDLE_BODY_BOTTOM,this.BodyLow());
   this.SetProperty(BAR_PROP_CANDLE_SIZE_SHADOW_UP,this.ShadowUpSize());
   this.SetProperty(BAR_PROP_CANDLE_SIZE_SHADOW_DOWN,this.ShadowDownSize());
//---
   this.SetProperty(BAR_PROP_TYPE,this.BodyType());
//--- Установим тип объекта в объект класса управления графическими объектами
   this.m_graph_elm.SetTypeNode(this.m_type);
  }
//+------------------------------------------------------------------+

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

На основании таких рассуждений, создадим сейчас предварительную версию класса-коллекции графических объектов. Для него нам потребуется указать идентификатор списка коллекции графических объектов в файле \MQL5\Include\DoEasy\Defines.mqh:

//--- Параметры таймера коллекции чартов
#define COLLECTION_CHARTS_PAUSE        (500)                      // Пауза таймера коллекции чартов в миллисекундах
#define COLLECTION_CHARTS_COUNTER_STEP (16)                       // Шаг приращения счётчика таймера чартов
#define COLLECTION_CHARTS_COUNTER_ID   (9)                        // Идентификатор счётчика таймера чартов
//--- Идентификаторы списков коллекций
#define COLLECTION_HISTORY_ID          (0x777A)                   // Идентификатор списка исторической коллекции
#define COLLECTION_MARKET_ID           (0x777B)                   // Идентификатор списка рыночной коллекции
#define COLLECTION_EVENTS_ID           (0x777C)                   // Идентификатор списка коллекции событий
#define COLLECTION_ACCOUNT_ID          (0x777D)                   // Идентификатор списка коллекции аккаунтов
#define COLLECTION_SYMBOLS_ID          (0x777E)                   // Идентификатор списка коллекции символов
#define COLLECTION_SERIES_ID           (0x777F)                   // Идентификатор списка коллекции таймсерий
#define COLLECTION_BUFFERS_ID          (0x7780)                   // Идентификатор списка коллекции индикаторных буферов
#define COLLECTION_INDICATORS_ID       (0x7781)                   // Идентификатор списка коллекции индикаторов
#define COLLECTION_INDICATORS_DATA_ID  (0x7782)                   // Идентификатор списка коллекции индикаторных данных
#define COLLECTION_TICKSERIES_ID       (0x7783)                   // Идентификатор списка коллекции тиковых серий
#define COLLECTION_MBOOKSERIES_ID      (0x7784)                   // Идентификатор списка коллекции серий стаканов цен
#define COLLECTION_MQL5_SIGNALS_ID     (0x7785)                   // Идентификатор списка коллекции mql5-сигналов
#define COLLECTION_CHARTS_ID           (0x7786)                   // Идентификатор списка коллекции чартов
#define COLLECTION_CHART_WND_ID        (0x7787)                   // Идентификатор списка окон чартов
#define COLLECTION_GRAPH_OBJ_ID        (0x7788)                   // Идентификатор списка коллекции графических объектов
//--- Идентификаторы типов отложенных запросов

В папке библиотеки \MQL5\Include\DoEasy\Collections\ создадим новый файл GraphElementsCollection.mqh класса CGraphElementsCollection:

//+------------------------------------------------------------------+
//|                                      GraphElementsCollection.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"
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "ListObj.mqh"
#include "..\Services\Select.mqh"
#include "..\Objects\Graph\Form.mqh"
//+------------------------------------------------------------------+
//| Коллекция графических объектов                                   |
//+------------------------------------------------------------------+
class CGraphElementsCollection : public CBaseObj
  {
private:
   CListObj          m_list_all_graph_obj;      // Список всех графических объектов
   int               m_delta_graph_obj;         // Разница в количестве графических объектов по сравнению с прошлой проверкой
//--- Возвращает флаг наличия объекта-графического элемента в списке графических объектов
   bool              IsPresentGraphElmInList(const int id,const ENUM_GRAPH_ELEMENT_TYPE type_obj);
public:
//--- Возвращает себя
   CGraphElementsCollection *GetObject(void)                                                             { return &this;                        }
   //--- Возвращает полный список-коллекцию "как есть"
   CArrayObj        *GetList(void)                                                                       { return &this.m_list_all_graph_obj;   }
   //--- Возвращает список по выбранному (1) целочисленному, (2) вещественному и (3) строковому свойству, удовлетворяющему сравниваемому критерию
   CArrayObj        *GetList(ENUM_CANV_ELEMENT_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode=EQUAL)  { return CSelect::ByGraphCanvElementProperty(this.GetList(),property,value,mode);  }
   CArrayObj        *GetList(ENUM_CANV_ELEMENT_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByGraphCanvElementProperty(this.GetList(),property,value,mode);  }
   CArrayObj        *GetList(ENUM_CANV_ELEMENT_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByGraphCanvElementProperty(this.GetList(),property,value,mode);  }
   //--- Возвращает количество новых графических объектов
   int               NewObjects(void)   const                                                            { return this.m_delta_graph_obj;       }
   //--- Конструктор
                     CGraphElementsCollection();
//--- Выводит в журнал описание свойств объекта (full_prop=true - все свойства, false - только поддерживаемые - реализуется в наследниках класса)
   virtual void      Print(const bool full_prop=false,const bool dash=false);
//--- Выводит в журнал краткое описание объекта
   virtual void      PrintShort(const bool dash=false,const bool symbol=false);
  };
//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CGraphElementsCollection::CGraphElementsCollection()
  {
   ::ChartSetInteger(::ChartID(),CHART_EVENT_MOUSE_MOVE,true);
   ::ChartSetInteger(::ChartID(),CHART_EVENT_MOUSE_WHEEL,true);
   this.m_list_all_graph_obj.Sort(SORT_BY_CANV_ELEMENT_ID);
   this.m_list_all_graph_obj.Clear();
   this.m_list_all_graph_obj.Type(COLLECTION_GRAPH_OBJ_ID);
  }
//+------------------------------------------------------------------+

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

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

Осталось подключить файл класса-коллекции графических объектов к файлу главного объекта библиотеки CEngine, расположенного по адресу \MQL5\Include\DoEasy\Engine.mqh:

//+------------------------------------------------------------------+
//|                                                       Engine.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "Services\TimerCounter.mqh"
#include "Collections\HistoryCollection.mqh"
#include "Collections\MarketCollection.mqh"
#include "Collections\EventsCollection.mqh"
#include "Collections\AccountsCollection.mqh"
#include "Collections\SymbolsCollection.mqh"
#include "Collections\ResourceCollection.mqh"
#include "Collections\TimeSeriesCollection.mqh"
#include "Collections\BuffersCollection.mqh"
#include "Collections\IndicatorsCollection.mqh"
#include "Collections\TickSeriesCollection.mqh"
#include "Collections\BookSeriesCollection.mqh"
#include "Collections\MQLSignalsCollection.mqh"
#include "Collections\ChartObjCollection.mqh"
#include "Collections\GraphElementsCollection.mqh"
#include "TradingControl.mqh"
//+------------------------------------------------------------------+
//| Класс-основа библиотеки                                          |
//+------------------------------------------------------------------+
class CEngine
  {

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


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

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

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

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

В глобальной области вместо подключения файлов

#include <Arrays\ArrayObj.mqh>
#include <DoEasy\Services\Select.mqh>
#include <DoEasy\Objects\Graph\Form.mqh>

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

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart81.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 <DoEasy\Engine.mqh>
//--- defines
#define        FORMS_TOTAL (4)   // Количество создаваемых форм
#define        START_X     (4)   // Начальная координата X фигуры
#define        START_Y     (4)   // Начальная координата Y фигуры
//--- 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
CEngine        engine;
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'246,244,244';     // Исходный ≈бледно-серый
   array_clr[1]=C'249,251,250';     // Конечный ≈бледный серо-зелёный
//--- Создадим массив с текущим символом и установим его для использования в библиотеке
   string array[1]={Symbol()};
   engine.SetUsedSymbols(array);
   //--- Создадим объект-таймсерию для текущего символа и периода и выведем его описание в журнал
   engine.SeriesCreate(Symbol(),Period());
   engine.GetTimeSeriesCollection().PrintShort(false); // Краткие описания
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Удалим из советника функции FigureType() и FigureProcessing() — они нам не нужны для этого теста, но занимают практически весь объём кода советника.

На их место впишем три функции.

Функция, возвращающая флаг существования формы с указанным именем:

//+------------------------------------------------------------------+
//| Возвращает флаг существования формы с указанным именем           |
//+------------------------------------------------------------------+
bool IsPresentForm(const string name)
  {
   //--- В цикле по списку объектов-форм
   for(int i=0;i<list_forms.Total();i++)
     {
      //--- получаем очередной объкт-форму
      CForm *form=list_forms.At(i);
      if(form==NULL)
         continue;
      //--- формируем искомое имя объекта как "Имя программы_" + переданное в функцию имя формы
      string nm=MQLInfoString(MQL_PROGRAM_NAME)+"_"+name;
      //--- Если текущий объект-форма имеет такое имя - возвращаем true
      if(form.NameObj()==nm)
         return true;
     }
   //--- По окончании цикла возвращаем false
   return false;
  }
//+------------------------------------------------------------------+

Функция, скрывающая все формы кроме одной с указанным именем:

//+------------------------------------------------------------------+
//| Скрывает все формы кроме одной с указанным именем                |
//+------------------------------------------------------------------+
void HideFormAllExceptOne(const string name)
  {
   //--- В цикле по списку объектов-форм
   for(int i=0;i<list_forms.Total();i++)
     {
      //--- получаем очередной объкт-форму
      CForm *form=list_forms.At(i);
      if(form==NULL)
         continue;
      //--- формируем искомое имя объекта как "Имя программы_" + переданное в функцию имя формы
      string nm=MQLInfoString(MQL_PROGRAM_NAME)+"_"+name;
      //--- Если текущий объект-форма имеет такое имя - отображаем его
      if(form.NameObj()==nm)
         form.Show();
      //--- иначе - скрываем
      else
         form.Hide();
     }
  }
//+------------------------------------------------------------------+

Функция, возвращающая флаг удержания клавиши "Ctrl" на клавиатуре:

//+------------------------------------------------------------------+
//| Возвращает флаг удержания клавиши "Ctrl" на клавиатуре           |
//+------------------------------------------------------------------+
bool IsCtrlKeyPressed(void)
  {
   return((TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)&0x80)!=0);
  }
//+------------------------------------------------------------------+

Все функции достаточно просты и, думаю, в пояснениях не нуждаются.

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

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Если перемещаем мышь
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      CForm *form=NULL;
      datetime time=0;
      double price=0;
      int wnd=0;
      
      //--- Если клавиша Ctrl не нажата
      if(!IsCtrlKeyPressed())
        {
         //--- очищаем список созданных объектов-форм и разрешаем прокручивать график мышкой
         list_forms.Clear();
         ChartSetInteger(ChartID(),CHART_MOUSE_SCROLL,true);
         return;
        }
      
      //--- Если координаты  X и Y графика успешно преобразованы в значения время и цена
      if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,wnd,time,price))
        {
         //--- получим индекс бара, над которым находится курсор
         int index=iBarShift(Symbol(),PERIOD_CURRENT,time);
         if(index==WRONG_VALUE)
            return;
         
         //--- Получим объект-бар по индексу
         CBar *bar=engine.SeriesGetBar(Symbol(),Period(),index);
         if(bar==NULL)
            return;
         
         //--- Преобразуем координаты графика из представления время/цена объекта-бара в координаты по оси X и Y
         int x=(int)lparam,y=(int)dparam;
         if(!ChartTimePriceToXY(ChartID(),0,bar.Time(),(bar.Open()+bar.Close())/2.0,x,y))
            return;
         
         //--- Запретим перемещать график мышкой
         ChartSetInteger(ChartID(),CHART_MOUSE_SCROLL,false);
         
         //--- Создадим имя объекта-формы и скроем все объекты кроме одного с таким именем
         string name="FormBar_"+(string)index;
         HideFormAllExceptOne(name);
         
         //--- Если ещё нет объекта-формы с таким именем
         if(!IsPresentForm(name))
           {
            //--- создадим новый объект-форму
            form=bar.CreateForm(index,name,x,y,76,16);
            if(form==NULL)
               return;
            
            //--- Установим форме флаги активности и неперемещаемости
            form.SetActive(true);
            form.SetMovable(false);
            //--- Установим непрозрачность 200
            form.SetOpacity(200);
            //--- Цвет фона формы зададим как первый цвет из массива цветов
            form.SetColorBackground(array_clr[0]);
            //--- Цвет очерчивающей рамки формы
            form.SetColorFrame(C'47,70,59');
            //--- Установим флаг рисования тени
            form.SetShadow(true);
            //--- Рассчитаем цвет тени как цвет фона графика, преобразованный в монохромный
            color clrS=form.ChangeColorSaturation(form.ColorBackground(),-100);
            //--- Если в настройках задано использовать цвет фона графика, то затемним монохромный цвет на 20 единиц
            //--- Иначе - будем использовать для рисования тени заданный в настройках цвет
            color clr=(InpUseColorBG ? form.ChangeColorLightness(clrS,-20) : InpColorForm3);
            //--- Нарисуем тень формы со смещением от формы вправо-вниз на три пикселя по всем осям
            //--- Непрозрачность тени при этом установим равной 200, а радиус размытия равный 4
            form.DrawShadow(2,2,clr,200,3);
            //--- Зальём фон формы вертикальным градиентом
            form.Erase(array_clr,form.Opacity());
            //--- Нарисуем очерчивающий прямоугольник по краям формы
            form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,form.ColorFrame(),form.Opacity());
            //--- Если объект-форму не удалось добавить в список - удаляем форму и уходим из обработчика
            if(!list_forms.Add(form))
              {
               delete form;
               return;
              }
            //--- Зафиксируем внешний вид формы
            form.Done();
           }
         //--- Если объект-форма существует
         if(form!=NULL)
           {
            //--- нарисуем на нём текст с описанием типа бара, соответствующего положению курсора мышки и покажем форму
            form.TextOnBG(0,bar.BodyTypeDescription(),form.Width()/2,form.Height()/2-1,FRAME_ANCHOR_CENTER,C'7,28,21');
            form.Show();
           }
         //--- Перерисуем чарт
         ChartRedraw();
        }
     }
  }
//+------------------------------------------------------------------+

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

Скомпилируем советник и запустим его на графике. Зажмём клавишу Ctrl и подвигаем мышкой по графику. Для каждого бара будет создаваться объект-форма, на который будет выводиться описание типа бара (медвежий/бычий/доджи). Отпускание клавиши Ctrl удалит все созданные объекты.



Что дальше

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

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

К содержанию

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

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