Графика в библиотеке DoEasy (Часть 87): Коллекция графических объектов - контроль модификации свойств объектов на всех открытых графиках

Artyom Trishkin | 5 ноября, 2021

Содержание


Концепция

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

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

Своё событие мы отправить можем — это пользовательское событие, отправляемое на график нашей программы посредством функции EventChartCustom().
Эта функция может генерировать пользовательское событие для указанного в ней графика. При этом, отправляя идентификатор события на указанный график, функция автоматически добавляет к его значению величину константы CHARTEVENT_CUSTOM. Получив такое событие в библиотеке, нам остаётся вычесть из него это добавленное значение, и мы узнаем, что же за событие произошло на каком-то другом графике. Чтобы понять, на каком графике произошло событие, мы можем указать его (графика) идентификатор в long-параметре события lparam. Тогда, видя, что параметр lparam имеет значение (по умолчанию для событий графических объектов lparam и dparam не имеют значений — равны нулю), мы уже понимаем, что это событие пришло с другого графика — отнимаем от параметра id, тоже передаваемого в событии, значение CHARTEVENT_CUSTOM, и получаем идентификатор события. А вот с каким объектом произошло это событие мы узнаем из параметра sparam — в нём передаётся имя объекта.

Таким образом, мы определились, что события с других графиков мы отправлять всё же можем на график нашей программы. Определить, что же это за событие мы тоже можем — по идентификатору события (id), определить график можем по параметру lparam, а имя объекта — по параметру sparam. Но теперь нам нужно придумать, чем мы эти события будем контролировать на других графиках — ведь программа-то запущена на одном графике, а получать события и отправлять их в библиотеку и на график программы мы должны с других графиков. И на этих других графиках должна работать программа, о которой знает наша библиотека и может её запускать.

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

Индикатор не будет иметь рисуемых буферов, и всё, что будет делать, — это в обработчике OnChartEvent() отслеживать два события графических объектов CHARTEVENT_OBJECT_CHANGE и CHARTEVENT_OBJECT_DRAG и отправлять их на график программы как пользовательское событие, которое нам останется определить и обработать в библиотеке.

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

Файлы библиотеки, в которых было произведено переименование переменной:

DataTick.mqh, Symbol.mqh, Bar.mqh, PendRequest.mqh, Order.mqh, MQLSignal.mqh, IndicatorDE.mqh, DataInd.mqh, Buffer.mqh, GCnvElement.mqh, Event.mqh, ChartWnd.mqh, ChartObj.mqh, MarketBookOrd.mqh, Account.mqh, GStdGraphObj.mqh.


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

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

   MSG_GRAPH_OBJ_TEXT_ANCHOR_LEFT_UPPER,              // Точка привязки в левом верхнем углу
   MSG_GRAPH_OBJ_TEXT_ANCHOR_LEFT,                    // Точка привязки слева по центру
   MSG_GRAPH_OBJ_TEXT_ANCHOR_LEFT_LOWER,              // Точка привязки в левом нижнем углу
   MSG_GRAPH_OBJ_TEXT_ANCHOR_LOWER,                   // Точка привязки снизу по центру
   MSG_GRAPH_OBJ_TEXT_ANCHOR_RIGHT_LOWER,             // Точка привязки в правом нижнем углу
   MSG_GRAPH_OBJ_TEXT_ANCHOR_RIGHT,                   // Точка привязки справа по центру
   MSG_GRAPH_OBJ_TEXT_ANCHOR_RIGHT_UPPER,             // Точка привязки в правом верхнем углу
   MSG_GRAPH_OBJ_TEXT_ANCHOR_UPPER,                   // Точка привязки сверху по центру
   MSG_GRAPH_OBJ_TEXT_ANCHOR_CENTER,                  // Точка привязки строго по центру объекта

//--- CGraphElementsCollection
   MSG_GRAPH_OBJ_FAILED_GET_ADDED_OBJ_LIST,           // Не удалось получить список вновь добавленных объектов
   MSG_GRAPH_OBJ_FAILED_DETACH_OBJ_FROM_LIST,         // Не удалось изъять графический объект из списка
   
   MSG_GRAPH_OBJ_CREATE_EVN_CTRL_INDICATOR,           // Создан индикатор контроля и отправки событий
   MSG_GRAPH_OBJ_FAILED_CREATE_EVN_CTRL_INDICATOR,    // Не удалось создать индикатор контроля и отправки событий
   MSG_GRAPH_OBJ_CLOSED_CHARTS,                       // Закрыто окон графиков:
   MSG_GRAPH_OBJ_OBJECTS_ON_CLOSED_CHARTS,            // С ними удалено объектов:
   
  };
//+------------------------------------------------------------------+

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

   {"Точка привязки в левом верхнем углу","Anchor point at the upper left corner"},
   {"Точка привязки слева по центру","Anchor point to the left in the center"},
   {"Точка привязки в левом нижнем углу","Anchor point at the lower left corner"},
   {"Точка привязки снизу по центру","Anchor point below in the center"},
   {"Точка привязки в правом нижнем углу","Anchor point at the lower right corner"},
   {"Точка привязки справа по центру","Anchor point to the right in the center"},
   {"Точка привязки в правом верхнем углу","Anchor point at the upper right corner"},
   {"Точка привязки сверху по центру","Anchor point above in the center"},
   {"Точка привязки строго по центру объекта","Anchor point strictly in the center of the object"},

//--- CGraphElementsCollection
   {"Не удалось получить список вновь добавленных объектов","Failed to get the list of newly added objects"},
   {"Не удалось изъять графический объект из списка","Failed to detach graphic object from the list"},
   
   {"Создан индикатор контроля и отправки событий","An indicator for monitoring and sending events has been created"},
   {"Не удалось создать индикатор контроля и отправки событий","Failed to create indicator for monitoring and sending events"},
   {"Закрыто окон графиков: ","Closed chart windows: "},
   {"С ними удалено объектов: ","Objects removed with them: "},
   
  };
//+---------------------------------------------------------------------+


Метод Symbol() в файле \MQL5\Include\DoEasy\Objects\Graph\Standard\GStdGraphObj.mqh класса абстрактного стандартного графического объекта возвращает символ графического объекта "График":

//--- Символ для объекта "График" 
   string            Symbol(void)                  const { return this.GetProperty(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL);                      }
   void              SetChartObjSymbol(const string symbol)
                       {
                        if(::ObjectSetString(CGBaseObj::ChartID(),CGBaseObj::Name(),OBJPROP_SYMBOL,symbol))
                           this.SetProperty(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL,symbol);
                       }

Но так как у нас все методы, работающие с графическим объектом "График", имеют в своём названии префикс "ChartObj", то для согласованности с другими методами переименуем и этот:

//--- Символ для объекта "График" 
   string            ChartObjSymbol(void)          const { return this.GetProperty(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL);                      }
   void              SetChartObjSymbol(const string symbol)
                       {
                        if(::ObjectSetString(CGBaseObj::ChartID(),CGBaseObj::Name(),OBJPROP_SYMBOL,symbol))
                           this.SetProperty(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL,symbol);
                       }
//--- Возвращает флаги видимости объекта на таймфреймах

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

//+------------------------------------------------------------------+
//| Сравнивает объекты CGStdGraphObj между собой по всем свойствам   |
//+------------------------------------------------------------------+
bool CGStdGraphObj::IsEqual(CGStdGraphObj *compared_obj) const
  {
   int begin=0, end=GRAPH_OBJ_PROP_INTEGER_TOTAL;
   for(int i=begin; i<end; i++)
     {
      ENUM_GRAPH_OBJ_PROP_INTEGER prop=(ENUM_GRAPH_OBJ_PROP_INTEGER)i;
      if(this.GetProperty(prop)!=compared_obj.GetProperty(prop)) return false; 
     }
   begin=end; end+=GRAPH_OBJ_PROP_DOUBLE_TOTAL;
   for(int i=begin; i<end; i++)
     {
      ENUM_GRAPH_OBJ_PROP_DOUBLE prop=(ENUM_GRAPH_OBJ_PROP_DOUBLE)i;
      if(this.GetProperty(prop)!=compared_obj.GetProperty(prop)) return false; 
     }
   begin=end; end+=GRAPH_OBJ_PROP_STRING_TOTAL;
   for(int i=begin; i<end; i++)
     {
      ENUM_GRAPH_OBJ_PROP_STRING prop=(ENUM_GRAPH_OBJ_PROP_STRING)i;
      if(this.GetProperty(prop)!=compared_obj.GetProperty(prop)) return false; 
     }
   return true;
  }
//+------------------------------------------------------------------+
//| Выводит в журнал свойства объекта                                |
//+------------------------------------------------------------------+
void CGStdGraphObj::Print(const bool full_prop=false,const bool dash=false)
  {
   ::Print("============= ",CMessage::Text(MSG_LIB_PARAMS_LIST_BEG)," (",this.Header(),") =============");
   int begin=0, end=GRAPH_OBJ_PROP_INTEGER_TOTAL;
   for(int i=begin; i<end; i++)
     {
      ENUM_GRAPH_OBJ_PROP_INTEGER prop=(ENUM_GRAPH_OBJ_PROP_INTEGER)i;
      if(!full_prop && !this.SupportProperty(prop)) continue;
      ::Print(this.GetPropertyDescription(prop));
     }
   ::Print("------");
   begin=end; end+=GRAPH_OBJ_PROP_DOUBLE_TOTAL;
   for(int i=begin; i<end; i++)
     {
      ENUM_GRAPH_OBJ_PROP_DOUBLE prop=(ENUM_GRAPH_OBJ_PROP_DOUBLE)i;
      if(!full_prop && !this.SupportProperty(prop)) continue;
      ::Print(this.GetPropertyDescription(prop));
     }
   ::Print("------");
   begin=end; end+=GRAPH_OBJ_PROP_STRING_TOTAL;
   for(int i=begin; i<end; i++)
     {
      ENUM_GRAPH_OBJ_PROP_STRING prop=(ENUM_GRAPH_OBJ_PROP_STRING)i;
      if(!full_prop && !this.SupportProperty(prop)) continue;
      ::Print(this.GetPropertyDescription(prop));
     }
   ::Print("============= ",CMessage::Text(MSG_LIB_PARAMS_LIST_END)," (",this.Header(),") =============\n");
  }
//+------------------------------------------------------------------+

...

//+------------------------------------------------------------------+
//| Проверяет изменения свойств объекта                              |
//+------------------------------------------------------------------+
void CGStdGraphObj::PropertiesCheckChanged(void)
  {
   bool changed=false;
   int begin=0, end=GRAPH_OBJ_PROP_INTEGER_TOTAL;
   for(int i=begin; i<end; i++)
     {
      ENUM_GRAPH_OBJ_PROP_INTEGER prop=(ENUM_GRAPH_OBJ_PROP_INTEGER)i;
      if(!this.SupportProperty(prop)) continue;
      if(this.GetProperty(prop)!=this.GetPropertyPrev(prop))
        {
         changed=true;
         ::Print(DFUN,this.Name(),": ",TextByLanguage(" Изменённое свойство: "," Modified property: "),GetPropertyDescription(prop));
        }
     }

   begin=end; end+=GRAPH_OBJ_PROP_DOUBLE_TOTAL;
   for(int i=begin; i<end; i++)
     {
      ENUM_GRAPH_OBJ_PROP_DOUBLE prop=(ENUM_GRAPH_OBJ_PROP_DOUBLE)i;
      if(!this.SupportProperty(prop)) continue;
      if(this.GetProperty(prop)!=this.GetPropertyPrev(prop))
        {
         changed=true;
         ::Print(DFUN,this.Name(),": ",TextByLanguage(" Изменённое свойство: "," Modified property: "),GetPropertyDescription(prop));
        }
     }

   begin=end; end+=GRAPH_OBJ_PROP_STRING_TOTAL;
   for(int i=begin; i<end; i++)
     {
      ENUM_GRAPH_OBJ_PROP_STRING prop=(ENUM_GRAPH_OBJ_PROP_STRING)i;
      if(!this.SupportProperty(prop)) continue;
      if(this.GetProperty(prop)!=this.GetPropertyPrev(prop))
        {
         changed=true;
         ::Print(DFUN,this.Name(),": ",TextByLanguage(" Изменённое свойство: "," Modified property: "),GetPropertyDescription(prop));
        }
     }
   if(changed)
      PropertiesCopyToPrevData();
  }
//+------------------------------------------------------------------+


Индикатор для рассылки сообщений об изменении свойств объектов на всех графиках

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

Наш индикатор должен знать:

  1. идентификатор графика, на котором он запущен (назовём его SourseID) и
  2. идентификатор графика, на который он должен отправлять пользовательские события (DestinationID).
Если с DestinationID всё однозначно — его мы должны указать во входных параметрах индикатора, то с параметром SourseID есть некоторые нюансы.

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

Итак, в навигаторе редактора в папке \MQL5\Indicators\ создадим новую папку DoEasy\


а в ней — новый пользовательский индикатор EventControl.mq5.



При создании укажем два входных параметра типа long с начальным значением 0:


На следующем шаге работы мастера укажем необходимость включения в код индикатора обработчика OnChartEvent(), установив соответствующую галочку:


На следующем шаге оставим все поля и чекбоксы пустыми и нажмём кнопку "Готово":


Всё, наш индикатор создан.

Если сейчас его скомпилировать, то получим предупреждение, что у индикатора нет ни одного рисуемого буфера:


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

//+------------------------------------------------------------------+
//|                                                 EventControl.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"
#property indicator_chart_window
#property indicator_plots 0
//--- input parameters
input long     InpChartSRC = 0;
input long     InpChartDST = 0;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator shortname
   IndicatorSetString(INDICATOR_SHORTNAME,"EventSend_From#"+(string)InpChartSRC+"_To#"+(string)InpChartDST);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Короткое имя будет содержать в себе:

В обработчике OnCalculate() мы ничего не делаем — просто возвращаем значение количества баров графика:

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   return rates_total;
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if(id==CHARTEVENT_OBJECT_CHANGE || id==CHARTEVENT_OBJECT_DRAG)
     {
      EventChartCustom(InpChartDST,(ushort)id,InpChartSRC,dparam,sparam);
     }
  }
//+------------------------------------------------------------------+

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

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

Файл индикатора можно посмотреть в прилагаемых к библиотеке файлах в конце статьи.

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

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

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2021, MetaQuotes Software Corp. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2021, MetaQuotes Software Corp."
#property link      "https://mql5.com/ru/users/artmedia70"
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "DataSND.mqh"
#include "DataIMG.mqh"
#include "Data.mqh"
#ifdef __MQL4__
#include "ToMQL4.mqh"
#endif 
//+------------------------------------------------------------------+
//| Ресурсы                                                          |
//+------------------------------------------------------------------+
#define PATH_TO_EVENT_CTRL_IND         "Indicators\\DoEasy\\EventControl.ex5"
//+------------------------------------------------------------------+
//| Макроподстановки                                                 |
//+------------------------------------------------------------------+

По этой макроподстановке будем далее получать путь к скомпилированному файлу индикатора в ресурсах библиотеки.

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

//+------------------------------------------------------------------+
//| Данные для работы с графическими элементами                      |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Список возможных событий графических объектов                    |
//+------------------------------------------------------------------+
enum ENUM_GRAPH_OBJ_EVENT
  {
   GRAPH_OBJ_EVENT_NO_EVENT = CHART_OBJ_EVENTS_NEXT_CODE,// Нет события
   GRAPH_OBJ_EVENT_CREATE,                            // Событие "Создание нового графического объекта"
   GRAPH_OBJ_EVENT_CHANGE,                            // Событие "Изменение свойств графического объекта"
   GRAPH_OBJ_EVENT_MOVE,                              // Событие "Перемещение графического объекта"
   GRAPH_OBJ_EVENT_RENAME,                            // Событие "Переименование графического объекта"
   GRAPH_OBJ_EVENT_DELETE,                            // Событие "Удаление графического объекта"
  };
#define GRAPH_OBJ_EVENTS_NEXT_CODE  (GRAPH_OBJ_EVENT_DELETE+1)  // Код следующего события после последнего кода события графических объектов
//+------------------------------------------------------------------+
//| Список способов привязки                                         |
//| (выравнивание текста по горизонтали и вертикали)                 |
//+------------------------------------------------------------------+

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


Обработка сигналов индикатора о событиях изменения свойств объектов

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

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

В файле \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh в приватной секции класса CChartObjectsControl объявим три новые переменные:

//+------------------------------------------------------------------+
//| Класс управления объектами чарта                                 |
//+------------------------------------------------------------------+
class CChartObjectsControl : public CObject
  {
private:
   CArrayObj         m_list_new_graph_obj;      // Список добавленных графических объектов
   ENUM_TIMEFRAMES   m_chart_timeframe;         // Период графика
   long              m_chart_id;                // Идентификатор графика
   long              m_chart_id_main;           // Идентификатор графика управляющей программы
   string            m_chart_symbol;            // Символ графика
   bool              m_is_graph_obj_event;      // Флаг события в списке графических объектов
   int               m_total_objects;           // Количество графических объектов
   int               m_last_objects;            // Количество графических объектов на прошлой проверке
   int               m_delta_graph_obj;         // Разница в количестве графических объектов по сравнению с прошлой проверкой
   int               m_handle_ind;              // Хендл индикатора-контроллера событий
   string            m_name_ind;                // Короткое имя индикатора-контроллера событий
   
//--- Возвращает имя последнего добавленного на график графического объекта
   string            LastAddedGraphObjName(void);
//--- Устанавливает для графика разрешение отслеживания событий мышки и графических объектов
   void              SetMouseEvent(void);
   
public:

Из описания переменных в комментариях понятно их назначение.

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

public:
//--- Возврат значений переменных
   ENUM_TIMEFRAMES   Timeframe(void)                           const { return this.m_chart_timeframe;    }
   long              ChartID(void)                             const { return this.m_chart_id;           }
   string            Symbol(void)                              const { return this.m_chart_symbol;       }
   bool              IsEvent(void)                             const { return this.m_is_graph_obj_event; }
   int               TotalObjects(void)                        const { return this.m_total_objects;      }
   int               Delta(void)                               const { return this.m_delta_graph_obj;    }
//--- Создаёт новый объект стандартного графического объекта
   CGStdGraphObj    *CreateNewGraphObj(const ENUM_OBJECT obj_type,const long chart_id, const string name);
//--- Возвращает список вновь добавленных объектов
   CArrayObj        *GetListNewAddedObj(void)                        { return &this.m_list_new_graph_obj;}
//--- Создаёт индикатор контроля событий
   bool              CreateEventControlInd(const long chart_id_main);
//--- Добавляет на чарт индикатор контроля событий
   bool              AddEventControlInd(void);
//--- Проверяет объекты на чарте
   void              Refresh(void);
//--- Конструкторы

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

//--- Проверяет объекты на чарте
   void              Refresh(void);
//--- Конструкторы
                     CChartObjectsControl(void)
                       { 
                        this.m_list_new_graph_obj.Clear();
                        this.m_list_new_graph_obj.Sort();
                        this.m_chart_id=::ChartID();
                        this.m_chart_timeframe=(ENUM_TIMEFRAMES)::ChartPeriod(this.m_chart_id);
                        this.m_chart_symbol=::ChartSymbol(this.m_chart_id);
                        this.m_chart_id_main=::ChartID();
                        this.m_is_graph_obj_event=false;
                        this.m_total_objects=0;
                        this.m_last_objects=0;
                        this.m_delta_graph_obj=0;
                        this.m_name_ind="";
                        this.m_handle_ind=INVALID_HANDLE;
                        this.SetMouseEvent();
                       }
                     CChartObjectsControl(const long chart_id)
                       { 
                        this.m_list_new_graph_obj.Clear();
                        this.m_list_new_graph_obj.Sort();
                        this.m_chart_id=chart_id;
                        this.m_chart_timeframe=(ENUM_TIMEFRAMES)::ChartPeriod(this.m_chart_id);
                        this.m_chart_symbol=::ChartSymbol(this.m_chart_id);
                        this.m_chart_id_main=::ChartID();
                        this.m_is_graph_obj_event=false;
                        this.m_total_objects=0;
                        this.m_last_objects=0;
                        this.m_delta_graph_obj=0;
                        this.m_name_ind="";
                        this.m_handle_ind=INVALID_HANDLE;
                        this.SetMouseEvent();
                       }
//--- Деструктор
                     ~CChartObjectsControl()
                       {
                        ::ChartIndicatorDelete(this.ChartID(),0,this.m_name_ind);
                        ::IndicatorRelease(this.m_handle_ind);
                       }
                     
//--- Сравнивает объекты CChartObjectsControl между собой по идентификатору графика (для сортировки списка по свойству объекта)
   virtual int       Compare(const CObject *node,const int mode=0) const
                       {
                        const CChartObjectsControl *obj_compared=node;
                        return(this.ChartID()>obj_compared.ChartID() ? 1 : this.ChartID()<obj_compared.ChartID() ? -1 : 0);
                       }

//--- Обработчик событий
   void              OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam);

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


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

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

//+------------------------------------------------------------------+
//| CChartObjectsControl: Создаёт индикатор контроля событий         |
//+------------------------------------------------------------------+
bool CChartObjectsControl::CreateEventControlInd(const long chart_id_main)
  {
   this.m_chart_id_main=chart_id_main;
   string name="::"+PATH_TO_EVENT_CTRL_IND;
   ::ResetLastError();
   this.m_handle_ind=::iCustom(this.Symbol(),this.Timeframe(),name,this.ChartID(),this.m_chart_id_main);
   if(this.m_handle_ind==INVALID_HANDLE)
     {
      CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_CREATE_EVN_CTRL_INDICATOR);
      CMessage::ToLog(DFUN,::GetLastError(),true);
      return false;
     }
   this.m_name_ind="EventSend_From#"+(string)this.ChartID()+"_To#"+(string)this.m_chart_id_main;
   Print
     (
      DFUN,this.Symbol()," ",TimeframeDescription(this.Timeframe()),": ",
      CMessage::Text(MSG_GRAPH_OBJ_CREATE_EVN_CTRL_INDICATOR)," \"",this.m_name_ind,"\""
     );
   return true;
  }
//+------------------------------------------------------------------+

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

Обратите внимание, что при задании пути к индикатору в ресурсах библиотеки, мы указываем перед строкой пути знак разрешения контекста "::".
Тогда как при создании ресурса мы будем указывать знак "\\".

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

//+------------------------------------------------------------------+
//|CChartObjectsControl: Добавляет на чарт индикатор контроля событий|
//+------------------------------------------------------------------+
bool CChartObjectsControl::AddEventControlInd(void)
  {
   if(this.m_handle_ind==INVALID_HANDLE)
      return false;
   return ::ChartIndicatorAdd(this.ChartID(),0,this.m_handle_ind);
  }
//+------------------------------------------------------------------+

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

Перед определением класса коллекции графических объектов укажем путь к ресурсу, в котором хранится индикатор:

//+------------------------------------------------------------------+
//| Коллекция графических объектов                                   |
//+------------------------------------------------------------------+
#resource "\\"+PATH_TO_EVENT_CTRL_IND;          // Индикатор контроля событий графических объектов, упакованный в ресурсы программы
class CGraphElementsCollection : public CBaseObj
  {

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

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

class CGraphElementsCollection : public CBaseObj
  {
private:
   CArrayObj         m_list_charts_control;     // Список объектов управления чартами
   CListObj          m_list_all_canv_elm_obj;   // Список всех графических элементов на канвасе
   CListObj          m_list_all_graph_obj;      // Список всех графических объектов
   bool              m_is_graph_obj_event;      // Флаг события в списке графических объектов
   int               m_total_objects;           // Количество графических объектов
   int               m_delta_graph_obj;         // Разница в количестве графических объектов по сравнению с прошлой проверкой
   
//--- Возвращает флаг наличия объекта класса графического элемента в списке-коллекции графических элементов
   bool              IsPresentGraphElmInList(const int id,const ENUM_GRAPH_ELEMENT_TYPE type_obj);
//--- Возвращает флаг наличия объекта класса графического объекта в списке-коллекции графических объектов
   bool              IsPresentGraphObjInList(const long chart_id,const string name);
//--- Возвращает флаг наличия графического объекта на графике по имени
   bool              IsPresentGraphObjOnChart(const long chart_id,const string name);
//--- Возвращает указатель на объект управления объектами указанного чарта
   CChartObjectsControl *GetChartObjectCtrlObj(const long chart_id);
//--- Создаёт новый объект управления графическими объектами указанного чарта и добавляет его в список
   CChartObjectsControl *CreateChartObjectCtrlObj(const long chart_id);
//--- Обновляет список графических объектов по идентификатору чарта
   CChartObjectsControl *RefreshByChartID(const long chart_id);
//--- Проверяет наличие окна графика
   bool              IsPresentChartWindow(const long chart_id);
//--- Обрабатывает удаление окна графика
   void              RefreshForExtraObjects(void);
//--- Возвращает первый свободный идентификатор графического (1) объекта, (2) элемента на канвасе
   long              GetFreeGraphObjID(void);
   long              GetFreeCanvElmID(void);
//--- Добавляет графический объект в коллекцию
   bool              AddGraphObjToCollection(const string source,CChartObjectsControl *obj_control);
//--- Находит объект, имеющийся в коллекции, но отсутствующий на графике
   CGStdGraphObj    *FindMissingObj(const long chart_id);
//--- Находит графический объект, имеющийся на графике, но отсутствующий в коллекции
   string            FindExtraObj(const long chart_id);
//--- Удаляет (1) указанный, (2) по идентификатору графика графический объект из списка-коллекции графических объектов
   bool              DeleteGraphObjFromList(CGStdGraphObj *obj);
   void              DeleteGraphObjectsFromList(const long chart_id);
//--- Удаляет объект управления графиками из списка
   bool              DeleteGraphObjCtrlObjFromList(CChartObjectsControl *obj);
  
public:

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

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

//+------------------------------------------------------------------+
//| Создаёт новый объект управления графическими объектами           |
//| указанного чарта и добавляет его в список                        |
//+------------------------------------------------------------------+
CChartObjectsControl *CGraphElementsCollection::CreateChartObjectCtrlObj(const long chart_id)
  {
//--- Создаём новый объект управления объектами графика по идентификатору
   CChartObjectsControl *obj=new CChartObjectsControl(chart_id);
//--- Если объект не создан - сообщаем об ошибке и возвращаем NULL
   if(obj==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_GRAPH_ELM_COLLECTION_ERR_FAILED_CREATE_CTRL_OBJ),(string)chart_id);
      return NULL;
     }
//--- Если объект не удалось добавить в список - сообщаем об ошибке, удаляем объект и возвращаем NULL
   if(!this.m_list_charts_control.Add(obj))
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
      delete obj;
      return NULL;
     }
   if(obj.ChartID()!=CBaseObj::GetMainChartID() && obj.CreateEventControlInd(CBaseObj::GetMainChartID()))
      obj.AddEventControlInd();
//--- Возвращаем указатель на созданный и добавленный в список объект
   return obj;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Обновляет список всех графических объектов                       |
//+------------------------------------------------------------------+
void CGraphElementsCollection::Refresh(void)
  {
   this.RefreshForExtraObjects();
//--- Объявим переменные для поиска графиков
   long chart_id=0;
   int i=0;
//--- В цикле по всем открытым графикам терминала (не более 100)
   while(i<CHARTS_MAX)
     {
      //--- Получим идентификатор графика
      chart_id=::ChartNext(chart_id);
      if(chart_id<0)
         break;
      //--- Получаем указатель на объект управления графическими объектами
      //--- и обновляем список графических объектов по идентификатору чарта
      CChartObjectsControl *obj_ctrl=this.RefreshByChartID(chart_id);
      //--- Если указатель получить не удалось - идём к следующему графику
      if(obj_ctrl==NULL)
         continue;
      //--- Если есть событие изменения количества объектов на графике
      if(obj_ctrl.IsEvent())
        {
         //--- Если добавлен графический объект на график
         if(obj_ctrl.Delta()>0)
           {
            //--- Получаем список добавленных графических объектов и перемещаем их в список-коллекцию
            //--- (если объект поместить в коллекцию не удалось - идём к следующему объекту)
            if(!AddGraphObjToCollection(DFUN_ERR_LINE,obj_ctrl))
               continue;
           }
         //--- Если удалён графический объект
         else if(obj_ctrl.Delta()<0)
           {
            // Найдём лишний объект в списке
            CGStdGraphObj *obj=this.FindMissingObj(chart_id);
            if(obj!=NULL)
              {
               //--- Выведем в журнал короткое описание найденного объекта, который удалён с графика
               obj.PrintShort();
               //--- Удалим объект класса удалённого графического объекта из списка-коллекции
               if(!this.DeleteGraphObjFromList(obj))
                  CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_DETACH_OBJ_FROM_LIST);
              }
           }
        }
      //--- Увеличиваем индекс цикла
      i++;
     }
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Проверяет наличие окна графика                                   |
//+------------------------------------------------------------------+
bool CGraphElementsCollection::IsPresentChartWindow(const long chart_id)
  {
   long chart=0;
   int i=0;
//--- В цикле по всем открытым графикам терминала (не более 100)
   while(i<CHARTS_MAX)
     {
      //--- Получим идентификатор графика
      chart=::ChartNext(chart);
      if(chart<0)
         break;
      if(chart==chart_id)
         return true;
      //--- Увеличиваем индекс цикла
      i++;
     }
   return false;
  }
//+------------------------------------------------------------------+

Здесь: в цикле по всем открытым графикам в терминале получаем идентификатор очередного графика и сравниваем его с переданным в метод.
Если идентификаторы совпадают — значит график есть, и возвращаем true
.
По завершении цикла возвращаем false — графика с указанным идентификатором нет.

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

//+------------------------------------------------------------------+
//| Обрабатывает удаление окна графика                               |
//+------------------------------------------------------------------+
void CGraphElementsCollection::RefreshForExtraObjects(void)
  {
   for(int i=this.m_list_charts_control.Total()-1;i>WRONG_VALUE;i--)
     {
      CChartObjectsControl *obj_ctrl=this.m_list_charts_control.At(i);
      if(obj_ctrl==NULL)
         continue;
      if(!this.IsPresentChartWindow(obj_ctrl.ChartID()))
        {
         long chart_id=obj_ctrl.ChartID();
         int total_ctrl=m_list_charts_control.Total();
         this.DeleteGraphObjCtrlObjFromList(obj_ctrl);
         int total_obj=m_list_all_graph_obj.Total();
         this.DeleteGraphObjectsFromList(chart_id);
         int del_ctrl=total_ctrl-m_list_charts_control.Total();
         int del_obj=total_obj-m_list_all_graph_obj.Total();
         Print
           (
            DFUN,CMessage::Text(MSG_GRAPH_OBJ_CLOSED_CHARTS),(string)del_ctrl,". ",
            CMessage::Text(MSG_GRAPH_OBJ_OBJECTS_ON_CLOSED_CHARTS),(string)del_obj
           );
        }
     }
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Удаляет графический объект по идентификатору графика             |
//| из списка-коллекции графических объектов                         |
//+------------------------------------------------------------------+
void CGraphElementsCollection::DeleteGraphObjectsFromList(const long chart_id)
  {
   CArrayObj *list=CSelect::ByGraphicStdObjectProperty(GetListGraphObj(),GRAPH_OBJ_PROP_CHART_ID,chart_id,EQUAL);
   if(list==NULL)
      return;
   for(int i=list.Total();i>WRONG_VALUE;i--)
     {
      CGStdGraphObj *obj=list.At(i);
      if(obj==NULL)
         continue;
      this.DeleteGraphObjFromList(obj);
     }
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Удаляет объект управления графиками из списка                    |
//+------------------------------------------------------------------+
bool CGraphElementsCollection::DeleteGraphObjCtrlObjFromList(CChartObjectsControl *obj)
  {
   this.m_list_charts_control.Sort();
   int index=this.m_list_charts_control.Search(obj);
   return(index==WRONG_VALUE ? false : m_list_charts_control.Delete(index));
  }
//+------------------------------------------------------------------+

Здесь: устанавливаем списку объектов управления графиками флаг сортированного списка, при помощи метода Search() находим индекс указанного объекта в списке и, если объекта нет в списке, — возвращаем false, иначе — возвращаем результат работы метода Delete().

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

Для получения идентификатора события графического объекта из пользовательского события нам нужно от значения полученного id вычесть значение CHARTEVENT_CUSTOM и вместе с проверкой id проверять рассчитанное значение идентификатора события в переменной idx.

Далее, если lparam не равен нулю — значит событие получено не с текущего графика, иначе — с текущего.
Остаётся заменить все вхождения ::ChartID() в коде на полученный chart_id:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CGraphElementsCollection::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   CGStdGraphObj *obj=NULL;
   ushort idx=ushort(id-CHARTEVENT_CUSTOM);
   if(id==CHARTEVENT_OBJECT_CHANGE || id==CHARTEVENT_OBJECT_DRAG || idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG)
     {
      //--- Получим идентификатор графика. Если lparam равен нулю -
      //--- значит событие с текущего графика,
      //--- иначе - пользовательское событие от индикатора
      long chart_id=(lparam==0 ? ::ChartID() : lparam);
      //--- Если объект, свойства которого изменены, или который был перемещён,
      //--- успешно получен из списка-коллекции по его имени, записанном в sparam
      obj=this.GetStdGraphObject(sparam,chart_id);
      if(obj!=NULL)
        {
         //--- Обновим свойства полученного объекта
         //--- и проверим их изменение
         obj.PropertiesRefresh();
         obj.PropertiesCheckChanged();
        }
      //--- Если объект не удалось получить по имени - он отсутствует в списке,
      //--- а значит его имя было изменено
      else
        {
         //--- Найдём в списке объект, которого нет на графике
         obj=this.FindMissingObj(chart_id);
         if(obj==NULL)
            return;
         //--- Получим имя переименованного  графического объекта на графике, которого нет в списке-коллекции
         string name_new=this.FindExtraObj(chart_id);
         //--- Установим новое имя объекту в списке-коллекции, которому не соответствует ни один графический объект на графике,
         //--- обновим свойства объекта и проверим их изменение
         obj.SetName(name_new);
         obj.PropertiesRefresh();
         obj.PropertiesCheckChanged();
        }
     }
  }
//+------------------------------------------------------------------+

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


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

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

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



Что дальше

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

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

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

К содержанию

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

Графика в библиотеке DoEasy (Часть 83): Класс абстрактного стандартного графического объекта
Графика в библиотеке DoEasy (Часть 84): Классы-наследники абстрактного стандартного графического объекта
Графика в библиотеке DoEasy (Часть 85): Коллекция графических объектов - добавляем вновь создаваемые
Графика в библиотеке DoEasy (Часть 86): Коллекция графических объектов - контролируем модификацию свойств