Прочие классы в библиотеке DoEasy (Часть 70): Расширение функционала и автообновление коллекции объектов-чартов

Artyom Trishkin | 16 апреля, 2021

Содержание


Концепция

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

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

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


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

Приступим. В первую очередь (как стало обычным) в файл \MQL5\Include\DoEasy\Data.mqh впишем индексы новых сообщений:

   MSG_CHART_OBJ_CHART_WINDOW,                        // Главное окно графика
   MSG_CHART_OBJ_CHART_SUBWINDOW,                     // Подокно графика
   MSG_CHART_OBJ_CHART_SUBWINDOWS_NUM,                // Подокон
   MSG_CHART_OBJ_INDICATORS_MW_NAME_LIST,             // Индикаторы в главном окне графика
   MSG_CHART_OBJ_INDICATORS_SW_NAME_LIST,             // Индикаторы в окне графика
   MSG_CHART_OBJ_INDICATOR,                           // Индикатор
   MSG_CHART_OBJ_INDICATORS_TOTAL,                    // Индикаторов
   MSG_CHART_OBJ_WINDOW_N,                            // Окно
   MSG_CHART_OBJ_INDICATORS_NONE,                     // Отсутствуют
   MSG_CHART_OBJ_ERR_FAILED_GET_WIN_OBJ,              // Не удалось получить объект-окно графика
   MSG_CHART_OBJ_SCREENSHOT_CREATED,                  // Скриншот создан
   MSG_CHART_OBJ_TEMPLATE_SAVED,                      // Шаблон графика сохранён
   MSG_CHART_OBJ_TEMPLATE_APPLIED,                    // Шаблон применён к графику
  
//--- 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,               // Нельзя открыть новый график, так как количество открытых графиков уже максимальное
  
  };
//+------------------------------------------------------------------+

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

   {"Главное окно графика","Main chart window"},
   {"Подокно графика","Chart subwindow"},
   {"Подокон","Subwindows"},
   {"Индикаторы в главном окне графика","Indicators in the main chart window"},
   {"Индикаторы в окне графика","Indicators in the chart window"},
   {"Индикатор","Indicator"},
   {"Индикаторов","Indicators total"},
   {"Окно","Window"},
   {"Отсутствуют","No indicators"},
   {"Не удалось получить объект-окно графика","Failed to get the chart window object"},
   {"Скриншот создан","Screenshot created"},
   {"Шаблон графика сохранён","Chart template saved"},
   {"Шаблон применён к графику","Template applied to the chart"},
   
//--- 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"},
   
  };
//+---------------------------------------------------------------------+


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

В файле \MQL5\Include\DoEasy\Defines.mqh добавим эти новые макроподстановки:

//--- Параметры данных для файловых операций
#define DIRECTORY                      ("DoEasy\\")               // Каталог библиотеки для расположения папок объектов классов
#define RESOURCE_DIR                   ("DoEasy\\Resource\\")     // Каталог библиотеки для расположения папок ресурсов
#define SCREENSHOT_DIR                 ("DoEasy\\ScreenShots\\")  // Каталог библиотеки для расположения папок скриншотов
#define TEMPLATE_DIR                   ("DoEasy\\")               // Каталог библиотеки для расположения папок шаблонов
#define FILE_EXT_GIF                   (".gif")                   // Расширение имени файла изображения GIF
#define FILE_EXT_PNG                   (".png")                   // Расширение имени файла изображения PNG
#define FILE_EXT_BMP                   (".bmp")                   // Расширение имени файла изображения BMP
#define SCREENSHOT_FILE_EXT            (FILE_EXT_PNG)             // Формат файла скриншотов графиков (расширение: можно использовать .gif, .png и .bmp)
//--- Параметры символов

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

Скриншоты сохраняются в папку (Каталог данных терминала)\MQL5\Files\

А шаблоны сохраняются в папку (Каталог данных терминала)\ MQL5\Profiles\Templates\

Таким образом, добавление к имени файла указанных макроподстановок сделает хранение файлов библиотеки более адресным.
Скриншоты будут сохраняться в папку \MQL5\Files\DoEasy\ScreenShots\, а шаблоны — в папку MQL5\Profiles\Templates\DoEasy\.

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

//+------------------------------------------------------------------+
//| Возвращает имя для файла (имя программы+локальное время)         |
//+------------------------------------------------------------------+
string FileNameWithTimeLocal(const string time_prefix=NULL)
  {
   string name=
     (
      MQLInfoString(MQL_PROGRAM_NAME)+"_"+time_prefix+(time_prefix==NULL ? "" : "_")+
      TimeToString(TimeLocal(),TIME_DATE|TIME_MINUTES|TIME_SECONDS)
     );
   ResetLastError();
   if(StringReplace(name," ","_")==WRONG_VALUE)
      CMessage::ToLog(DFUN,GetLastError(),true);
   if(StringReplace(name,":",".")==WRONG_VALUE)
      CMessage::ToLog(DFUN,GetLastError(),true);
   return name;
  }
//+------------------------------------------------------------------+

В функции создаётся строка из наименования программы + переданное значение в параметрах функции + локальное время компьютера в формате Дата/Часы-Минуты/Секунды. Затем все пробелы заменяются знаками подчёркивания (_), а все двоеточия — точками (.) и возвращается полученная строка. Если замены не сработали, то будут выведены об этом сообщения в журнал.
Хочется отметить, что функция будет возвращать одинаковое имя файла в течении секунды. Т. е. если за секунду вызвать функцию несколько раз, то она всегда вернёт одну и ту же строку в течении этой секунды. Поэтому здесь введён входной параметр функции, в котором можно передать дополнительную информацию о файле для уникальности его идентификации и, как дополнение — большей информативности.

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

В файле \MQL5\Include\DoEasy\Objects\Chart\ChartWnd.mqh в приватной секции класса добавим переменные для хранения координат курсора в окне:

//+------------------------------------------------------------------+
//| Класс объекта-окна графика                                       |
//+------------------------------------------------------------------+
class CChartWnd : public CBaseObj
  {
private:
   CArrayObj         m_list_ind;                                        // Список индикаторов
   int               m_window_num;                                      // Номер подокна
   int               m_wnd_coord_x;                                     // Координата X для времени на графике в окне
   int               m_wnd_coord_y;                                     // Координата Y для цены на графике в окне
//--- Возвращает флаг наличия индикатора из списка в окне
   bool              IsPresentInWindow(const CWndInd *ind);
//--- Удаляет из списка уже отсутствующие в окне индикаторы
   void              IndicatorsDelete(void);
//--- Добавляет в список новые индикаторы
   void              IndicatorsAdd(void);
//--- Устанавливает номер подокна
   void              SetWindowNum(const int num)                        { this.m_window_num=num;   }
   
public:

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

//--- Обновляет данные по прикреплённым индикаторам
   void              Refresh(void);
   
//--- Преобразует координаты графика из представления время/цена в координаты по оси X и Y
   bool              TimePriceToXY(const datetime time,const double price);
//--- Возвращает координаты X и Y расположения курсора в окне
   int               XFromTimePrice(void)                         const { return this.m_wnd_coord_x;  }
   int               YFromTimePrice(void)                         const { return this.m_wnd_coord_y;  }
//--- Возвращает относительную координату Y расположения курсора в окне
   int               YFromTimePriceRelative(void)  const { return this.m_wnd_coord_y-this.YDistance();}
   
  };
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Параметрический конструктор                                      |
//+------------------------------------------------------------------+
CChartWnd::CChartWnd(const long chart_id,const int wnd_num) : m_window_num(wnd_num),
                                                              m_wnd_coord_x(0),
                                                              m_wnd_coord_y(0)
  {
   CBaseObj::SetChartID(chart_id);
   this.IndicatorsListCreate();
  }
//+------------------------------------------------------------------+

За пределами тела класса напишем реализацию метода, преобразующего координаты графика из представления время/цена в координаты по оси X и Y:

//+------------------------------------------------------------------+
//| Преобразует координаты графика из представления                  |
//| время/цена в координаты по оси X и Y                             |
//+------------------------------------------------------------------+
bool CChartWnd::TimePriceToXY(const datetime time,const double price)
  {
   ::ResetLastError();
   if(!::ChartTimePriceToXY(this.m_chart_id,this.m_window_num,time,price,this.m_wnd_coord_x,this.m_wnd_coord_y))
     {
      //CMessage::ToLog(DFUN,::GetLastError(),true);
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

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

Метод записывает полученный результат в новые переменные для их хранения, добавленные нами выше, а методы XFromTimePrice() и YFromTimePrice() возвращают значения этих переменных. Таким образом, мы сначала должны вызвать метод TimePriceToXY(), а после того, как он вернул true, мы можем получить значение нужной нам координаты.

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

//+------------------------------------------------------------------+
//| Обновляет данные по прикреплённым индикаторам                    |
//+------------------------------------------------------------------+
void CChartWnd::Refresh(void)
  {
   int change=::ChartIndicatorsTotal(this.m_chart_id,this.m_window_num)-this.m_list_ind.Total();
   if(change!=0)
     {
      this.IndicatorsDelete();
      this.IndicatorsAdd();
     }
  }
//+------------------------------------------------------------------+


Доработаем класс объекта-чарта, расположенный в файле \MQL5\Include\DoEasy\Objects\Chart\ChartObj.mqh. В прошлой статье мы сделали работу метода WindowsTotal() таким образом, что за один вызов сразу получали значение из окружения и записывали его же в свойства объекта. Однако это оказалось не очень практично с точки зрения наглядности построения логики кода и количества обращений к окружению, и я решил отказаться от этой затеи. Теперь метод просто возвращает значение свойства объекта:

   int WindowsTotal(void) const { return (int)this.GetProperty(CHART_PROP_WINDOWS_TOTAL); }

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

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

Подключим к файлу класса CChartObj файл класса CSelect:

//+------------------------------------------------------------------+
//|                                                     ChartObj.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 "..\..\Objects\BaseObj.mqh"
#include "..\..\Services\Select.mqh"
#include "ChartWnd.mqh"
//+------------------------------------------------------------------+

Он нам потребуется для фильтрации списка объектов-чартов по их свойствам.

В приватной секции класса впишем две новые переменные-члены класса для хранения времени для координаты X и цены для координаты Y на графике:

//+------------------------------------------------------------------+
//| Класс объекта-чарта                                              |
//+------------------------------------------------------------------+
class CChartObj : public CBaseObj
  {
private:
   CArrayObj         m_list_wnd;                                  // Список объектов окон графика
   long              m_long_prop[CHART_PROP_INTEGER_TOTAL];       // Целочисленные свойства
   double            m_double_prop[CHART_PROP_DOUBLE_TOTAL];      // Вещественные свойства
   string            m_string_prop[CHART_PROP_STRING_TOTAL];      // Строковые свойства
   int               m_digits;                                    // Digits() символа
   datetime          m_wnd_time_x;                                // Время для координаты X на графике в окне
   double            m_wnd_price_y;                               // Цена для координаты Y на графике в окне
   

Здесь же  — в приватной секции класса, объявим метод, добавляющий расширение файлу скриншота при его отсутствии:

//--- Создаёт список окон графика
   void              CreateWindowsList(void);
//--- Добавляет расширение файлу скриншота при его отсутствии
   string            FileNameWithExtention(const string filename);
   
public:

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

//--- Возвращает флаг, что объект-чарт принадлежит графику программы
   bool              IsMainChart(void)                               const { return(this.m_chart_id==CBaseObj::GetMainChartID());            }
//--- Возвращает указанное по индексу окно графика
   CChartWnd        *GetWindowByIndex(const int index)               const { return this.m_list_wnd.At(index);                               }
//--- Возвращает объект-окно по его номеру подокна
   CChartWnd        *GetWindowByNum(const int win_num)               const;
//--- Возвращает объект-окно по имени индикатора в нём
   CChartWnd        *GetWindowByIndicator(const string ind_name)     const;
   
//--- Выводит в журнал данные всех индикаторов всех окон графика
   void              PrintWndIndicators(void);
//--- Выводит в журнал свойства всех окон графика
   void              PrintWndParameters(void);

//--- Сдвигает график на указанное количество баров относительно указанной позиции графика
   bool              Navigate(const int shift,const ENUM_CHART_POSITION position);
//--- Сдвигает график (1) влево, (2) вправо на указанное количество баров
   bool              NavigateLeft(const int shift);
   bool              NavigateRight(const int shift);
//--- Смещает график (1) к началу, (2) к концу исторических данных
   bool              NavigateBegin(void);
   bool              NavigateEnd(void);

//--- Создаёт скриншот графика
   bool              ScreenShot(const string filename,const int width,const int height,const ENUM_ALIGN_MODE align);
//--- Создаёт скриншот в разрешении (1) окна графика, (2) 800х600, (3) 750х562 пикселей
   bool              ScreenShotWndSize(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER);
   bool              ScreenShot800x600(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER);
   bool              ScreenShot750x562(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER);
   
//--- Сохраняет шаблон графика с текущими настройками
   bool              SaveTemplate(const string filename=NULL);
//--- Применяет к графику указанный шаблон
   bool              ApplyTemplate(const string filename=NULL);
   
//--- Преобразует координаты X и Y графика окна в значения время и цена
   int               XYToTimePrice(const long x,const double y);
//--- Возвращает (1) время, (2) цену из значений координат XY
   datetime          TimeFromXY(void)                                const { return this.m_wnd_time_x;   }
   double            PriceFromXY(void)                               const { return this.m_wnd_price_y;  }
   
  };
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Параметрический конструктор                                      |
//+------------------------------------------------------------------+
CChartObj::CChartObj(const long chart_id) : m_wnd_time_x(0),m_wnd_price_y(0)
  {
  }

Рассмотрим реализацию новых методов.

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

//+------------------------------------------------------------------+
//| Возвращает объект-окно по имени индикатора в нём                 |
//+------------------------------------------------------------------+
CChartWnd *CChartObj::GetWindowByIndicator(const string ind_name) const
  {
   int index=(this.m_program==PROGRAM_INDICATOR && ind_name==NULL ? ::ChartWindowFind() : ::ChartWindowFind(this.m_chart_id,ind_name));
   return this.GetWindowByIndex(index);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Сдвигает график на указанное количество баров                    |
//| относительно указанной позиции графика                           |
//+------------------------------------------------------------------+
bool CChartObj::Navigate(const int shift,const ENUM_CHART_POSITION position)
  {
   ::ResetLastError();
   bool res=::ChartNavigate(m_chart_id,position,shift);
   if(!res)
      CMessage::ToLog(DFUN,::GetLastError(),true);
   return res;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Сдвигает график влево на указанное количество баров              |
//+------------------------------------------------------------------+
bool CChartObj::NavigateLeft(const int shift)
  {
   this.SetAutoscrollOFF();
   return this.Navigate(shift,CHART_CURRENT_POS);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Сдвигает график вправо на указанное количество баров             |
//+------------------------------------------------------------------+
bool CChartObj::NavigateRight(const int shift)
  {
   this.SetAutoscrollOFF();
   return this.Navigate(-shift,CHART_CURRENT_POS);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Смещает график к началу исторических данных                      |
//+------------------------------------------------------------------+
bool CChartObj::NavigateBegin(void)
  {
   this.SetAutoscrollOFF();
   return this.Navigate(0,CHART_BEGIN);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Смещает график к концу исторических данных                       |
//+------------------------------------------------------------------+
bool CChartObj::NavigateEnd(void)
  {
   this.SetAutoscrollOFF();
   return this.Navigate(0,CHART_END);
  }
//+------------------------------------------------------------------+

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

Метод, создающий скриншот графика:

//+------------------------------------------------------------------+
//| Создаёт скриншот графика                                         |
//+------------------------------------------------------------------+
bool CChartObj::ScreenShot(const string filename,const int width,const int height,const ENUM_ALIGN_MODE align)
  {
   ::ResetLastError();
   if(!::ChartScreenShot(m_chart_id,filename,width,height,align))
     {
      CMessage::ToLog(DFUN,::GetLastError(),true);
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

В метод передаются имя файла скриншота, ширина и высота получаемого изображения и выравнивание (ENUM_ALIGN_MODE). Выравнивание необходимо при создании вертикальных снимков — когда высота изображения больше его ширины. В этом случае выравнивание указывает нам к какому краю графика будет "прижато" изображение.
Здесь мы просто делаем скриншот с параметрами, переданными в метод, при помощи функции ChartScreenShot().
При успешном создании скриншота экрана возвращаем true.
Если же скриншот создать не удалось, то выводим об этом сообщение в журнал и возвращаем false.

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

  1. скриншот по размеру окна графика,
  2. скриншот размером 800х600,
  3. скриншот размером 750х562.

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

Метод, создающий скриншот графика в разрешении окна графика (включая шкалы цены и времени при их наличии):

//+------------------------------------------------------------------+
//| Создаёт скриншот графика в разрешении окна графика               |
//+------------------------------------------------------------------+
bool CChartObj::ScreenShotWndSize(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER)
  {
//--- Создаём имя файла или используем переданное в метод
   string name=
     (filename==NULL || filename=="" ? 
      SCREENSHOT_DIR+FileNameWithTimeLocal(this.Symbol()+"_"+TimeframeDescription(this.Timeframe()))+SCREENSHOT_FILE_EXT :  
      this.FileNameWithExtention(filename)
     );
//--- Получаем окно графика, имеющего наибольший из всех окон номер
   CChartWnd *wnd=this.GetWindowByNum(this.m_list_wnd.Total()-1);
   if(wnd==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_ERR_FAILED_GET_WIN_OBJ),string(this.m_list_wnd.Total()-1));
      return false;
     }
//--- Рассчитываем ширину и высоту скриншота с учётом размеров шкал цены и времени
   int width=this.WidthInPixels()+(IsShowPriceScale() ? 56 : 0);
   int height=wnd.YDistance()+wnd.HeightInPixels()+(this.IsShowDateScale() ? 15 : 0);
//--- Создаём скриншот и возвращаем результат работы метода ScreenShot()
   bool res=this.ScreenShot(name,width,height,align);
   if(res)
      ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_SCREENSHOT_CREATED),": ",name," (",(string)width," x ",(string)height,")");
   return res;
  }
//+------------------------------------------------------------------+

В листинге метода подробно прокомментирована его логика. Вкратце: при создании имени, если в метод передан NULL, то создаём имя файла, состоящее из пути к файлам скриншотов библиотеки, наименования программы и расширения, заданного по умолчанию в файле Defines.mqh. Если же имя файла передано в метод не пустым, то при помощи метода FileNameWithExtention(), который рассмотрим далее, проверяем наличие расширения в имени файла (для скриншотов может быть только три расширения: .gif, .png и .bmp) и добавляем расширение к имени файла при его отсутствии.

Чтобы рассчитать размеры скриншота с учётом всех окон, принадлежащих графику, нам нужно найти окно, которое имеет наибольший номер из всех (0 — главное окно графика, 1, 2, 3, N — все открытые на нём окна сверху-вниз). Т. е. самое нижнее окно будет иметь наибольший номер. Зная дистанцию от верхнего края главного окна графика до верхнего края самого нижнего окна, открытого на графике, мы получим точку отсчёта, к которой нужно прибавить высоту этого окна. Таким образом мы получим полную высоту всего графика. И нам останется лишь проверить наличие на графике шкалы времени. Если шкала есть, то прибавляем к уже рассчитанной высоте 15 пикселей (размер подбирался опытным путём), если шкалы нет, то ничего не прибавляем. Таким образом мы находим высоту будущего скриншота.

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

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

Метод, создающий скриншот графика в разрешении 800x600 пикселей:

//+------------------------------------------------------------------+
//| Создаёт скриншот графика в разрешении 800x600 пикселей           |
//+------------------------------------------------------------------+
bool CChartObj::ScreenShot800x600(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER)
  {
   string name=
     (filename==NULL || filename=="" ? 
      SCREENSHOT_DIR+FileNameWithTimeLocal(this.Symbol()+"_"+TimeframeDescription(this.Timeframe()))+SCREENSHOT_FILE_EXT :  
      this.FileNameWithExtention(filename)
     );
   int width=800;
   int height=600;
   bool res=this.ScreenShot(name,width,height,align);
   if(res)
      ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_SCREENSHOT_CREATED),": ",name," (",(string)width," x ",(string)height,")");
   return res;
  }
//+------------------------------------------------------------------+

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

Метод, создающий скриншот графика в разрешении 750x562 пикселя:

//+------------------------------------------------------------------+
//| Создаёт скриншот графика в разрешении 750x562 пикселей           |
//+------------------------------------------------------------------+
bool CChartObj::ScreenShot750x562(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER)
  {
   string name=
     (filename==NULL || filename=="" ? 
      SCREENSHOT_DIR+FileNameWithTimeLocal(this.Symbol()+"_"+TimeframeDescription(this.Timeframe()))+SCREENSHOT_FILE_EXT :  
      this.FileNameWithExtention(filename)
     );
   int width=750;
   int height=562;
   bool res=this.ScreenShot(name,width,height,align);
   if(res)
      ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_SCREENSHOT_CREATED),": ",name," (",(string)width," x ",(string)height,")");
   return res;
  }
//+------------------------------------------------------------------+

Метод аналогичен методу создания скриншота 800х600 пикселей за исключением размеров изображения.

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

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

//+------------------------------------------------------------------+
//| Сохраняет шаблон графика с текущими настройками                  |
//+------------------------------------------------------------------+
bool CChartObj::SaveTemplate(const string filename=NULL)
  {
   ::ResetLastError();
   string name=
     (filename==NULL || filename=="" ? 
      TEMPLATE_DIR+::MQLInfoString(MQL_PROGRAM_NAME) :  
      filename
     );
   if(!::ChartSaveTemplate(this.m_chart_id,name))
     {
      CMessage::ToLog(DFUN,::GetLastError(),true);
      return false;
     }
   ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_TEMPLATE_SAVED),": ",this.Symbol()," ",TimeframeDescription(this.Timeframe()));
   return true;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Применяет к графику указанный шаблон                             |
//+------------------------------------------------------------------+
bool CChartObj::ApplyTemplate(const string filename=NULL)
  {
   ::ResetLastError();
   string name=
     (filename==NULL || filename=="" ? 
      TEMPLATE_DIR+::MQLInfoString(MQL_PROGRAM_NAME) :  
      filename
     );
   if(!::ChartApplyTemplate(this.m_chart_id,name))
     {
      CMessage::ToLog(DFUN,::GetLastError(),true);
      return false;
     }
   ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_TEMPLATE_APPLIED),": ",this.Symbol()," ",TimeframeDescription(this.Timeframe()));
   return true;
  }
//+------------------------------------------------------------------+

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

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

Метод, преобразующий координаты X и Y графика окна в значения время и цена:

//+------------------------------------------------------------------+
//|Преобразует координаты  X и Y графика окна в значения время и цена|
//+------------------------------------------------------------------+
int CChartObj::XYToTimePrice(const long x,const double y)
  {
   int sub_window=WRONG_VALUE;
   ::ResetLastError();
   if(!::ChartXYToTimePrice(this.m_chart_id,(int)x,(int)y,sub_window,this.m_wnd_time_x,this.m_wnd_price_y))
     {
      //CMessage::ToLog(DFUN,::GetLastError(),true);
      return WRONG_VALUE;
     }
   return sub_window;
  }
//+------------------------------------------------------------------+

Функция ChartXYToTimePrice() преобразует координаты  X и Y графика в значения время и цена. При этом она записывает в переменную sub_window, передаваемую в неё по ссылке, номер подокна, в котором находятся координаты X и Y графика, для которых необходимо вернуть время и цену.
На основании этого данный метод возвращает номер подокна графика: 0 — если координаты находятся в главном окне графика, 1,2,3 и т. д. — если координаты попадают в соответствующее подокно графика, и -1 — если координаты вычислить не удалось. После вызова данного метода, и если значение, которое он вернул, не равно -1, то можно получить время и цену при помощи методов TimeFromXY() и PriceFromXY(), которые просто возвращают значения переменных, в которые будут записаны полученные время и цена функцией ChartXYToTimePrice().

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

//+------------------------------------------------------------------+
//| Добавляет расширение файлу скриншота при его отсутствии          |
//+------------------------------------------------------------------+
string CChartObj::FileNameWithExtention(const string filename)
  {
   if(::StringFind(filename,FILE_EXT_GIF)>WRONG_VALUE || ::StringFind(filename,FILE_EXT_PNG)>WRONG_VALUE || ::StringFind(filename,FILE_EXT_BMP)>WRONG_VALUE)
      return filename;
   return filename+SCREENSHOT_FILE_EXT;
  }
//+------------------------------------------------------------------+

В метод передаётся проверяемая строка, в которой нужно найти расширение файла скриншота. Так как форматы файлов для скриншотов строго определены, и ими могут быть только три типа файлов — GIF, PNG и BMP, то, если в переданной в метод строке есть вхождение хотя бы одной подстроки с таким расширением (т. е. — расширение уже задано), то метод возвращает переданную в него строку без изменений. Иначе — к строке добавляется расширение имени файла, заданное по умолчанию в файле Defines.mqh, и это файл формата PNG (расширение .png), и изменённая строка возвращается.

О проблемах, выявленных при добавлении нового окна на график:
при подробном тестировании было выявлено, что в момент,  когда мы добавляем на график новый индикатор в окне, появляется его окно (но мы ещё не нажали кнопку "ОК" или "Отмена"), и это окно сразу же видно в терминале как уже существующее. На данный момент библиотека его увидит и добавит в свой список окно объекта-чарта, при этом индикатора в этом окне не будет. Но если мы нажмём отмену в окне добавления нового оконного индикатора, то этого окна уже не будет в списке окон графика клиентского терминала. Библиотека удалит это окно из списка на следующей проверке.

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

Таким образом, метод для создания списка окон графика будет доработан:

//+------------------------------------------------------------------+
//| Создаёт список окон графика                                      |
//+------------------------------------------------------------------+
void CChartObj::CreateWindowsList(void)
  {
   //--- Очищаем список окон графика
   this.m_list_wnd.Clear();
   //--- Получаем общее количество окон графика из окружения
   int total=(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOWS_TOTAL);
   //--- В цикле по общему количеству окон
   for(int i=0;i<total;i++)
     {
      //--- Создаём новый объект-окно чарта
      CChartWnd *wnd=new CChartWnd(this.m_chart_id,i);
      if(wnd==NULL)
         continue;
      //--- Если номер окна больше 0 (не главное окно графика) и в нём ещё нет индикатора,
      //--- то удаляем вновь созданный объект-окно чарта и идём на следующую итерацию цикла
      if(wnd.WindowNum()!=0 && wnd.IndicatorsTotal()==0)
        {
         delete wnd;
         continue;
        }
      //--- Если объект в список не был добавлен - удаляем этот объект
      this.m_list_wnd.Sort();
      if(!this.m_list_wnd.Add(wnd))
         delete wnd;
     }
   //--- Если количество объектов в списке соответствует количеству окон на графике,
   //--- то записываем это значение в свойство объекта-чарта
   //--- Если же количество объектов в списке не соответствует количеству окон на графике,
   //--- то в свойство объекта-чарта записываем значение количества объектов в списке.
   int value=int(this.m_list_wnd.Total()==total ? total : this.m_list_wnd.Total());
   this.SetProperty(CHART_PROP_WINDOWS_TOTAL,value);
  }
//+------------------------------------------------------------------+

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

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

Доработка классов библиотеки завершена.


Автообновление класса-коллекции объектов чартов и окон

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

В файле \MQL5\Include\DoEasy\Objects\Chart\ChartObj.mqh класса объекта чарта дополним метод Refresh() так, чтобы мы могли проверять не только изменение количества открытых окон на графике (в объекте-чарте), но и контролировать количество индикаторов в уже открытых окнах (в одно окно можно разместить несколько индикаторов).

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

//+------------------------------------------------------------------+
//| Обновляет объект-чарт и список его окон                          |
//+------------------------------------------------------------------+
void CChartObj::Refresh(void)
  {
   for(int i=0;i<m_list_wnd.Total();i++)
     {
      CChartWnd *wnd=m_list_wnd.At(i);
      if(wnd==NULL)
         continue;
      wnd.Refresh();
     }
   int change=(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOWS_TOTAL)-this.WindowsTotal();
   if(change==0)
      return;
   this.CreateWindowsList();
  }
//+------------------------------------------------------------------+

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

В файле класса-коллекции объектов-чартов \MQL5\Include\DoEasy\Collections\ChartObjCollection.mqh исправим логическую ошибку, мешающую обновлению объектов-чартов в списке-коллекции и, соответственно — и обновлению их окон и индикаторов в них.

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

//+------------------------------------------------------------------+
//| Обновляет список-коллекцию объектов-чартов                       |
//+------------------------------------------------------------------+
void CChartObjCollection::Refresh(void)
  {
   //--- Получаем количество открытых графиков в терминале и
   int charts_total=this.ChartsTotal();
   //--- рассчитываем разницу между количеством открытых графиков в терминале
   //--- и объектов-чартов в списке-коллекции, эти значения выводим в комментарии на графике
   int change=charts_total-this.m_list.Total();
   Comment(DFUN,", list total=",DataTotal(),", charts total=",charts_total,", change=",change);
   //--- Если нет изменений - уходим
   if(change==0)
      return;
   //--- Если добавлен график в терминале
   if(change>0)
     {
      //--- Находим недостающий объект-чарт, создаём и добавляем его в список-коллекцию
      this.FindAndCreateMissingChartObj();
      //--- Получаем текущий график и возвращаемся к нему т.к.
      //--- добавление нового графика переключает фокус на него
      CChartObj *chart=this.GetChart(GetMainChartID());
      if(chart!=NULL)
         chart.SetBringToTopON(true);
     }
   //--- Если удалён график в терминале
   else if(change<0)
    {
     //--- Находим лишний объект-чарт в списке-коллекции и удаляем его из списка
     this.FindAndDeleteExcessChartObj();
    }
   //--- В цикле по количеству объектов-чартов в списке
   for(int i=0;i<this.m_list.Total();i++)
     {
      //--- получаем очередной объект-чарт и
      CChartObj *chart=this.m_list.At(i);
      if(chart==NULL)
         continue;
      //--- обновляем его
      chart.Refresh();
     }
  }
//+------------------------------------------------------------------+

Решение банально — достаточно перенести блок кода обновления объектов-чартов вышеперед проверкой изменения количества открытых графиков в клиентском терминале:

//+------------------------------------------------------------------+
//| Обновляет список-коллекцию объектов-чартов                       |
//+------------------------------------------------------------------+
void CChartObjCollection::Refresh(void)
  {
   //--- В цикле по количеству объектов-чартов в списке
   for(int i=0;i<this.m_list.Total();i++)
     {
      //--- получаем очередной объект-чарт и
      CChartObj *chart=this.m_list.At(i);
      if(chart==NULL)
         continue;
      //--- обновляем его
      chart.Refresh();
     }
   //--- Получаем количество открытых графиков в терминале и
   int charts_total=this.ChartsTotal();
   //--- рассчитываем разницу между количеством открытых графиков в терминале
   //--- и объектов-чартов в списке-коллекции, эти значения выводим в комментарии на графике
   int change=charts_total-this.m_list.Total();
   //--- Если нет изменений - уходим
   if(change==0)
      return;
   //--- Если добавлен график в терминале
   if(change>0)
     {
      //--- Находим недостающий объект-чарт, создаём и добавляем его в список-коллекцию
      this.FindAndCreateMissingChartObj();
      //--- Получаем текущий график и возвращаемся к нему т.к.
      //--- добавление нового графика переключает фокус на него
      CChartObj *chart=this.GetChart(GetMainChartID());
      if(chart!=NULL)
         chart.SetBringToTopON(true);
     }
   //--- Если удалён график в терминале
   else if(change<0)
    {
     //--- Находим лишний объект-чарт в списке-коллекции и удаляем его из списка
     this.FindAndDeleteExcessChartObj();
    }
  }
//+------------------------------------------------------------------+

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

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

//--- Обновляет (1) список-коллекцию объектов-чартов, (2) указанный объект-чарт
   void                    Refresh(void);
   void                    Refresh(const long chart_id);

//--- (1) Открывает новый график с указанным символом и периодом, (2) закрывает указанный график
   bool                    Open(const string symbol,const ENUM_TIMEFRAMES timeframe);
   bool                    Close(const long chart_id);

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

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

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

//+------------------------------------------------------------------+
//| Открывает новый график с указанным символом и периодом           |
//+------------------------------------------------------------------+
bool CChartObjCollection::Open(const string symbol,const ENUM_TIMEFRAMES timeframe)
  {
   if(this.m_list.Total()==CHARTS_MAX)
     {
      ::Print(CMessage::Text(MSG_CHART_COLLECTION_ERR_CHARTS_MAX)," (",(string)CHARTS_MAX,")");
      return false;
     }
   ::ResetLastError();
   long chart_id=::ChartOpen(symbol,timeframe);
   if(chart_id==0)
      CMessage::ToLog(::GetLastError(),true);
   return(chart_id>0);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Закрывает указанный график                                       |
//+------------------------------------------------------------------+
bool CChartObjCollection::Close(const long chart_id)
  {
   ::ResetLastError();
   bool res=::ChartClose(chart_id);
   if(!res)
      CMessage::ToLog(DFUN,::GetLastError(),true);
   return res;
  }
//+------------------------------------------------------------------+

Здесь: если попытка закрытия указанного по идентификатору графика не была успешной — выводим об этом запись в журнал.
Метод возвращает результат работы функции ChartClose().

В файле \MQL5\Include\DoEasy\Engine.mqh главного объекта библиотеки CEngine добавим методы для управления коллекцией чартов.

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

//--- Возвращает список объектов-чартов по (1) символу, (2) таймфрейму
   CArrayObj           *ChartGetChartsList(const string symbol)                        { return this.m_charts.GetChartsList(symbol);         }
   CArrayObj           *ChartGetChartsList(const ENUM_TIMEFRAMES timeframe)            { return this.m_charts.GetChartsList(timeframe);      }

переименуем для соответствия аналогичным методам других классов и переместим чуть выше в листинге:

//--- Создаёт коллекцию чартов
   bool                 ChartCreateCollection(void)                                    { return this.m_charts.CreateCollection();            }
//--- Возвращает (1) коллекцию чартов, (2) список чартов из коллекции чартов
   CChartObjCollection *GetChartObjCollection(void)                                    { return &this.m_charts;                              }
   CArrayObj           *GetListCharts(void)                                            { return this.m_charts.GetList();                     }
//--- Возвращает список объектов-чартов по (1) символу, (2) таймфрейму
   CArrayObj           *GetListCharts(const string symbol)                             { return this.m_charts.GetChartsList(symbol);         }
   CArrayObj           *GetListCharts(const ENUM_TIMEFRAMES timeframe)                 { return this.m_charts.GetChartsList(timeframe);      }

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

//--- Создаёт коллекцию чартов
   bool                 ChartCreateCollection(void)                                    { return this.m_charts.CreateCollection();            }
//--- Возвращает (1) коллекцию чартов, (2) список чартов из коллекции чартов
   CChartObjCollection *GetChartObjCollection(void)                                    { return &this.m_charts;                              }
   CArrayObj           *GetListCharts(void)                                            { return this.m_charts.GetList();                     }
//--- Возвращает список объектов-чартов по (1) символу, (2) таймфрейму
   CArrayObj           *GetListCharts(const string symbol)                             { return this.m_charts.GetChartsList(symbol);         }
   CArrayObj           *GetListCharts(const ENUM_TIMEFRAMES timeframe)                 { return this.m_charts.GetChartsList(timeframe);      }

//--- Возвращает (1) указанный объект-чарт, (2) объект-чарт с программой, (3) объект-чарт последнего открытого графика
   CChartObj           *ChartGetChartObj(const long chart_id)                          { return this.m_charts.GetChart(chart_id);            }
   CChartObj           *ChartGetMainChart(void)                                        { return this.m_charts.GetChart(this.m_charts.GetMainChartID());}
   CChartObj           *ChartGetLastOpenedChart(void)                                  { return this.m_charts.GetChart(this.GetListCharts().Total()-1);}
   
//--- Возвращает количество чартов в списке-коллекции
   int                  ChartsTotal(void)                                              { return this.m_charts.DataTotal();                   }

//--- Обновляет (1) указанный по идентификатору чарт, (2) всех чарты
   void                 ChartRefresh(const long chart_id)                              { this.m_charts.Refresh(chart_id);                    }
   void                 ChartsRefreshAll(void)                                         { this.m_charts.Refresh();                            }

//--- (1) Открывает, (2) закрывает указанный график
   bool                 ChartOpen(const string symbol,const ENUM_TIMEFRAMES timeframe) { return this.m_charts.Open(symbol,timeframe);        }
   bool                 ChartClose(const long chart_id)                                { return this.m_charts.Close(chart_id);               }
   
//--- Возвращает (1) коллекцию буферов, (2) список буферов из коллекции 

Метод ChartGetLastOpenedChart() просто возвращает указатель на самый последний объект, находящийся в списке коллекции объектов-чартов,
а метод ChartsTotal() возвращает размер списка коллекции объектов-чартов.
Методы ChartOpen() и ChartClose() возвращают результат работы методов Open() и Close() класса-коллекции чартов соответственно.

Это все изменения и доработки, запланированные нами на сегодня.


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

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

Что мы сделаем. К панельке советника добавим новые кнопки с такими значками:

Логика тестирования нового функционала будет такой:

Итак, в обработчике OnInit() советника впишем включение разрешения на отслеживание событий мыши для текущего графика:

//--- Проверка воспроизведения стандартного звука по макроподстановке и пользовательского звука по описанию
   engine.PlaySoundByDescription(SND_OK);
//--- Ждём 600 миллисекунд
   engine.Pause(600);
   engine.PlaySoundByDescription(TextByLanguage("Звук упавшей монетки 2","The sound of a falling coin 2"));


//--- Проверка расчёта координат курсора в окнах графика.
//--- Установим текущему графику разрешение на отслеживание событий перемещения мыши
   engine.ChartGetMainChart().SetEventMouseMoveON();
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Это необходимо чтобы программа могла получать сообщения о событиях перемещения и нажатия кнопок мышки (CHARTEVENT_MOUSE_MOVE).

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

Напишем такую обработку события перемещения курсора:

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Если работа в тестере - выход
   if(MQLInfoInteger(MQL_TESTER))
      return;
//--- Обработка событий мыши
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Обработка нажатий кнопок в панели
      if(StringFind(sparam,"BUTT_")>0)
         PressButtonEvents(sparam);
     }
//--- Обработка событий библиотеки DoEasy
   if(id>CHARTEVENT_CUSTOM-1)
     {
      OnDoEasyEvent(id,lparam,dparam,sparam);
     }
//--- Проверка ChartXYToTimePrice()
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Получим объект-чарт текущего (главного) графика программы
      CChartObj *chart=engine.ChartGetMainChart();
      if(chart==NULL)
         return;
      //--- Получим номер подокна, в котором находится курсор
      int wnd_num=chart.XYToTimePrice(lparam,dparam);
      if(wnd_num==WRONG_VALUE)
         return;
      //--- Получим рассчитанные время и цену расположения курсора
      datetime time=chart.TimeFromXY();
      double price=chart.PriceFromXY();
      //--- Получим объект-окно чарта, в котором расположен курсор, по номеру подокна
      CChartWnd *wnd=chart.GetWindowByNum(wnd_num);
      if(wnd==NULL)
         return;
      //--- Если координаты X и Y рассчитаны по времени и цене (делаем обратное предыдущему преобразование),
      if(wnd.TimePriceToXY(time,price))
        {
         //--- то выведем в комментарии рассчитанные по X и Y курсора время, цену и номер окна,
         //--- а также преобразованные обратно из времени и цены координаты X и Y курсора
         Comment
           (
            DFUN,"time: ",TimeToString(time),", price: ",DoubleToString(price,Digits()),
            ", win num: ",(string)wnd_num,": x: ",(string)wnd.XFromTimePrice(),
            ", y: ",(string)wnd.YFromTimePrice()," (",(string)wnd.YFromTimePriceRelative(),")")
           ;
        }
     }
  }
//+------------------------------------------------------------------+

Логика обработки события перемещения мыши подробно расписана в коде OnChartEvent() советника.

В функции советника CreateButtons() добавим код для создания новых кнопок панели:

//+------------------------------------------------------------------+
//| Создаёт панель кнопок                                            |
//+------------------------------------------------------------------+
bool CreateButtons(const int shift_x=20,const int shift_y=0)
  {
   int h=18,w=82,offset=2,wpt=14;
   int cx=offset+shift_x+wpt*2+2,cy=offset+shift_y+(h+1)*(TOTAL_BUTT/2)+3*h+1;
   int x=cx,y=cy;
   int shift=0;
   for(int i=0;i<TOTAL_BUTT;i++)
     {
      x=x+(i==7 ? w+2 : 0);
      if(i==TOTAL_BUTT-6) x=cx;
      y=(cy-(i-(i>6 ? 7 : 0))*(h+1));
      if(!ButtonCreate(butt_data[i].name,x,y,(i<TOTAL_BUTT-6 ? w : w*2+2),h,butt_data[i].text,(i<4 ? clrGreen : i>6 && i<11 ? clrRed : clrBlue)))
        {
         Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text);
         return false;
        }
     }
   
   h=18; offset=2;
   cx=offset+shift_x; cy=offset+shift_y+(h+1)*(TOTAL_BUTT/2)+3*h+1;
   x=cx; y=cy;
   shift=0;
   for(int i=0;i<18;i++)
     {
      y=(cy-(i-(i>6 ? 7 : 0))*(h+1));
      if(!ButtonCreate(butt_data[i].name+"_PRICE",((i>6 && i<14) || i>17 ? x+wpt*2+w*2+5 : x),y,wpt,h,"P",(i<4 ? clrGreen : i>6 && i<11 ? clrChocolate : clrBlue)))
        {
         Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text+" \"P\"");
         return false;
        }
      if(!ButtonCreate(butt_data[i].name+"_TIME",((i>6 && i<14) || i>17 ? x+wpt*2+w*2+5+wpt+1 : x+wpt+1),y,wpt,h,"T",(i<4 ? clrGreen : i>6 && i<11 ? clrChocolate : clrBlue)))
        {
         Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text+" \"T\"");
         return false;
        }
     }
   //--- Кнопки "Влево", "Вправо"
   int xbn=x+wpt*2+w*2+5;
   int ybn=y+h*3+3;
   if(!ButtonCreate(prefix+"BUTT_NAVIGATE_LEFT1",xbn,ybn,wpt,h,"<",clrGreen))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_LEFT1");
      return false;
     }
   if(!ButtonCreate(prefix+"BUTT_NAVIGATE_RIGHT1",xbn+wpt+1,ybn,wpt,h,">",clrGreen))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_RIGHT1");
      return false;
     }
   //--- Кнопки "Влево 10", "Вправо 10"
   ybn=y+h*2+2;
   if(!ButtonCreate(prefix+"BUTT_NAVIGATE_LEFT10",xbn,ybn,wpt,h,"<<",clrGreen))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_LEFT10");
      return false;
     }
   if(!ButtonCreate(prefix+"BUTT_NAVIGATE_RIGHT10",xbn+wpt+1,ybn,wpt,h,">>",clrGreen))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_RIGHT10");
      return false;
     }
   //--- Кнопки "Home", "End"
   ybn=y+h+1;
   if(!ButtonCreate(prefix+"BUTT_NAVIGATE_HOME",xbn,ybn,wpt,h,"|<",clrGreen))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_HOME");
      return false;
     }
   if(!ButtonCreate(prefix+"BUTT_NAVIGATE_END",xbn+wpt+1,ybn,wpt,h,">|",clrGreen))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_END");
      return false;
     }
   //--- Кнопки "Open", "Close"
   ybn=y;
   if(!ButtonCreate(prefix+"BUTT_CHART_OPEN",xbn,ybn,wpt,h,"N",clrBlue))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_CHART_OPEN");
      return false;
     }
   if(!ButtonCreate(prefix+"BUTT_CHART_CLOSE",xbn+wpt+1,ybn,wpt,h,"X",clrRed))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_CHART_CLOSE");
      return false;
     }
   //--- Кнопка ScreenShot
   ybn=y-h-1;
   if(!ButtonCreate(prefix+"BUTT_CHART_SCREENSHOT",xbn,ybn,wpt*2+offset,h,"[O]",clrBlue))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_CHART_SCREENSHOT");
      return false;
     }
   
   ChartRedraw(0);
   return true;
  }
//+------------------------------------------------------------------+

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

В функции обработки нажатий кнопок PressButtonEvents() впишем обработку нажатий на новые кнопки:

//+------------------------------------------------------------------+
//| Обработка нажатий кнопок                                         |
//+------------------------------------------------------------------+
void PressButtonEvents(const string button_name)
  {
   bool comp_magic=true;   // Временная переменная для выбора использования составного магика со случайными идентификаторами групп
   string comment="";
   //--- Преобразуем имя кнопки в её строковый идентификатор
   string button=StringSubstr(button_name,StringLen(prefix));
   //--- Случайные номера групп 1 и 2 в диапазоне 0 - 15
   group1=(uchar)Rand();
   group2=(uchar)Rand();
   uint magic=(comp_magic ? engine.SetCompositeMagicNumber(magic_number,group1,group2) : magic_number);
   //--- Если кнопка в нажатом состоянии
   if(ButtonState(button_name))
     {
      //--- Если нажата кнопка смещения графика влево на 1 бар
      if(button=="BUTT_NAVIGATE_LEFT1")
        {
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
            chart.NavigateLeft(1);
        }
      //--- Если нажата кнопка смещения графика вправо на 1 бар
      if(button=="BUTT_NAVIGATE_RIGHT1")
        {
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
            chart.NavigateRight(1);
        }
      //--- Если нажата кнопка смещения графика влево на 10 баров
      if(button=="BUTT_NAVIGATE_LEFT10")
        {
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
            chart.NavigateLeft(10);
        }
      //--- Если нажата кнопка смещения графика вправо на 10 баров
      if(button=="BUTT_NAVIGATE_RIGHT10")
        {
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
            chart.NavigateRight(10);
        }
      //--- Если нажата кнопка смещения графика в начало истории
      if(button=="BUTT_NAVIGATE_HOME")
        {
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
            chart.NavigateBegin();
        }
      //--- Если нажата кнопка смещения графика в конец истории
      if(button=="BUTT_NAVIGATE_END")
        {
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
            chart.NavigateEnd();
        }
      //--- Если нажата кнопка открытия нового графика
      if(button=="BUTT_CHART_OPEN")
        {
         int total_charts=engine.ChartsTotal();
         static int first_index=total_charts;
         string name=SymbolName(total_charts-first_index,true);
         if(engine.ChartOpen(name,PERIOD_CURRENT))
           {
            engine.ChartsRefreshAll();
            CChartObj *chart=engine.ChartGetMainChart();
            if(chart!=NULL)
               chart.SetBringToTopON(true);
           }
         //--- Этот блок кода нужен только для данного теста, и только при условии наличия открытого чарта GBPUSD
         //--- При этом, чарт GBPUSD должен отличаться настройками от чартов, открываемых по умолчанию
         CArrayObj *list_gbpusd=engine.GetListCharts("GBPUSD");
         if(list_gbpusd!=NULL && list_gbpusd.Total()>0)
           {
            CChartObj *chart=list_gbpusd.At(0);
            if(chart.SaveTemplate())
              {
               chart=engine.ChartGetLastOpenedChart();
               if(chart!=NULL)
                  chart.ApplyTemplate();
              }
           }
         //--- Конец тестового блока кода
        }
      //--- Если нажата кнопка закрытия последнего графика
      if(button=="BUTT_CHART_CLOSE")
        {
         CArrayObj *list_charts=engine.GetListCharts();
         if(list_charts!=NULL)
           {
            list_charts.Sort(SORT_BY_CHART_ID);
            CChartObj *chart=list_charts.At(list_charts.Total()-1);
            if(chart!=NULL && !chart.IsMainChart())
              engine.ChartClose(chart.ID());
           }
        }
      //--- Если нажата кнопка ScreenShot
      if(button=="BUTT_CHART_SCREENSHOT")
        {
         static int num=0;
         if(++num>3) num=1;
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
           {
            switch(num)
              {
               case 1 : chart.ScreenShot800x600(); break;
               case 2 : chart.ScreenShot750x562(); break;
               default: chart.ScreenShotWndSize(); break;
              }
           }
        }
      
      //--- Если нажата кнопка BUTT_BUY: Открыть позицию Buy
      if(button==EnumToString(BUTT_BUY))
        {
         ...
         ...
         ...
       ...
       ...
     ...
     ...

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

Это все доработки, которые нам необходимо было сделать в новом тестовом советнике.

Скомпилируем советник, и запустим его на графике любого символа с настройками "Использовать текущий символ" и "Использовать текущий таймфрейм":


Перед запуском советника обязательно откройте новый график символа GBPUSD и настройте его внешний вид иначе, чем у графиков, открываемых по умолчанию при использовании шаблона default.tpl, например, так (график GBPUSD был открыт заранее):



Теперь можно протестировать новый функционал библиотеки нажатием кнопок панели:


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

CChartObj::SaveTemplate: Chart template saved: GBPUSD H4
CChartObj::ApplyTemplate: Template applied to the chart: USDCHF H1
CChartObj::SaveTemplate: Chart template saved: GBPUSD H4
CChartObj::ApplyTemplate: Template applied to the chart: GBPUSD H1
CChartObj::SaveTemplate: Chart template saved: GBPUSD H4
CChartObj::ApplyTemplate: Template applied to the chart: EURUSD H1
CChartObj::SaveTemplate: Chart template saved: GBPUSD H4
CChartObj::ApplyTemplate: Template applied to the chart: USDRUB H1
CChartObj::SaveTemplate: Chart template saved: GBPUSD H4
CChartObj::ApplyTemplate: Template applied to the chart: EURJPY H1
CChartObj::SaveTemplate: Chart template saved: GBPUSD H4
CChartObj::ApplyTemplate: Template applied to the chart: EURGBP H1
CChartObjCollection::Close: Wrong chart ID (4101)

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

//--- Параметры таймера коллекции чартов
#define COLLECTION_CHARTS_PAUSE        (500)                      // Пауза таймера коллекции чартов в миллисекундах
#define COLLECTION_CHARTS_COUNTER_STEP (16)                       // Шаг приращения счётчика таймера чартов
#define COLLECTION_CHARTS_COUNTER_ID   (9)                        // Идентификатор счётчика таймера чартов

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

Осталось протестировать создание скриншотов текущего графика. Каждое нажатие кнопки будет создавать скриншот графика в определённом размере. Первое нажатие — скриншот 800x600, второе нажатие — скриншот 750x562, третье нажатие — скриншот в текущем размере графика:


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

CChartObj::ScreenShot800x600: Screenshot created: DoEasy\ScreenShots\TestDoEasyPart70_EURUSD_H1_2021.04.13_14.02.25.png (800 x 600)
CChartObj::ScreenShot750x562: Screenshot created: DoEasy\ScreenShots\TestDoEasyPart70_EURUSD_H1_2021.04.13_14.02.28.png (750 x 562)
CChartObj::ScreenShotWndSize: Screenshot created: DoEasy\ScreenShots\TestDoEasyPart70_EURUSD_H1_2021.04.13_14.02.29.png (726 x 321)

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

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


Что дальше

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

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

К содержанию

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

Прочие классы в библиотеке DoEasy (Часть 67): Класс объекта-чарта
Прочие классы в библиотеке DoEasy (Часть 68): Класс объекта-окна графика и классы объектов-индикаторов в окне графика
Прочие классы в библиотеке DoEasy (Часть 69): Класс-коллекция объектов-чартов