English 中文 Español Deutsch 日本語 Português
Графика в библиотеке DoEasy (Часть 95): Элементы управления составными графическими объектами

Графика в библиотеке DoEasy (Часть 95): Элементы управления составными графическими объектами

MetaTrader 5Примеры | 4 февраля 2022, 12:48
1 636 0
Artyom Trishkin
Artyom Trishkin

Содержание


Концепция

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

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

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

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


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

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

//--- CLinkedPivotPoint
   MSG_GRAPH_OBJ_EXT_NOT_ANY_PIVOTS_X,                // Для объекта не установлено ни одной опорной точки по оси X
   MSG_GRAPH_OBJ_EXT_NOT_ANY_PIVOTS_Y,                // Для объекта не установлено ни одной опорной точки по оси Y
   MSG_GRAPH_OBJ_EXT_NOT_ATACHED_TO_BASE,             // Объект не привязан к базовому графическому объекту
   MSG_GRAPH_OBJ_EXT_FAILED_CREATE_PP_DATA_OBJ,       // Не удалось создать объект данных опорной точки X и Y
   MSG_GRAPH_OBJ_EXT_NUM_BASE_PP_TO_SET_X,            // Количество опорных точек базового объекта для расчёта координаты X: 
   MSG_GRAPH_OBJ_EXT_NUM_BASE_PP_TO_SET_Y,            // Количество опорных точек базового объекта для расчёта координаты Y: 

//--- CGStdGraphObjExtToolkit
   MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA,     // Не удалось изменить размер массива данных времени опорной точки
   MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA,    // Не удалось изменить размер массива данных цены опорной точки
   MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM,   // Не удалось создать объект-форму для контроля опорной точки
   
  };
//+------------------------------------------------------------------+

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

//--- CLinkedPivotPoint
   {"Для объекта не установлено ни одной опорной точки по оси X","The object does not have any pivot points set along the x-axis"},
   {"Для объекта не установлено ни одной опорной точки по оси Y","The object does not have any pivot points set along the y-axis"},
   {"Объект не привязан к базовому графическому объекту","The object is not attached to the base graphical object"},
   {"Не удалось создать объект данных опорной точки X и Y.","Failed to create X and Y reference point data object"},
   {"Количество опорных точек базового объекта для расчёта координаты X: ","Number of reference points of the base object to set the X coordinate: "},
   {"Количество опорных точек базового объекта для расчёта координаты Y: ","Number of reference points of the base object to set the Y coordinate: "},
   
//--- CGStdGraphObjExtToolkit
   {"Не удалось изменить размер массива данных времени опорной точки","Failed to resize pivot point time data array"},
   {"Не удалось изменить размер массива данных цены опорной точки","Failed to resize pivot point price data array"},
   {"Не удалось создать объект-форму для контроля опорной точки","Failed to create form object to control pivot point"},
   
  };
//+---------------------------------------------------------------------+


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

#define CLR_DEFAULT                    (0xFF000000)               // Цвет фона символа в навигаторе по умолчанию

на более понятную по смыслу

#define CLR_MW_DEFAULT                 (0xFF000000)               // Цвет фона символа в обзоре рынка по умолчанию

и макроподстановку

#define NULL_COLOR                     (0x00FFFFFF)               // Ноль для канваса с альфа-каналом

на более понятную по смыслу

#define CLR_CANV_NULL                  (0x00FFFFFF)               // Ноль для канваса с альфа-каналом

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

//--- Параметры графических объектов
#define PROGRAM_OBJ_MAX_ID             (10000)                    // Максимальное значение идентификатора графического объекта, принадлежащего программе
#define CTRL_POINT_SIZE                (5)                        // Радиус контрольной точки на форме управления опорными точками графического объекта
#define CTRL_FORM_SIZE                 (40)                       // Размер формы контрольной точки управления опорными точками графического объекта
//+------------------------------------------------------------------+
//| Перечисления                                                     |
//+------------------------------------------------------------------+


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

Просто нажмём сочетание клавиш Ctrl+Shift+H и введём в поля такие значения и установим галочки как на рисунке:


И нажмём кнопку Replace in Files (Заменить в файлах). Редактор выполнит поиск по всем папкам библиотеки и выполнит в них замену.

Точно так же сделаем и для замены "NULL_COLOR" на "CLR_CANV_NULL"

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

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

//+------------------------------------------------------------------+
//| Выводит в журнал краткое описание объекта                        |
//+------------------------------------------------------------------+
void CGStdArrowBuyObj::PrintShort(const bool dash=false,const bool symbol=false)
  {
   ::Print
     (
      (dash ? " - " : "")+this.Header(symbol)," \"",CGBaseObj::Name(),"\": ID ",(string)this.GetProperty(GRAPH_OBJ_PROP_ID,0),
      ", ",::TimeToString(CGBaseObj::TimeCreate(),TIME_DATE|TIME_MINUTES|TIME_SECONDS)
     );
  }
//+------------------------------------------------------------------+

Это чисто "дизайнерская" доработка.
Сделана во всех файлах папки \MQL5\Include\DoEasy\Objects\Graph\Standard\, и их можно посмотреть самостоятельно в прикреплённых к статье файлах.


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

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

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

Но всё по порядку...

В каталоге библиотеки \MQL5\Include\DoEasy\Objects\Graph\ создадим новую папку Extend\, а в ней новый файл CGStdGraphObjExtToolkit.mqh класса CGStdGraphObjExtToolkit, унаследованного от базового класса CObject для построения стандартной библиотеки MQL5:

//+------------------------------------------------------------------+
//|                                      CGStdGraphObjExtToolkit.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 "..\..\Graph\Form.mqh"
//+------------------------------------------------------------------+
//| Класс инструментария расширенного                                |
//| стандартного графического объекта                                |
//+------------------------------------------------------------------+
class CGStdGraphObjExtToolkit : public CObject
  {
  }

К файлу класса должен быть подключен файл класса объекта-формы.

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

//+------------------------------------------------------------------+
//| Класс инструментария расширенного                                |
//| стандартного графического объекта                                |
//+------------------------------------------------------------------+
class CGStdGraphObjExtToolkit : public CObject
  {
private:
   long              m_base_chart_id;           // Идентификатор графика базового графического объекта
   int               m_base_subwindow;          // Подокно графика базового графического объекта
   ENUM_OBJECT       m_base_type;               // Тип базового объекта
   string            m_base_name;               // Имя базового объекта
   int               m_base_pivots;             // Количество опорных точек базового объекта
   datetime          m_base_time[];             // Массив времени опорных точек базового объекта
   double            m_base_price[];            // Массив цен опорных точек базового объекта
   int               m_base_x;                  // Координата X базового объекта
   int               m_base_y;                  // Координата Y базового объекта
   int               m_ctrl_form_size;          // Размер форм управления опорными точками
   int               m_shift;                   // Смещение координат для корректировки размещения формы
   CArrayObj         m_list_forms;              // Список объектов-форм управления опорными точками
//--- Создаёт объект-форму на опорной точке базового объекта
   CForm            *CreateNewControlPointForm(const int index);
//--- Возвращает экранные координаты X и Y указанной опорной точки графического объекта
   bool              GetControlPointCoordXY(const int index,int &x,int &y);
public:

Для хранения координат цены и времени будем использовать массивы, так как у одного графического объекта может быть несколько опорных точек и, соответственно, координаты каждой точки будут храниться в соответствующих ячейках массива. Координата первой точки — по индексу массива 0, координата второй точки — по индексу массива 1, третьей — по индексу 2, и т.д.
Смещение координат формы нам нужно для того, чтобы точно расположить форму по центру опорной точки объекта, и этим смещением будет половина её размера. При этом, если размер формы задать кратным двойке, например, 10, то он будет скорректирован на 1 больше, т.е. — 11. Это нужно для того, чтобы форма могла расположиться точно по центру опорной точки графического объекта и не имела размер одной из сторон больше на один пиксель размера противоположной стороны.
В списке форм будем хранить все создаваемые формы, и из него же будем получать к ним доступ по указателю. А метод для расчёта экранных координат нам нужен чтобы знать на каких координатах экрана располагать форму, чтобы она встала точно на опорную точку графического объекта.

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

public:
//--- Устанавливает параметры базового объекта составного графического объекта
   void              SetBaseObj(const ENUM_OBJECT base_type,const string base_name,
                                const long base_chart_id,const int base_subwindow,
                                const int base_pivots,const int ctrl_form_size,
                                const int base_x,const int base_y,
                                const datetime &base_time[],const double &base_price[]);
//--- Устанавливает координаты (1) времени, (2) цены, (3) времени и цены базового объекта
   void              SetBaseObjTime(const datetime time,const int index);
   void              SetBaseObjPrice(const double price,const int index);
   void              SetBaseObjTimePrice(const datetime time,const double price,const int index);
//--- Устанавливает экранные координаты (1) X, (2) Y, (3) X и Y базового объекта
   void              SetBaseObjCoordX(const int value)                        { this.m_base_x=value;                          }
   void              SetBaseObjCoordY(const int value)                        { this.m_base_y=value;                          }
   void              SetBaseObjCoordXY(const int value_x,const int value_y)   { this.m_base_x=value_x; this.m_base_y=value_y; }
//--- (1) Устанавливает, (2) возвращает размер формы контрольных точек управления опорными точками
   void              SetControlFormSize(const int size);
   int               GetControlFormSize(void)                          const { return this.m_ctrl_form_size;                }
//--- Возвращает указатель на форму опорной точки по (1) индексу, (2) имени
   CForm            *GetControlPointForm(const int index)                     { return this.m_list_forms.At(index);           }
   CForm            *GetControlPointForm(const string name,int &index);
//--- Возвращает количество опорных точек базового объекта
   int               GetNumPivotsBaseObj(void)                          const { return this.m_base_pivots;                    }
//--- Создаёт объекты-формы на опорных точках базового объекта
   bool              CreateAllControlPointForm(void);
//--- Удаляет все объекты-формы из списка
   void              DeleteAllControlPointForm(void);
   
//--- Обработчик событий
   void              OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam);

//--- Конструктор/Деструктор
                     CGStdGraphObjExtToolkit(const ENUM_OBJECT base_type,const string base_name,
                                             const long base_chart_id,const int base_subwindow,
                                             const int base_pivots,const int ctrl_form_size,
                                             const int base_x,const int base_y,
                                             const datetime &base_time[],const double &base_price[])
                       {
                        this.m_list_forms.Clear();
                        this.SetBaseObj(base_type,base_name,base_chart_id,base_subwindow,base_pivots,ctrl_form_size,base_x,base_y,base_time,base_price);
                        this.CreateAllControlPointForm();
                       }
                     CGStdGraphObjExtToolkit(){;}
                    ~CGStdGraphObjExtToolkit(){;}
  };
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Устанавливает параметры базового объекта                         |
//| составного графического объекта                                  |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::SetBaseObj(const ENUM_OBJECT base_type,const string base_name,
                                         const long base_chart_id,const int base_subwindow,
                                         const int base_pivots,const int ctrl_form_size,
                                         const int base_x,const int base_y,
                                         const datetime &base_time[],const double &base_price[])
  {
   this.m_base_chart_id=base_chart_id;       // Идентификатор графика базового графического объекта
   this.m_base_subwindow=base_subwindow;     // Подокно графика базового графического объекта
   this.m_base_type=base_type;               // Тип базового объекта
   this.m_base_name=base_name;               // Имя базового объекта
   this.m_base_pivots=base_pivots;           // Количество опорных точек базового объекта
   this.m_base_x=base_x;                     // Координата X базового объекта
   this.m_base_y=base_y;                     // Координата Y базового объекта
   this.SetControlFormSize(ctrl_form_size);  // Размер форм управления опорными точками
   
   if(this.m_base_type==OBJ_LABEL            || this.m_base_type==OBJ_BUTTON  ||
      this.m_base_type==OBJ_BITMAP_LABEL     || this.m_base_type==OBJ_EDIT    ||
      this.m_base_type==OBJ_RECTANGLE_LABEL  || this.m_base_type==OBJ_CHART)
      return;
   
   if(::ArraySize(base_time)==0)
     {
      CMessage::ToLog(DFUN+"base_time: ",MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY);
      return;
     }
   if(::ArraySize(base_price)==0)
     {
      CMessage::ToLog(DFUN+"base_price: ",MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY);
      return;
     }
   if(::ArrayResize(this.m_base_time,this.m_base_pivots)!=this.m_base_pivots)
     {
      CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA);
      return;
     }
   if(::ArrayResize(this.m_base_price,this.m_base_pivots)!=this.m_base_pivots)
     {
      CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA);
      return;
     }
   for(int i=0;i<this.m_base_pivots;i++)
     {
      this.m_base_time[i]=base_time[i];      // Время (i) опорной точки базового объекта
      this.m_base_price[i]=base_price[i];    // Цена (i) опорной точки базового объекта
     }
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//|Устанавливает размер контрольных точек управления опорными точками|
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::SetControlFormSize(const int size)
  {
   this.m_ctrl_form_size=(size>254 ? 255 : size<5 ? 5 : size%2==0 ? size+1 : size);
   this.m_shift=(int)ceil(m_ctrl_form_size/2)+1;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Устанавливает координату времени базового объекта                |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::SetBaseObjTime(const datetime time,const int index)
  {
   if(index>this.m_base_pivots-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY);
      return;
     }
   this.m_base_time[index]=time;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Устанавливает координату цены базового объекта                   |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::SetBaseObjPrice(const double price,const int index)
  {
   if(index>this.m_base_pivots-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY);
      return;
     }
   this.m_base_price[index]=price;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Устанавливает координаты времени и цены базового объекта         |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::SetBaseObjTimePrice(const datetime time,const double price,const int index)
  {
   if(index>this.m_base_pivots-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY);
      return;
     }
   this.m_base_time[index]=time;
   this.m_base_price[index]=price;
  }
//+------------------------------------------------------------------+

Метод идентичен двум вышерассмотренным, но устанавливает в массивы класса как цену, так и время.

Метод, возвращающий координаты X и Y указанной опорной точки графического объекта в экранных координатах:

//+------------------------------------------------------------------+
//| Возвращает координаты X и Y указанной опорной точки              |
//| графического объекта в экранных координатах                      |
//+------------------------------------------------------------------+
bool CGStdGraphObjExtToolkit::GetControlPointCoordXY(const int index,int &x,int &y)
  {
   switch(this.m_base_type)
     {
      case OBJ_LABEL             :
      case OBJ_BUTTON            :
      case OBJ_BITMAP_LABEL      :
      case OBJ_EDIT              :
      case OBJ_RECTANGLE_LABEL   :
      case OBJ_CHART             :
        x=this.m_base_x;
        y=this.m_base_y;
        break;
      default:
        if(!::ChartTimePriceToXY(this.m_base_chart_id,this.m_base_subwindow,this.m_base_time[index],this.m_base_price[index],x,y))
          {
           x=0;
           y=0;
           return false;
          }
     }
   return true;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Возвращает указатель на форму опорной точки по имени             |
//+------------------------------------------------------------------+
CForm *CGStdGraphObjExtToolkit::GetControlPointForm(const string name,int &index)
  {
   index=WRONG_VALUE;
   for(int i=0;i<this.m_list_forms.Total();i++)
     {
      CForm *form=this.m_list_forms.At(i);
      if(form==NULL)
         continue;
      if(form.Name()==name)
        {
         index=i;
         return form;
        }
     }
   return NULL;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Создаёт объект-форму на опорной точке базового объекта           |
//+------------------------------------------------------------------+
CForm *CGStdGraphObjExtToolkit::CreateNewControlPointForm(const int index)
  {
   string name=this.m_base_name+"_TKPP_"+(index<this.m_base_pivots ? (string)index : "X");
   CForm *form=this.GetControlPointForm(index);
   if(form!=NULL)
      return NULL;
   int x=0, y=0;
   if(!this.GetControlPointCoordXY(index,x,y))
      return NULL;
   return new CForm(this.m_base_chart_id,this.m_base_subwindow,name,x-this.m_shift,y-this.m_shift,this.GetControlFormSize(),this.GetControlFormSize());
  }
//+------------------------------------------------------------------+

В метод передаётся индекс требуемой опорной точки, на которой необходимо создать объект-форму.
Создаём имя объекта-формы, состоящее из имени базового объекта + аббревиатура от "ToolKit Pivot Point" (_TKPP) + индекс опорной точки. При создании описания индекса проверяем его значение и, если оно меньше количества опорных точек базового объекта (подсчёт начинается с нуля), то используем строковое представление переданного в метод индекса. Иначе — используем значок "X". Зачем это нужно? К базовому объекту можно будет в дальнейшем прикреплять зависимые объекты не только на его опорных точках, но и между ними. Плюс, нам нужно будет для перемещения объекта целиком, создать контрольную форму по центру линии базового объекта, за которую и будем перемещать весь объект. Поэтому в названии формы нужно сразу предусмотреть возможность создания формы не только для опорных точек, но и для других тоже.
Далее проверяем наличие формы в списке по индексу, переданному в метод и, если по этому индексу в списке уже есть объект-форма (указатель на него не равен NULL), то возвращаем NULL.
Затем преобразуем координаты опорной точки по её индексу в экранные координаты и возвращаем результат создания объекта-формы на полученных координатах. При этом от обеих координат отнимаем величину их смещения — для точного позиционирования центра формы на опорной точке.
Мы могли бы просто задать значение для точки привязки формы по центру, но в библиотеке мы условились, что точка привязки всех форм будет неизменной — в левом верхнем её углу. Поэтому везде, где это требуется, используем смещение для позиционирования объектов-форм.

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

//+------------------------------------------------------------------+
//| Создаёт объекты-формы на опорных точках базового объекта         |
//+------------------------------------------------------------------+
bool CGStdGraphObjExtToolkit::CreateAllControlPointForm(void)
  {
   bool res=true;
   //--- В цикле по количеству опорных точек базового объекта
   for(int i=0;i<this.m_base_pivots;i++)
     {
      //--- Создаём новый объект-форму на текущей опорной точке, соответствующей индексу цикла
      CForm *form=this.CreateNewControlPointForm(i);
      //--- Если форму создать не удалось - сообщаем об этом и добавляем к итоговому результату значение false
      if(form==NULL)
        {
         CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM);
         res &=false;
        }
      //--- Если форму добавить в список не удалось - сообщаем об этом, удаляем созданную форму и добавляем к итоговому результату значение false
      if(!this.m_list_forms.Add(form))
        {
         CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
         delete form;
         res &=false;
        }
      //--- Устанавливаем все необходимые свойства созданному объекту-форме
      form.SetBelong(GRAPH_OBJ_BELONG_PROGRAM); // Объект создан программно
      form.SetActive(true);                     // Объект-форма активен
      form.SetMovable(true);                    // Объект перемещаемый
      form.SetActiveAreaShift(0,0,0,0);         // Активная область объекта - вся форма целиком
      form.SetFlagSelected(false,false);        // Объект не выбран
      form.SetFlagSelectable(false,false);      // Объект не доступен для выбора мышкой
      form.Erase(CLR_CANV_NULL,0);              // Заливаем форму прозрачным цветом и устанавливаем полную прозрачность
      //form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,clrSilver); // Рисование очерчивающего прямоугольника для визуального отображения расположения формы
      form.DrawCircle((int)floor(form.Width()/2),(int)floor(form.Height()/2),CTRL_POINT_SIZE,clrDodgerBlue);   // Рисуем окружность в центре формы
      form.DrawCircleFill((int)floor(form.Width()/2),(int)floor(form.Height()/2),2,clrDodgerBlue);             // Рисуем точку в центре формы
      form.Done();                              // Фиксируем изначальное состояние объекта-формы (его внешний вид)
     }
   //--- Перерисовываем график для отображения изменений (в случае успеха) и возвращаем итоговый результат
   if(res)
      ::ChartRedraw(this.m_base_chart_id);
   return res;
  }
//+------------------------------------------------------------------+

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

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

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

//+------------------------------------------------------------------+
//| Удаляет все объекты-формы из списка                              |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::DeleteAllControlPointForm(void)
  {
   this.m_list_forms.Clear();
  }
//+------------------------------------------------------------------+

Просто используем метод Clear(), полностью очищающий весь список.

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

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      for(int i=0;i<this.m_list_forms.Total();i++)
        {
         CForm *form=this.m_list_forms.At(i);
         if(form==NULL)
            continue;
         int x=0, y=0;
         if(!this.GetControlPointCoordXY(i,x,y))
            continue;
         form.SetCoordX(x-this.m_shift);
         form.SetCoordY(y-this.m_shift);
         form.Update();
        }
      ::ChartRedraw(this.m_base_chart_id);
     }
  }
//+------------------------------------------------------------------+

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

Так как объект инструментария расширенного стандартного графического объекта будет храниться в самом объекте класса стандартного графического объекта, то нам нужно доработать этот класс в файле \MQL5\Include\DoEasy\Objects\Graph\Standard\GStdGraphObj.mqh.

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

//+------------------------------------------------------------------+
//|                                                 GStdGraphObj.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 "..\GBaseObj.mqh"
#include "..\..\..\Services\Properties.mqh"
#include "..\..\Graph\Form.mqh"
#include "..\..\Graph\Extend\CGStdGraphObjExtToolkit.mqh"
//+------------------------------------------------------------------+
//| Класс данных опорной точки зависимого объекта                    |
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Класс абстрактного стандартного графического объекта             |
//+------------------------------------------------------------------+
class CGStdGraphObj : public CGBaseObj
  {
private:
   CArrayObj         m_list;                                            // Список зависимых графических объектов
   CProperties      *Prop;                                              // Указатель на объект свойств
   CLinkedPivotPoint m_linked_pivots;                                   // Связанные опорные точки
   CGStdGraphObjExtToolkit *ExtToolkit;                                 // Указатель на инструментарий расширенного графического объекта
   int               m_pivots;                                          // Количество опорных точек объекта
//--- Считывает и устанавливает (1) время, (2) цену указанной опорной точки объекта
   void              SetTimePivot(const int index);
   void              SetPricePivot(const int index);
//--- Считывает и устанавливает (1) цвет, (2) стиль, (3) толщину, (4) значение, (5) текст указанного уровня объекта
   void              SetLevelColor(const int index);
   void              SetLevelStyle(const int index);
   void              SetLevelWidth(const int index);
   void              SetLevelValue(const int index);
   void              SetLevelText(const int index);
//--- Считывает и устанавливает имя BMP-файла для объекта "Графическая метка". Индекс: 0-состояние ON, 1-состояние OFF
   void              SetBMPFile(const int index);

public:

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

public:
//--- Устанавливает (1) целочисленное, (2) вещественное и (3) строковое свойство объекта
   void              SetProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index,long value)     { this.Prop.Curr.SetLong(property,index,value);    }
   void              SetProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index,double value)    { this.Prop.Curr.SetDouble(property,index,value);  }
   void              SetProperty(ENUM_GRAPH_OBJ_PROP_STRING property,int index,string value)    { this.Prop.Curr.SetString(property,index,value);  }
//--- Возвращает из массива свойств (1) целочисленное, (2) вещественное и (3) строковое свойство объекта
   long              GetProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index)          const { return this.Prop.Curr.GetLong(property,index);   }
   double            GetProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index)           const { return this.Prop.Curr.GetDouble(property,index); }
   string            GetProperty(ENUM_GRAPH_OBJ_PROP_STRING property,int index)           const { return this.Prop.Curr.GetString(property,index); }
//--- Устанавливает прошлое (1) целочисленное, (2) вещественное и (3) строковое свойство объекта
   void              SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index,long value) { this.Prop.Prev.SetLong(property,index,value);    }
   void              SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index,double value){ this.Prop.Prev.SetDouble(property,index,value);  }
   void              SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING property,int index,string value){ this.Prop.Prev.SetString(property,index,value);  }
//--- Возвращает из массива прошлых свойств (1) целочисленное, (2) вещественное и (3) строковое свойство объекта
   long              GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index)      const { return this.Prop.Prev.GetLong(property,index);   }
   double            GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index)       const { return this.Prop.Prev.GetDouble(property,index); }
   string            GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING property,int index)       const { return this.Prop.Prev.GetString(property,index); }
   
//--- Возвращает (1) себя, (2) свойства, (3) историю изменений
   CGStdGraphObj    *GetObject(void)                                       { return &this;            }
   CProperties      *Properties(void)                                      { return this.Prop;        }
   CChangeHistory   *History(void)                                         { return this.Prop.History;}
   CGStdGraphObjExtToolkit *GetExtToolkit(void)                            { return this.ExtToolkit;  }
//--- Возвращает флаг поддержания объектом данного свойства
   virtual bool      SupportProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property) { return true;             }
   virtual bool      SupportProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property)  { return true;             }
   virtual bool      SupportProperty(ENUM_GRAPH_OBJ_PROP_STRING property)  { return true;             }

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

private:
//--- Устанавливает координату X (1) из указанного свойства базового объекта в указанный подчинённый объект, (2) из базового объекта
   void              SetCoordXToDependentObj(CGStdGraphObj *obj,const int prop_from,const int modifier_from,const int modifier_to);
   void              SetCoordXFromBaseObj(const int prop_from,const int modifier_from,const int modifier_to);
//--- Устанавливает координату Y (1) из указанного свойства базового объекта в указанный подчинённый объект, (2) из базового объекта
   void              SetCoordYToDependentObj(CGStdGraphObj *obj,const int prop_from,const int modifier_from,const int modifier_to);
   void              SetCoordYFromBaseObj(const int prop_from,const int modifier_from,const int modifier_to);
//--- Устанавливает в указанный подчинённый объект (1) целочисленное, (2) вещественное, (3) строковое свойство
   void              SetDependentINT(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_INTEGER prop,const long value,const int modifier);
   void              SetDependentDBL(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_DOUBLE prop,const double value,const int modifier);
   void              SetDependentSTR(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_STRING prop,const string value,const int modifier);

public:
//--- Обработчик событий
   void              OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- Конструктор по умолчанию
                     CGStdGraphObj(){ this.m_type=OBJECT_DE_TYPE_GSTD_OBJ; this.m_species=WRONG_VALUE; this.ExtToolkit=NULL; }
//--- Деструктор
                    ~CGStdGraphObj()
                       {
                        if(this.Prop!=NULL)
                           delete this.Prop;
                        if(this.ExtToolkit!=NULL)
                          {
                           this.ExtToolkit.DeleteAllControlPointForm();
                           delete this.ExtToolkit;
                          }
                       }
protected:
//--- Защищённый параметрический конструктор
                     CGStdGraphObj(const ENUM_OBJECT_DE_TYPE obj_type,
                                   const ENUM_GRAPH_ELEMENT_TYPE elm_type,
                                   const ENUM_GRAPH_OBJ_BELONG belong,
                                   const ENUM_GRAPH_OBJ_SPECIES species,
                                   const long chart_id, const int pivots,
                                   const string name);
                     
public:
//+-------------------------------------------------------------------+ 
//|Методы упрощённого доступа и установки свойств графического объекта|
//+-------------------------------------------------------------------+

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

public:
//+-------------------------------------------------------------------+ 
//|Методы упрощённого доступа и установки свойств графического объекта|
//+-------------------------------------------------------------------+
//--- Количество опорных точек объекта
   int               Pivots(void)                  const { return this.m_pivots;                                                          }
//--- Номер объекта в списке
   int               Number(void)                  const { return (int)this.GetProperty(GRAPH_OBJ_PROP_NUM,0);                            }
   void              SetNumber(const int number)         { this.SetProperty(GRAPH_OBJ_PROP_NUM,0,number);                                 }


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

//+------------------------------------------------------------------+
//| Защищённый параметрический конструктор                           |
//+------------------------------------------------------------------+
CGStdGraphObj::CGStdGraphObj(const ENUM_OBJECT_DE_TYPE obj_type,
                             const ENUM_GRAPH_ELEMENT_TYPE elm_type,
                             const ENUM_GRAPH_OBJ_BELONG belong,
                             const ENUM_GRAPH_OBJ_SPECIES species,
                             const long chart_id,const int pivots,
                             const string name)
  {
//--- Создаём объект свойств со значениями по умолчанию
   this.Prop=new CProperties(GRAPH_OBJ_PROP_INTEGER_TOTAL,GRAPH_OBJ_PROP_DOUBLE_TOTAL,GRAPH_OBJ_PROP_STRING_TOTAL);
   this.ExtToolkit=(elm_type==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED ? new CGStdGraphObjExtToolkit() : NULL);
//--- Устанавливаем количество опорных точек и уровней объекта
   this.m_pivots=pivots;
   int levels=(int)::ObjectGetInteger(chart_id,name,OBJPROP_LEVELS);

//--- Устанавливаем размерности массивов свойств в соответствии с количеством опорных точек и уровней
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_TIME,this.m_pivots);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_PRICE,this.m_pivots);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELCOLOR,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELSTYLE,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELWIDTH,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELVALUE,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELTEXT,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_BMPFILE,2);
   
//--- Устанавливаем объекту (1) его тип, тип графического (2) объекта, (3) елемента, (4) принадлежность и (5) номер подокна, (6) Digits символа графика
   this.m_type=obj_type;
   this.SetName(name);
   CGBaseObj::SetChartID(chart_id);
   CGBaseObj::SetTypeGraphObject(CGBaseObj::GraphObjectType(obj_type));
   CGBaseObj::SetTypeElement(elm_type);
   CGBaseObj::SetBelong(belong);
   CGBaseObj::SetSpecies(species);
   CGBaseObj::SetSubwindow(chart_id,name);
   CGBaseObj::SetDigits((int)::SymbolInfoInteger(::ChartSymbol(chart_id),SYMBOL_DIGITS));
   
//--- Сохранение целочисленных свойств, присущих всем графическим объектам, но не имеющиеся у графического объекта
   this.SetProperty(GRAPH_OBJ_PROP_CHART_ID,0,CGBaseObj::ChartID());                // Идентификатор графика
   this.SetProperty(GRAPH_OBJ_PROP_WND_NUM,0,CGBaseObj::SubWindow());               // Номер подокна графика
   this.SetProperty(GRAPH_OBJ_PROP_TYPE,0,CGBaseObj::TypeGraphObject());            // Тип графического объекта (ENUM_OBJECT)
   this.SetProperty(GRAPH_OBJ_PROP_ELEMENT_TYPE,0,CGBaseObj::TypeGraphElement());   // Тип графического элемента (ENUM_GRAPH_ELEMENT_TYPE)
   this.SetProperty(GRAPH_OBJ_PROP_BELONG,0,CGBaseObj::Belong());                   // Принадлежность графического объекта
   this.SetProperty(GRAPH_OBJ_PROP_SPECIES,0,CGBaseObj::Species());                 // Вид графического объекта
   this.SetProperty(GRAPH_OBJ_PROP_GROUP,0,0);                                      // Группа графических объектов
   this.SetProperty(GRAPH_OBJ_PROP_ID,0,0);                                         // Идентификатор объекта
   this.SetProperty(GRAPH_OBJ_PROP_BASE_ID,0,0);                                    // Идентификатор базового объекта
   this.SetProperty(GRAPH_OBJ_PROP_NUM,0,0);                                        // Номер объекта в списке
   this.SetProperty(GRAPH_OBJ_PROP_CHANGE_HISTORY,0,false);                         // Флаг хранения истории изменений
   this.SetProperty(GRAPH_OBJ_PROP_BASE_NAME,0,this.Name());                        // Имя базового объекта
   
//--- Сохранение свойств, присущих всем графическим объектам, имеющимся у графического объекта
   this.PropertiesRefresh();
   
//--- Сохранение базовых свойств в родительском объекте
   this.m_create_time=(datetime)this.GetProperty(GRAPH_OBJ_PROP_CREATETIME,0);
   this.m_back=(bool)this.GetProperty(GRAPH_OBJ_PROP_BACK,0);
   this.m_selected=(bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTED,0);
   this.m_selectable=(bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTABLE,0);
   this.m_hidden=(bool)this.GetProperty(GRAPH_OBJ_PROP_HIDDEN,0);

//--- Инициализация инструментария расширенного графического объекта
   if(this.GraphElementType()==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)
     {
      datetime times[];
      double prices[];
      if(::ArrayResize(times,this.Pivots())!=this.Pivots())
         CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA);
      if(::ArrayResize(prices,this.Pivots())!=this.Pivots())
         CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA);
      for(int i=0;i<this.Pivots();i++)
        {
         times[i]=this.Time(i);
         prices[i]=this.Price(i);
        }
      this.ExtToolkit.SetBaseObj(this.TypeGraphObject(),this.Name(),this.ChartID(),this.SubWindow(),this.Pivots(),CTRL_FORM_SIZE,this.XDistance(),this.YDistance(),times,prices);
      this.ExtToolkit.CreateAllControlPointForm();
      this.SetFlagSelected(false,false);
      this.SetFlagSelectable(false,false);
     }

//--- Сохранение текущих свойств в прошлые
   this.PropertiesCopyToPrevData();
  }
//+-------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Проверяет изменения свойств объекта                              |
//+------------------------------------------------------------------+
void CGStdGraphObj::PropertiesCheckChanged(void)
  {
   CGBaseObj::ClearEventsList();
   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;
      for(int j=0;j<Prop.CurrSize(prop);j++)
        {
         if(this.GetProperty(prop,j)!=this.GetPropertyPrev(prop,j))
           {
            changed=true;
            this.CreateAndAddNewEvent(GRAPH_OBJ_EVENT_CHANGE,this.ChartID(),prop,this.Name());
           }
        }
     }

   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;
      for(int j=0;j<Prop.CurrSize(prop);j++)
        {
         if(this.GetProperty(prop,j)!=this.GetPropertyPrev(prop,j))
           {
            changed=true;
            this.CreateAndAddNewEvent(GRAPH_OBJ_EVENT_CHANGE,this.ChartID(),prop,this.Name());
           }
        }
     }

   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;
      for(int j=0;j<Prop.CurrSize(prop);j++)
        {
         if(this.GetProperty(prop,j)!=this.GetPropertyPrev(prop,j) && prop!=GRAPH_OBJ_PROP_NAME)
           {
            changed=true;
            this.CreateAndAddNewEvent(GRAPH_OBJ_EVENT_CHANGE,this.ChartID(),prop,this.Name());
           }
        }
     }
   if(changed)
     {
      for(int i=0;i<this.m_list_events.Total();i++)
        {
         CGBaseEvent *event=this.m_list_events.At(i);
         if(event==NULL)
            continue;
         ::EventChartCustom(::ChartID(),event.ID(),event.Lparam(),event.Dparam(),event.Sparam());
        }
      if(this.AllowChangeHistory())
        {
         int total=HistoryChangesTotal();
         if(this.CreateNewChangeHistoryObj(total<1))
            ::Print
              (
               DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_SUCCESS_CREATE_SNAPSHOT)," #",(total==0 ? "0-1" : (string)total),
               ": ",this.HistoryChangedObjTimeChangedToString(total-1)
              );
        }
      //--- Если к объекту присоединены подчинённые (объект является базовым в составном графическом объекте)
      if(this.m_list.Total()>0)
        {
         //--- В цикле по количеству присоединённых графических объектов
         for(int i=0;i<this.m_list.Total();i++)
           {
            //--- получаем очередной графический объект,
            CGStdGraphObj *dep=m_list.At(i);
            if(dep==NULL)
               continue;
            //--- получаем объект данных его опорных точек,
            CLinkedPivotPoint *pp=dep.GetLinkedPivotPoint();
            if(pp==NULL)
               continue;
            //--- получаем количество координатных точек, к которым прикреплён объект
            int num=pp.GetNumLinkedCoords();
            //--- В цикле по координатным точкам объекта
            for(int j=0;j<num;j++)
              {
               //--- получаем количество координатных точек базового объекта для установки координаты X
               int numx=pp.GetBasePivotsNumX(j);
               //--- В цикле по каждой координатной точке для установки координаты X
               for(int nx=0;nx<numx;nx++)
                 {
                  //--- получаем свойство для установки координаты X, его модификатор,
                  //--- и устанавливаем его в выбранный текущим в основном цикле объект
                  int prop_from=pp.GetPropertyX(j,nx);
                  int modifier_from=pp.GetPropertyModifierX(j,nx);
                  this.SetCoordXToDependentObj(dep,prop_from,modifier_from,nx);
                 }
               //--- Получаем количество координатных точек базового объекта для установки координаты Y
               int numy=pp.GetBasePivotsNumY(j);
               //--- В цикле по каждой координатной точке для установки координаты Y
               for(int ny=0;ny<numy;ny++)
                 {
                  //--- получаем свойство для установки координаты Y, его модификатор,
                  //--- и устанавливаем его в выбранный текущим в основном цикле объект
                  int prop_from=pp.GetPropertyY(j,ny);
                  int modifier_from=pp.GetPropertyModifierY(j,ny);
                  this.SetCoordYToDependentObj(dep,prop_from,modifier_from,ny);
                 }
              }
            dep.PropertiesCopyToPrevData();
           }
         //--- Перемещаем контрольные точки управления на изменённые координаты
         if(ExtToolkit!=NULL)
           {
            for(int i=0;i<this.Pivots();i++)
              {
               ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i);
              }
            ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
            long   lparam=0;
            double dparam=0;
            string sparam="";
            ExtToolkit.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
           }
         //--- По завершению цикла обработки всех привязанных объектов, перерисовываем график для отображения всех изменений
         ::ChartRedraw(m_chart_id);
        }
      //--- Сохраняем текущие свойства как прошлые
      this.PropertiesCopyToPrevData();
     }
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Добавляет подчинённый стандартный графический объект в список    |
//+------------------------------------------------------------------+
bool CGStdGraphObj::AddDependentObj(CGStdGraphObj *obj)
  {
   //--- Если текущий объект не является расширенным - сообщаем об этом и возвращаем false
   if(this.TypeGraphElement()!=GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)
     {
      CMessage::ToLog(MSG_GRAPH_OBJ_NOT_EXT_OBJ);
      return false;
     }
   //--- Если не удалось добавить указатель на переданный объект в список - сообщаем об этом и возвращаем false
   if(!this.m_list.Add(obj))
     {
      CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_ADD_DEP_EXT_OBJ_TO_LIST);
      return false;
     }
   //--- Объект добавлен в список - устанавливаем для него номер в списке,
   //--- имя и идентификатор текущего объекта как базового,
   //--- установим флаги доступности и выделенности объекта
   //--- и тип графического элемента - стандартный расширенный графический объект
   obj.SetNumber(this.m_list.Total()-1);
   obj.SetBaseName(this.Name());
   obj.SetBaseObjectID(this.ObjectID());
   obj.SetFlagSelected(false,false);
   obj.SetFlagSelectable(false,false);
   obj.SetTypeElement(GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED);
   obj.PropertiesCopyToPrevData();
   return true;
  }
//+------------------------------------------------------------------+


Обработчик событий абстрактного стандартного графического объекта:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CGStdGraphObj::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(GraphElementType()!=GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)
      return;
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      if(ExtToolkit==NULL)
         return;
      for(int i=0;i<this.Pivots();i++)
        {
         ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i);
        }
      ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
      ExtToolkit.OnChartEvent(id,lparam,dparam,sparam);
     }
  }
//+------------------------------------------------------------------+

Пока обработчик обрабатывает лишь событие изменения графика.

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


При удалении расширенного стандартного графического объекта с графика, нам необходимо удалить с графика и объекты-формы его объекта-инструментария в случае, если такой объект был создан для графического объекта. Удаление графических объектов с графика мы обрабатываем в классе-коллекции графических элементов в файле \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh.

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

//+------------------------------------------------------------------+
//| Обрабатывает удаление расширенных графических объектов           |
//+------------------------------------------------------------------+
void CGraphElementsCollection::DeleteExtendedObj(CGStdGraphObj *obj)
  {
   if(obj==NULL)
      return;
   //--- Запомним идентификатор графика графического объекта и количество зависимых объектов в его списке
   long chart_id=obj.ChartID();
   int total=obj.GetNumDependentObj();
   //--- Если список зависимых объектов не пустой (значит это базовый объект)
   if(total>0)
     {
      CGStdGraphObjExtToolkit *toolkit=obj.GetExtToolkit();
      if(toolkit!=NULL)
        {
         toolkit.DeleteAllControlPointForm();
        }
      //--- Пройдёмся в цикле по всем зависимым объектам и удалим их
      for(int n=total-1;n>WRONG_VALUE;n--)
        {
         //--- Получаем очередной графический объект
         CGStdGraphObj *dep=obj.GetDependentObj(n);
         if(dep==NULL)
            continue;
         //--- Если его не получилось удалить с графика - выводим об этом сообщение в журнал
         if(!::ObjectDelete(dep.ChartID(),dep.Name()))
            CMessage::ToLog(DFUN+dep.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART);
        }
      //--- По окончании цикла обновляем график для отображения изменений и выходим из метода
      ::ChartRedraw(chart_id);
      return;
     }
   //--- Если это зависимый объект
   else if(obj.BaseObjectID()>0)
     {
      //--- Получаем имя базового объекта и его идентификатор
      string base_name=obj.BaseName();
      long base_id=obj.BaseObjectID();
      //--- Получаем базовый объект из списка-коллекции графических объектов
      CGStdGraphObj *base=GetStdGraphObject(base_name,chart_id);
      if(base==NULL)
         return;
      //--- получаем количество зависимых объектов в его списке
      int count=base.GetNumDependentObj();
      //--- Пройдёмся в цикле по всем его зависимым объектам и удалим их
      for(int n=count-1;n>WRONG_VALUE;n--)
        {
         //--- Получаем очередной графический объект
         CGStdGraphObj *dep=base.GetDependentObj(n);
         //--- Если указатель получить не удалось, или этот объект уже удалён с графика - идём к следующему
         if(dep==NULL || !this.IsPresentGraphObjOnChart(dep.ChartID(),dep.Name()))
            continue;
         //--- Если графический объект не получилось удалить с графика -
         //--- выводим об этом сообщение в журнал и идём к следующему
         if(!::ObjectDelete(dep.ChartID(),dep.Name()))
           {
            CMessage::ToLog(DFUN+dep.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART);
            continue;
           }
        }
      //--- Удалим базовый объект с графика и из списка
      if(!::ObjectDelete(base.ChartID(),base.Name()))
         CMessage::ToLog(DFUN+base.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART);
     }
   //--- Обновляем график для отображения изменений
   ::ChartRedraw(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    || id==CHARTEVENT_OBJECT_CLICK   ||
      idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG   || idx==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Рассчитаем идентификатор графика
      //--- Если id события соответствует событию с текущего графика, то идентификатор графика получаем от ChartID
      //--- Если id события соответствует пользовательскому событию, то идентификатор графика получаем из lparam
      //--- Иначе - идентификатору графика присваиваем -1
      long param=(id==CHARTEVENT_OBJECT_CLICK ? ::ChartID() : idx==CHARTEVENT_OBJECT_CLICK ? lparam : WRONG_VALUE);
      long chart_id=(param==WRONG_VALUE ? (lparam==0 ? ::ChartID() : lparam) : param);
      //--- Получим объект из списка-коллекции по его имени, записанном в sparam,
      //--- свойства которого изменены, или который был перемещён
      obj=this.GetStdGraphObject(sparam,chart_id);
      //--- Если объект не удалось получить по имени - он отсутствует в списке,
      //--- а значит его имя было изменено
      if(obj==NULL)
        {
         //--- Найдём в списке объект, которого нет на графике
         obj=this.FindMissingObj(chart_id);
         //--- Если и тут не удалось найти объект - уходим
         if(obj==NULL)
            return;
         //--- Получим имя переименованного  графического объекта на графике, которого нет в списке-коллекции
         string name_new=this.FindExtraObj(chart_id);
         //--- установим новое имя объекту в списке-коллекции, которому не соответствует ни один графический объект на графике,
         //--- и отправим событие с новым именем объекта на график управляющей программы
         if(obj.SetNamePrev(obj.Name()) && obj.SetName(name_new))
            ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_RENAME,obj.ChartID(),obj.TimeCreate(),obj.Name());
        }
      //--- Обновим свойства полученного объекта
      //--- и проверим их изменение
      obj.PropertiesRefresh();
      obj.PropertiesCheckChanged();
     }
//--- Обработка изменения графика для расширенных стандартных объектов
   if(id==CHARTEVENT_CHART_CHANGE || idx==CHARTEVENT_CHART_CHANGE)
     {
      CArrayObj *list=this.GetListStdGraphObjectExt();
      if(list!=NULL)
        {
         for(int i=0;i<list.Total();i++)
           {
            obj=list.At(i);
            if(obj==NULL)
               continue;
            obj.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
           }
        }
     }
  }
//+------------------------------------------------------------------+

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


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

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

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

   if(id==CHARTEVENT_CLICK)
     {
      if(!IsCtrlKeyPressed())
         return;
      //--- Получаем координаты щелчка по графику
      datetime time=0;
      double price=0;
      int sw=0;
      if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,sw,time,price))
        {
         //--- Получаем координаты правой точки для трендовой линии
         datetime time2=iTime(Symbol(),PERIOD_CURRENT,1);
         double price2=iOpen(Symbol(),PERIOD_CURRENT,1);
         
         //--- Создаём объект "Трендовая линия"
         string name_base="TrendLineExt";
         engine.CreateLineTrend(name_base,0,true,time,price,time2,price2);
         //--- Получаем объект из списка графических объектов по имени и идентификатору графика и распечатываем его свойства в журнал
         CGStdGraphObj *obj=engine.GraphGetStdGraphObjectExt(name_base,ChartID());
         
         //--- Создаём объект "Левая ценовая метка"
         string name_dep="PriceLeftExt";
         engine.CreatePriceLabelLeft(name_dep,0,false,time,price);
         //--- Получаем объект из списка графических объектов по имени и идентификатору графика и
         CGStdGraphObj *dep=engine.GraphGetStdGraphObject(name_dep,ChartID());
         //--- добавляем его в список привязанных графических объектов к объекту "Трендовая линия"
         obj.AddDependentObj(dep);
         //--- Устанавливаем его точку привязки по оси X и Y к левой точке трендовой линии
         dep.AddNewLinkedCoord(GRAPH_OBJ_PROP_TIME,0,GRAPH_OBJ_PROP_PRICE,0);
         
         //--- Создаём объект "Правая ценовая метка"
         name_dep="PriceRightExt";
         engine.CreatePriceLabelRight(name_dep,0,false,time2,price2);
         //--- Получаем объект из списка графических объектов по имени и идентификатору графика и
         dep=engine.GraphGetStdGraphObject(name_dep,ChartID());
         //--- добавляем его в список привязанных графических объектов к объекту "Трендовая линия"
         obj.AddDependentObj(dep);
         //--- Устанавливаем его точку привязки по оси X и Y к правой точке трендовой линии
         dep.AddNewLinkedCoord(GRAPH_OBJ_PROP_TIME,1,GRAPH_OBJ_PROP_PRICE,1);
        }
     }
   engine.GetGraphicObjCollection().OnChartEvent(id,lparam,dparam,sparam);

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

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


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

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

Что дальше

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

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

К содержанию

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

Графика в библиотеке DoEasy (Часть 93): Готовим функционал для создания составных графических объектов
Графика в библиотеке DoEasy (Часть 94): Составные графические объекты, перемещение и удаление

Прикрепленные файлы |
MQL5.zip (4271.12 KB)
Веб-проекты (Часть I): Создание веб-приложения в схеме Laravel/Nuxt/MetaTrader 5 Веб-проекты (Часть I): Создание веб-приложения в схеме Laravel/Nuxt/MetaTrader 5
Разработчики MetaTrader 5 предоставили MQL-сообществу множество технологических решений, что даёт возможность реализовывать сложные программные комплексы, схемы которых могут выходить даже за рамки «песочницы» локального компьютера.
Использование AutoIt с MQL5 Использование AutoIt с MQL5
В статье рассматривается создание скриптов для терминала MetraTrader 5 путем интеграции MQL5 с AutoIt. Я покажу, как автоматизировать различные задачи с помощью пользовательского интерфейса терминала, а также представлю класс, использующий библиотеку AutoItX.
Матрицы и векторы в MQL5 Матрицы и векторы в MQL5
Специальные типы данных matrix и vector позволяют писать код, приближенный к математической записи. Это избавляет от необходимости создавать вложенные циклы и помнить о правильной индексации массивов, которые участвуют в вычислении. Таким образом повышается надежность и скорость разработки сложных программ.
Графика в библиотеке DoEasy (Часть 94): Составные графические объекты, перемещение и удаление Графика в библиотеке DoEasy (Часть 94): Составные графические объекты, перемещение и удаление
В статье начнём разработку различных событий составного графического объекта. Рассмотрим частично перемещение и удаление составного графического объекта. Сегодня по большей части мы будем дорабатывать то, что было создано в прошлой статье.