English 中文 Español Deutsch 日本語 Português
preview
Графика в библиотеке DoEasy (Часть 99): Перемещаем расширенный графический объект одной контрольной точкой

Графика в библиотеке DoEasy (Часть 99): Перемещаем расширенный графический объект одной контрольной точкой

MetaTrader 5Примеры | 23 марта 2022, 11:20
2 104 0
Artyom Trishkin
Artyom Trishkin

Содержание


Концепция

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

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

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

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


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

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

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

//+------------------------------------------------------------------+
//| Список типов объектов библиотеки                                 |
//+------------------------------------------------------------------+
enum ENUM_OBJECT_DE_TYPE
  {
//--- Графика
   OBJECT_DE_TYPE_GBASE =  COLLECTION_ID_LIST_END+1,              // Тип объекта "Базовый объект всех графических объектов библиотеки"
   OBJECT_DE_TYPE_GELEMENT,                                       // Тип объекта "Графический элемент"
   OBJECT_DE_TYPE_GFORM,                                          // Тип объекта "Форма"
   OBJECT_DE_TYPE_GFORM_CONTROL,                                  // Тип объекта "Форма управления опорными точками графического объекта"
   OBJECT_DE_TYPE_GSHADOW,                                        // Тип объекта "Тень"
//--- Анимация
   OBJECT_DE_TYPE_GFRAME,                                         // Тип объекта "Один кадр анимации"
   OBJECT_DE_TYPE_GFRAME_TEXT,                                    // Тип объекта "Один кадр текстовой анимации"
   OBJECT_DE_TYPE_GFRAME_QUAD,                                    // Тип объекта "Один кадр прямоугольной анимации"
   OBJECT_DE_TYPE_GFRAME_GEOMETRY,                                // Тип объекта "Один кадр геометрической анимации"
   OBJECT_DE_TYPE_GANIMATIONS,                                    // Тип объекта "Анимации"
//--- Управление графическими объектами
   ...
   ...
   ...
  }


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

   MSG_LIB_SYS_REQUEST_OUTSIDE_LONG_ARRAY,            // Запрос за пределами long-массива
   MSG_LIB_SYS_REQUEST_OUTSIDE_DOUBLE_ARRAY,          // Запрос за пределами double-массива
   MSG_LIB_SYS_REQUEST_OUTSIDE_STRING_ARRAY,          // Запрос за пределами string-массива
   MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY,                 // Запрос за пределами массива
   MSG_LIB_SYS_FAILED_CONV_GRAPH_OBJ_COORDS_TO_XY,    // Не удалось преобразовать координаты графического объекта в экранные
   MSG_LIB_SYS_FAILED_CONV_TIMEPRICE_COORDS_TO_XY,    // Не удалось преобразовать координаты время/цена в экранные

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

   {"Запрос за пределами long-массива","Data requested outside the long-array"},
   {"Запрос за пределами double-массива","Data requested outside the double-array"},
   {"Запрос за пределами string-массива","Data requested outside the string-array"},
   {"Запрос за пределами массива","Data requested outside the array"},
   {"Не удалось преобразовать координаты графического объекта в экранные","Failed to convert graphics object coordinates to screen coordinates"},
   {"Не удалось преобразовать координаты время/цена в экранные","Failed to convert time/price coordinates to screen coordinates"},


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

Функция ChartTimePriceToXY(), в результате которой мы можем получить ошибку преобразования координат, у нас используется ещё и в классе объекта-окна графика в файле \MQL5\Include\DoEasy\Objects\Chart\ChartWnd.mqh. Впишем в метод TimePriceToXY() этого класса вывод сообщения об ошибке в журнал при её получении при попытке преобразования координат:

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

Сначала выведем в журнал запись "Не удалось преобразовать координаты время/цена в экранные", а затем описание ошибки вместе с кодом ошибки.


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

Пропишем его в инструментарии расширенного стандартного графического объекта в файле \MQL5\Include\DoEasy\Objects\Graph\Extend\CGStdGraphObjExtToolkit.mqh:

//+------------------------------------------------------------------+
//|                                      CGStdGraphObjExtToolkit.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
#property strict    // Нужно для mql4
//+------------------------------------------------------------------+
//| Включаемые файлы                                                 |
//+------------------------------------------------------------------+
#include "..\..\Graph\Form.mqh"
//+------------------------------------------------------------------+
//| Класс формы управления опорными точками графического объекта     |
//+------------------------------------------------------------------+
class CFormControl : public CForm
  {
private:
   bool              m_drawn;                   // Флаг, указывающий, что точка управления нарисована на форме
   int               m_pivot_point;             // Опорная точка, которой управляет форма
public:
//--- (1) Возвращает, (2) устанавливает флаг нарисованной точки
   bool              IsControlAlreadyDrawn(void)               const { return this.m_drawn;                       }
   void              SetControlPointDrawnFlag(const bool flag)       { this.m_drawn=flag;                         }
//--- (1) Возвращает, (2) устанавливает опорную точку, которой управляет форма
   int               GraphObjPivotPoint(void)                  const { return this.m_pivot_point;                 }
   void              SetGraphObjPivotPoint(const int index)          { this.m_pivot_point=index;                  }
//--- Конструктор
                     CFormControl(void)                              { this.m_type=OBJECT_DE_TYPE_GFORM_CONTROL;  }
                     CFormControl(const long chart_id,const int subwindow,const string name,const int pivot_point,const int x,const int y,const int w,const int h) :
                        CForm(chart_id,subwindow,name,x,y,w,h)
                          {
                           this.m_type=OBJECT_DE_TYPE_GFORM_CONTROL;
                           this.m_pivot_point=pivot_point;
                          }
  };
//+------------------------------------------------------------------+
//| Класс инструментария расширенного                                |
//| стандартного графического объекта                                |
//+------------------------------------------------------------------+
class CGStdGraphObjExtToolkit : public CObject

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

Приватная переменная-член класса m_pivot_point будет хранить индекс опорной точки, которой управляет форма. У графического объекта есть несколько таких точек управления. Например, у трендовой линии их три — две точки на концах линии, служащие для независимого изменения расположения концов линии, и одна центральная точка — для перемещения всего объекта целиком. Индексы, хранящиеся в объектах-формах, будут соответствовать индексам опорных точек такой линии: 0 и 1 — для точек по краям линии и 2 — для центральной. У других графических объектов могут быть совсем другие контрольные точки управления, но все индексы будут соответствовать существующим у объекта опорным точкам + одна дополнительная (не всегда, и об этом будет сказано в последующих статьях) — для перемещения объекта целиком.

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

Теперь все формы управления опорными точками в классе CGStdGraphObjExtToolkit будут иметь тип CFormControl, поэтому нам нужно исправить тип объекта-формы CForm на CFormControl, и добавим новые методы для работы с формами управления опорными точками графического объекта:

//+------------------------------------------------------------------+
//| Класс инструментария расширенного                                |
//| стандартного графического объекта                                |
//+------------------------------------------------------------------+
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;              // Список объектов-форм управления опорными точками
//--- Создаёт объект-форму на опорной точке базового объекта
   CFormControl     *CreateNewControlPointForm(const int index);
//--- Возвращает экранные координаты X и Y (1) указанной опорной точки графического объекта
   bool              GetControlPointCoordXY(const int index,int &x,int &y);
//--- Устанавливает параметры объекта-формы управления опорными точками
   void              SetControlFormParams(CFormControl *form,const int index);
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) имени
   CFormControl     *GetControlPointForm(const int index)                     { return this.m_list_forms.At(index);           }
   CFormControl     *GetControlPointForm(const string name,int &index);
//--- Возвращает количество (1) опорных точек базового объекта, (2) созданных объектов-форм управления контрольными точками
   int               GetNumPivotsBaseObj(void)                          const { return this.m_base_pivots;                    }
   int               GetNumControlPointForms(void)                      const { return this.m_list_forms.Total();             }
//--- Создаёт объекты-формы на опорных точках базового объекта
   bool              CreateAllControlPointForm(void);
//--- (1) Рисует на форме контрольную точку, (2) рисует на форме контрольную точку, на всех остальных формах - удаляет
   void              DrawControlPoint(CFormControl *form,const uchar opacity,const color clr);
   void              DrawOneControlPoint(CFormControl *form,const uchar opacity=255,const color clr=CTRL_POINT_COLOR);
//--- (1) Рисует цветом по умолчанию, (удаляет) на форме контрольную точку
   void              DrawControlPoint(CFormControl *form)                     { this.DrawControlPoint(form,255,CTRL_POINT_COLOR);}
   void              ClearControlPoint(CFormControl *form)                    { this.DrawControlPoint(form,0,CTRL_POINT_COLOR);  }
//--- Удаляет все объекты-формы из списка
   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(){;}
  };
//+------------------------------------------------------------------+


Доработаем метод GetControlPointCoordXY(), возвращающий координаты X и Y указанной опорной точки графического объекта в экранных координатах.

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

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

//+------------------------------------------------------------------+
//| Возвращает координаты X и Y указанной опорной точки              |
//| графического объекта в экранных координатах                      |
//+------------------------------------------------------------------+
bool CGStdGraphObjExtToolkit::GetControlPointCoordXY(const int index,int &x,int &y)
  {
//--- Объявим объекты-формы, от которых будем получать их экранные координаты
   CFormControl *form0=NULL, *form1=NULL;
//--- Установим X и Y в ноль - при неудаче будут возвращены эти значения
   x=0;
   y=0;
//--- В зависимости от типа графического объекта
   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             :
      case OBJ_EVENT             :
        //--- Записываем экранные координаты объекта и возвращаем true
        x=this.m_base_x;
        y=this.m_base_y;
        return true;
      
      //--- Линии (вертикальная и горизонтальная)
      case OBJ_VLINE             : break;
      case OBJ_HLINE             : break;
      //--- Линии
      case OBJ_TREND             :
      case OBJ_TRENDBYANGLE      :
      case OBJ_CYCLES            :
      case OBJ_ARROWED_LINE      :
      //--- Каналы
      case OBJ_CHANNEL           :
      case OBJ_STDDEVCHANNEL     :
      case OBJ_REGRESSION        :
      //--- Ганн
      case OBJ_GANNLINE          :
      case OBJ_GANNGRID          :
      //--- Фибоначчи
      case OBJ_FIBO              :
      case OBJ_FIBOTIMES         :
      case OBJ_FIBOFAN           :
      case OBJ_FIBOARC           :
      case OBJ_FIBOCHANNEL       :
      case OBJ_EXPANSION         :
        //--- Расчёт координат для форм на опорных точках линии
        if(index<this.m_base_pivots)
           return(::ChartTimePriceToXY(this.m_base_chart_id,this.m_base_subwindow,this.m_base_time[index],this.m_base_price[index],x,y) ? true : false);
        //--- Расчёт координат для центральной формы, находящейся между опорных точек линии
        else
          {
           form0=this.GetControlPointForm(0);
           form1=this.GetControlPointForm(1);
           if(form0==NULL || form1==NULL)
              return false;
           x=(form0.CoordX()+this.m_shift+form1.CoordX()+this.m_shift)/2;
           y=(form0.CoordY()+this.m_shift+form1.CoordY()+this.m_shift)/2;
           return true;
          }

      //--- Каналы
      case OBJ_PITCHFORK         : break;
      //--- Ганн
      case OBJ_GANNFAN           : break;
      //--- Эллиотт
      case OBJ_ELLIOTWAVE5       : break;
      case OBJ_ELLIOTWAVE3       : break;
      //--- Фигуры
      case OBJ_RECTANGLE         : break;
      case OBJ_TRIANGLE          : break;
      case OBJ_ELLIPSE           : break;
      //--- Стрелки
      case OBJ_ARROW_THUMB_UP    : break;
      case OBJ_ARROW_THUMB_DOWN  : break;
      case OBJ_ARROW_UP          : break;
      case OBJ_ARROW_DOWN        : break;
      case OBJ_ARROW_STOP        : break;
      case OBJ_ARROW_CHECK       : break;
      case OBJ_ARROW_LEFT_PRICE  : break;
      case OBJ_ARROW_RIGHT_PRICE : break;
      case OBJ_ARROW_BUY         : break;
      case OBJ_ARROW_SELL        : break;
      case OBJ_ARROW             : break;
      //--- Графические объекты с координатами время/цена
      case OBJ_TEXT              : break;
      case OBJ_BITMAP            : break;
      //---
      default                    : break;
     }
   return false;
  }
//+------------------------------------------------------------------+

Расчёт для опорных точек делается из значений, записанных в массивах координат опорных точек линии m_base_time и m_base_price. А для расчёта координат центральной точки воспользуемся координатами объектов-форм, прикреплённых к крайним опорным точкам линии. При успешном расчёте координат метод сразу возвращает true. В остальных случаях — либо возвращается false, либо по break обрывается выполнение кода в case переключателя switch и выпадаем к концу метода, где возвращается false.

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

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

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

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


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

//+------------------------------------------------------------------+
//| Создаёт объекты-формы на опорных точках базового объекта         |
//+------------------------------------------------------------------+
bool CGStdGraphObjExtToolkit::CreateAllControlPointForm(void)
  {
   bool res=true;
//--- В цикле по количеству опорных точек базового объекта
   for(int i=0;i<=this.m_base_pivots;i++)
     {
      //--- Создаём новый объект-форму на текущей опорной точке, соответствующей индексу цикла
      CFormControl *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;
        }
     }
//--- Перерисовываем график для отображения изменений (в случае успеха) и возвращаем итоговый результат
   if(res)
      ::ChartRedraw(this.m_base_chart_id);
   return res;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Устанавливает параметры объекта-формы управления опорными точками|
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::SetControlFormParams(CFormControl *form,const int index)
  {
   form.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);                // Объект создан программно
   form.SetActive(true);                                    // Объект-форма активен
   form.SetMovable(true);                                   // Объект перемещаемый
   int x=(int)::floor((form.Width()-CTRL_POINT_RADIUS*2)/2);// Смещение активной зоны от края формы
   form.SetActiveAreaShift(x,x,x,x);                        // Активная область объекта находится в центре формы, её размер равен равен двум значениям CTRL_POINT_RADIUS
   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.DrawRectangle(x,x,form.Width()-x-1,form.Height()-x-1,clrSilver);// Рисование очерчивающего прямоугольника для визуального отображения расположения активной зоны формы
   form.SetID(index+1);                                     // Устанавливаем идентификатор формы
   form.SetControlPointDrawnFlag(false);                    // Устанавливаем флаг, что точка управления не нарисована на форме
   form.Done();                                             // Фиксируем изначальное состояние объекта-формы (его внешний вид)
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Рисует на форме контрольную точку                                |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::DrawControlPoint(CFormControl *form,const uchar opacity,const color clr)
  {
   if(form==NULL)
      return;
   int c=int(::floor(form.Width()/2));                      // Центр формы (координаты)
   form.DrawCircle(c,c,CTRL_POINT_RADIUS,clr,opacity);      // Рисуем окружность в центре формы
   form.DrawCircleFill(c,c,2,clr,opacity);                  // Рисуем точку в центре формы
   form.SetControlPointDrawnFlag(opacity>0 ? true : false); // Устанавливаем флаг, что точка управления нарисована на форме
  }
//+------------------------------------------------------------------+


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

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

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

//+------------------------------------------------------------------+
//| Рисует на форме контрольную точку,                               |
//| на всех остальных формах - удаляет                               |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::DrawOneControlPoint(CFormControl *form,const uchar opacity=255,const color clr=CTRL_POINT_COLOR)
  {
   this.DrawControlPoint(form,opacity,clr);
   for(int i=0;i<this.GetNumControlPointForms();i++)
     {
      CFormControl *ctrl=this.GetControlPointForm(i);
      if(ctrl==NULL || ctrl.ID()==form.ID())
         continue;
      this.ClearControlPoint(ctrl);
     }
  }
//+------------------------------------------------------------------+

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

В обработчике событий заменим тип формы с CForm на CFormControl:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
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++)
        {
         CFormControl *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 нам нужно внести некоторые доработки для оптимизации кода методов. У нас есть повторяющиеся фрагменты одинакового кода в разных методах, поэтому будет целесообразно оформить такие блоки кода в отдельные методы и вызывать их там, где это необходимо, что упростит чтение кода.

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

//--- Возвращает (1) список зависимых объектов, (2) зависимый графический объект по индексу, (3) количество зависимых объектов
   CArrayObj        *GetListDependentObj(void)        { return &this.m_list;           }
   CGStdGraphObj    *GetDependentObj(const int index) { return this.m_list.At(index);  }
   int               GetNumDependentObj(void)         { return this.m_list.Total();    }
//--- Возвращает имя зависимого графического объекта по индексу
   string            NameDependent(const int index);
//--- Добавляет зависимый графический объект в список
   bool              AddDependentObj(CGStdGraphObj *obj);
//--- Изменяет координаты X и Y текущего и всех зависимых объектов
   bool              ChangeCoordsExtendedObj(const int x,const int y,const int modifier,bool redraw=false);
//--- Устанавливает координаты X и Y в связанные опорные точки указанного подчинённого объекта
   bool              SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj);
   
//--- Возвращает объект данных опорных точек
   CLinkedPivotPoint*GetLinkedPivotPoint(void)        { return &this.m_linked_pivots;  }

...

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);
//--- Устанавливает координаты X и Y в связанную опорную точку указанного подчинённого объекта по индексу
   void              SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj,CLinkedPivotPoint *pivot_point,const int index);
//--- Устанавливает в указанный подчинённый объект (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:
//--- Обработчик событий


В методе, проверяющем изменения свойств объекта, удалим указанный блок кода (этот код будет перенесён в отдельный метод):

      //--- Если к объекту присоединены подчинённые (объект является базовым в составном графическом объекте)
      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);
        }

Вместо удалённого блока впишем вызов нового метода:

      //--- Если к объекту присоединены подчинённые (объект является базовым в составном графическом объекте)
      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;
            //--- Устанавливаем координаты X и Y во все опорные точки подчинённого объекта и
            //--- сохраняем текущие свойства подчинённого графического объекта как прошлые
            if(this.SetCoordsXYtoDependentObj(dep))
               dep.PropertiesCopyToPrevData();
           }
         //--- Перемещаем контрольные точки управления на изменённые координаты
         if(this.ExtToolkit!=NULL)
           {
            for(int i=0;i<this.Pivots();i++)
              {
               this.ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i);
              }
            this.ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
            long   lparam=0;
            double dparam=0;
            string sparam="";
            this.ExtToolkit.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
           }
         //--- По завершению цикла обработки всех привязанных объектов, перерисовываем график для отображения всех изменений
         ::ChartRedraw(m_chart_id);
        }


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

//+------------------------------------------------------------------+
//| Перерисовывает форму управления контрольной точкой               |
//| расширенного стандартного графического объекта                   |
//+------------------------------------------------------------------+
void CGStdGraphObj::RedrawControlPointForms(const uchar opacity,const color clr)
  {
//--- Если у объекта нет инструментария расширенного стандартного графического объекта - уходим
   if(this.ExtToolkit==NULL)
      return;
//--- Получаем количество форм управления опорными точками
   int total_form=this.GetNumControlPointForms();
//--- В цикле по количеству форм управления опорными точками
   for(int i=0;i<total_form;i++)
     {
      //--- получаем очередной объект-форму
      CFormControl *form=this.ExtToolkit.GetControlPointForm(i);
      if(form==NULL)
         continue;
      //--- Рисуем на форме точку и окружность с указанной непрозрачностью и цветом
      //--- Если нужно установить полностью прозрачный цвет точки (стереть её),
      //--- и на форме всё ещё нарисована точка - стираем точку,
      if(opacity==0 && form.IsControlAlreadyDrawn())
         this.ExtToolkit.DrawControlPoint(form,0,clr);
      //--- иначе - рисуем точку с указанной непрозрачностью и цветом
      else
         this.ExtToolkit.DrawControlPoint(form,opacity,clr);
     }
   
//--- Получаем общее количество привязанных графических объектов
   int total_dep=this.GetNumDependentObj();
//--- В цикле по всем привязанным графическим объектам
   for(int i=0;i<total_dep;i++)
     {
      //--- получаем очередной графический объект из списка
      CGStdGraphObj *dep=this.GetDependentObj(i);
      if(dep==NULL)
         continue;
      //--- вызываем для него этот метод
      dep.RedrawControlPointForms(opacity,clr);
     }
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Изменяет координаты X и Y текущего и всех зависимых объектов     |
//+------------------------------------------------------------------+
bool CGStdGraphObj::ChangeCoordsExtendedObj(const int x,const int y,const int modifier,bool redraw=false)
  {
//--- Устанавливаем новые координаты для указанной в modifier опорной точки
   if(!this.SetTimePrice(x,y,modifier))
      return false;
//--- Если объект не является составным графическим объектом,
//--- или если к объекту не присоединены подчинённые графические объекты -
//--- больше тут делать нечего, возвращаем true
   if(this.ExtToolkit==NULL || this.m_list.Total()==0)
      return true;
//--- Получаем привязанный к точке modifier графический объект
   CGStdGraphObj *dep=this.GetDependentObj(modifier);
   if(dep==NULL)
      return false;
//--- Получаем объект данных опорных точек привязанного графического объекта
   CLinkedPivotPoint *pp=dep.GetLinkedPivotPoint();
   if(pp==NULL)
      return false;
//--- получаем количество координатных точек, к которым прикреплён объект
   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, его модификатор,
         //--- и устанавливаем его в подчинённый графический объект, присоединённый к точке modifier
         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, его модификатор,
         //--- и устанавливаем его в подчинённый графический объект, присоединённый к точке modifier
         int prop_from=pp.GetPropertyY(j,ny);
         int modifier_from=pp.GetPropertyModifierY(j,ny);
         this.SetCoordYToDependentObj(dep,prop_from,modifier_from,ny);
        }
     }
//--- Сохраняем текущие свойства подчинённого графического объекта как прошлые
   dep.PropertiesCopyToPrevData();
//--- Перемещаем контрольную точку управления на изменённые координаты
   this.ExtToolkit.SetBaseObjTimePrice(this.Time(modifier),this.Price(modifier),modifier);
   this.ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
//--- Если стоит флаг, перерисовываем график
   if(redraw)
      ::ChartRedraw(m_chart_id);
//--- Всё успешно - возвращаем true
   return true;
  }
//+------------------------------------------------------------------+

Теперь метод будет намного проще:

//+------------------------------------------------------------------+
//| Изменяет координаты X и Y текущего и всех зависимых объектов     |
//+------------------------------------------------------------------+
bool CGStdGraphObj::ChangeCoordsExtendedObj(const int x,const int y,const int modifier,bool redraw=false)
  {
//--- Устанавливаем новые координаты для указанной в modifier опорной точки
   if(!this.SetTimePrice(x,y,modifier))
      return false;
//--- Если объект является составным графическим объектом,
//--- и к объекту присоединены подчинённые графические объекты
   if(this.ExtToolkit!=NULL && this.m_list.Total()>0)
     {
      //--- Получаем привязанный к точке modifier графический объект
      CGStdGraphObj *dep=this.GetDependentObj(modifier);
      if(dep==NULL)
         return false;
      //--- Устанавливаем координаты X и Y во все опорные точки подчинённого объекта и
      //--- сохраняем текущие свойства подчинённого графического объекта как прошлые
      if(this.SetCoordsXYtoDependentObj(dep))
         dep.PropertiesCopyToPrevData();
     }
//--- Перемещаем контрольную точку управления на изменённые координаты
   this.ExtToolkit.SetBaseObjTimePrice(this.Time(modifier),this.Price(modifier),modifier);
   this.ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
//--- Если стоит флаг, перерисовываем график
   if(redraw)
      ::ChartRedraw(m_chart_id);
//--- Всё успешно - возвращаем true
   return true;
  }
//+------------------------------------------------------------------+


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

//+------------------------------------------------------------------+
//| Устанавливает координаты X и Y в связанную опорную точку         |
//| указанного подчинённого объекта по индексу                       |
//+------------------------------------------------------------------+
void CGStdGraphObj::SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj,CLinkedPivotPoint *pivot_point,const int index)
  {
//--- получаем количество координатных точек базового объекта для установки координаты X
   int numx=pivot_point.GetBasePivotsNumX(index);
//--- В цикле по каждой координатной точке для установки координаты X
   for(int nx=0;nx<numx;nx++)
     {
      //--- получаем свойство для установки координаты X, его модификатор,
      //--- и устанавливаем его в подчинённый графический объект, присоединённый к точке index
      int prop_from=pivot_point.GetPropertyX(index,nx);
      int modifier_from=pivot_point.GetPropertyModifierX(index,nx);
      this.SetCoordXToDependentObj(dependent_obj,prop_from,modifier_from,nx);
     }
//--- Получаем количество координатных точек базового объекта для установки координаты Y
   int numy=pivot_point.GetBasePivotsNumY(index);
   //--- В цикле по каждой координатной точке для установки координаты Y
   for(int ny=0;ny<numy;ny++)
     {
      //--- получаем свойство для установки координаты Y, его модификатор,
      //--- и устанавливаем его в подчинённый графический объект, присоединённый к точке index
      int prop_from=pivot_point.GetPropertyY(index,ny);
      int modifier_from=pivot_point.GetPropertyModifierY(index,ny);
      this.SetCoordYToDependentObj(dependent_obj,prop_from,modifier_from,ny);
     }
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Устанавливает координаты X и Y в связанные опорные точки         |
//| указанного подчинённого объекта                                  |
//+------------------------------------------------------------------+
bool CGStdGraphObj::SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj)
  {
//--- Получаем объект данных опорных точек привязанного графического объекта
   CLinkedPivotPoint *pp=dependent_obj.GetLinkedPivotPoint();
   if(pp==NULL)
      return false;
//--- получаем количество координатных точек, к которым прикреплён объект
   int num=pp.GetNumLinkedCoords();
//--- В цикле по координатным точкам объекта
//--- устанавливаем координаты X и Y во все опорные точки подчинённого объекта
   for(int j=0;j<num;j++)
      this.SetCoordsXYtoDependentObj(dependent_obj,pp,j);
   return true;
  }
//+------------------------------------------------------------------+

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


Впишем доработки в класс-коллекцию графических элементов \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh.

Так как стандартная функция ChartTimePriceToXY() возвращает нам сразу две координаты — X и Y, то для их хранения создадим в приватной секции структуру, в которой будут храниться координаты X и Y и смещения этих координат относительно центральной точки. А так как у графического объекта могут быть несколько опорных точек, то для того, чтобы сохранить для этого графического объекта координаты каждой его опорной точки, объявим массив с типом созданной структуры. Тогда в каждой ячейке этого массива мы будем иметь преобразованные из координаты " время/цена" в экранные X и Y, а также и смещения кооринат опорной точки относительно центральной точки графического объекта.

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

//+------------------------------------------------------------------+
//| Коллекция графических объектов                                   |
//+------------------------------------------------------------------+
#resource "\\"+PATH_TO_EVENT_CTRL_IND;          // Индикатор контроля событий графических объектов, упакованный в ресурсы программы
class CGraphElementsCollection : public CBaseObj
  {
private:
   //--- Структура данных опорной точки
   struct SDataPivotPoint
     {
      public:
         int         X;                         // Координата X опорной точки
         int         Y;                         // Координата Y опорной точки
         int         ShiftX;                    // Смещение координаты X опорной точки от центральной
         int         ShiftY;                    // Смещение координаты Y опорной точки от центральной
     };
   SDataPivotPoint   m_data_pivot_point[];      // Массив-структуры данных опорной точки
   CArrayObj         m_list_charts_control;     // Список объектов управления чартами
   CListObj          m_list_all_canv_elm_obj;   // Список всех графических элементов на канвасе
   CListObj          m_list_all_graph_obj;      // Список всех графических объектов
   CArrayObj         m_list_deleted_obj;        // Список удалённых графических объектов
   CMouseState       m_mouse;                   // Объект класса "Состояния мышки"
   bool              m_is_graph_obj_event;      // Флаг события в списке графических объектов
   int               m_total_objects;           // Количество графических объектов
   int               m_delta_graph_obj;         // Разница в количестве графических объектов по сравнению с прошлой проверкой

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

private:
//--- Находит объект, имеющийся в коллекции, но отсутствующий на графике
   CGStdGraphObj    *FindMissingObj(const long chart_id);
   CGStdGraphObj    *FindMissingObj(const long chart_id,int &index);
//--- Находит графический объект, имеющийся на графике, но отсутствующий в коллекции
   string            FindExtraObj(const long chart_id);
//--- Удаляет (1) указанный, (2) по идентификатору графика объект класса графического объекта из списка-коллекции графических объектов
   bool              DeleteGraphObjFromList(CGStdGraphObj *obj);
   void              DeleteGraphObjectsFromList(const long chart_id);
//--- Перемещает (1) указанный, (2) по индексу объект класса графического объекта в список удалённых графических объектов
   bool              MoveGraphObjToDeletedObjList(CGStdGraphObj *obj);
   bool              MoveGraphObjToDeletedObjList(const int index);
//--- Перемещает в список удалённых графических объектов все объекты по идентификатору графика
   void              MoveGraphObjectsToDeletedObjList(const long chart_id);
//--- Удаляет объект управления графиками из списка
   bool              DeleteGraphObjCtrlObjFromList(CChartObjectsControl *obj);
//--- Устанавливает для указанного графика флаги прокрутки чарта колёсиком мышки, контекстного меню и инструмента "перекрестие"
   void              SetChartTools(const long chart_id,const bool flag);
//--- Возвращает экранные координаты каждой опорной точки графического объекта
   bool              GetPivotPointCoordsAll(CGStdGraphObj *obj,SDataPivotPoint &array_pivots[]);
public:

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

//+------------------------------------------------------------------+
//| Возвращает экранные координаты                                   |
//| каждой опорной точки графического объекта                        |
//+------------------------------------------------------------------+
bool CGraphElementsCollection::GetPivotPointCoordsAll(CGStdGraphObj *obj,SDataPivotPoint &array_pivots[])
  {
//--- Если не удалось увеличить массив структур под количество опорных точек,
//--- сообщаем об этом в журнал и возвращаем false
   if(::ArrayResize(array_pivots,obj.Pivots())!=obj.Pivots())
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      return false;
     }
//--- В цикле по количеству опорных точек графического объекта
   for(int i=0;i<obj.Pivots();i++)
     {
      //--- Преобразуем координаты объекта в экранные и, если это не получилось - сообщаем об этом в журнал и возвращаем false
      if(!::ChartTimePriceToXY(obj.ChartID(),obj.SubWindow(),obj.Time(i),obj.Price(i),array_pivots[i].X,array_pivots[i].Y))
       {
        CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_CONV_GRAPH_OBJ_COORDS_TO_XY);
        return false;
       }
     }
//--- В зависимости от типа графического объекта
   switch(obj.TypeGraphObject())
     {
      //--- Одна опорная точка в экранных координатах
      case OBJ_LABEL             :
      case OBJ_BUTTON            :
      case OBJ_BITMAP_LABEL      :
      case OBJ_EDIT              :
      case OBJ_RECTANGLE_LABEL   :
      case OBJ_CHART             : break;
      
      //--- Одна опорная точка (только цена)
      case OBJ_HLINE             : break;
      
      //--- Одна опорная точка (только время)
      case OBJ_VLINE             : break;
      case OBJ_EVENT             : break;
      
      //--- Две опорные точки и центральная
      //--- Линии
      case OBJ_TREND             :
      case OBJ_TRENDBYANGLE      :
      case OBJ_CYCLES            :
      case OBJ_ARROWED_LINE      :
      //--- Каналы
      case OBJ_CHANNEL           :
      case OBJ_STDDEVCHANNEL     :
      case OBJ_REGRESSION        :
      //--- Ганн
      case OBJ_GANNLINE          :
      case OBJ_GANNGRID          :
      //--- Фибоначчи
      case OBJ_FIBO              :
      case OBJ_FIBOTIMES         :
      case OBJ_FIBOFAN           :
      case OBJ_FIBOARC           :
      case OBJ_FIBOCHANNEL       :
      case OBJ_EXPANSION         :
        //--- Рассчитываем и записываем в массив-структуру смещения всех опорных точек от центральной
        array_pivots[0].ShiftX=(array_pivots[1].X-array_pivots[0].X)/2; 
        array_pivots[0].ShiftY=(array_pivots[1].Y-array_pivots[0].Y)/2;
        array_pivots[1].ShiftX=(array_pivots[0].X-array_pivots[1].X)/2;
        array_pivots[1].ShiftY=(array_pivots[0].Y-array_pivots[1].Y)/2;
        return true;

      //--- Каналы
      case OBJ_PITCHFORK         : break;
      //--- Ганн
      case OBJ_GANNFAN           : break;
      //--- Эллиотт
      case OBJ_ELLIOTWAVE5       : break;
      case OBJ_ELLIOTWAVE3       : break;
      //--- Фигуры
      case OBJ_RECTANGLE         : break;
      case OBJ_TRIANGLE          : break;
      case OBJ_ELLIPSE           : break;
      //--- Стрелки
      case OBJ_ARROW_THUMB_UP    : break;
      case OBJ_ARROW_THUMB_DOWN  : break;
      case OBJ_ARROW_UP          : break;
      case OBJ_ARROW_DOWN        : break;
      case OBJ_ARROW_STOP        : break;
      case OBJ_ARROW_CHECK       : break;
      case OBJ_ARROW_LEFT_PRICE  : break;
      case OBJ_ARROW_RIGHT_PRICE : break;
      case OBJ_ARROW_BUY         : break;
      case OBJ_ARROW_SELL        : break;
      case OBJ_ARROW             : break;
      //--- Графические объекты с координатами время/цена
      case OBJ_TEXT              : break;
      case OBJ_BITMAP            : break;
      //---
      default: break;
     }
   return false;
  }
//+------------------------------------------------------------------+

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

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

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

Впишем такую обработку события перемещения центральной контрольной точки (формы) расширенного графического объекта:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CGraphElementsCollection::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   CGStdGraphObj *obj_std=NULL;  // Указатель на стандартный графический объект
   CGCnvElement  *obj_cnv=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_std=this.GetStdGraphObject(sparam,chart_id);
      //--- Если объект не удалось получить по имени - он отсутствует в списке,
      //--- а значит его имя было изменено
      if(obj_std==NULL)
        {
         //--- Найдём в списке объект, которого нет на графике
         obj_std=this.FindMissingObj(chart_id);
         //--- Если и тут не удалось найти объект - уходим
         if(obj_std==NULL)
            return;
         //--- Получим имя переименованного  графического объекта на графике, которого нет в списке-коллекции
         string name_new=this.FindExtraObj(chart_id);
         //--- установим новое имя объекту в списке-коллекции, которому не соответствует ни один графический объект на графике,
         //--- и отправим событие с новым именем объекта на график управляющей программы
         if(obj_std.SetNamePrev(obj_std.Name()) && obj_std.SetName(name_new))
            ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_RENAME,obj_std.ChartID(),obj_std.TimeCreate(),obj_std.Name());
        }
      //--- Обновим свойства полученного объекта
      //--- и проверим их изменение
      obj_std.PropertiesRefresh();
      obj_std.PropertiesCheckChanged();
     }

//--- Обработка событий стандартных графических объектов в списке-коллекции
   for(int i=0;i<this.m_list_all_graph_obj.Total();i++)
     {
      //--- Получаем очередной графический объект и
      obj_std=this.m_list_all_graph_obj.At(i);
      if(obj_std==NULL)
         continue;
      //--- вызываем его обработчик событий
      obj_std.OnChartEvent((id<CHARTEVENT_CUSTOM ? id : idx),lparam,dparam,sparam);
     }

//--- Обработка изменения графика для расширенных стандартных объектов
   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_std=list.At(i);
            if(obj_std==NULL)
               continue;
            obj_std.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
           }
        }
     }
//--- Обработка событий мышки графических объектов на канвасе
//--- Если событие - не изменение графика
   else
     {
      //--- Проверим нажата ли кнопка мышки
      bool pressed=(this.m_mouse.ButtonKeyState(id,lparam,dparam,sparam)==MOUSE_BUTT_KEY_STATE_LEFT ? true : false);
      ENUM_MOUSE_FORM_STATE mouse_state=MOUSE_FORM_STATE_NONE;
      //--- Объявим статические переменные для активной формы и флагов состояний
      static CForm *form=NULL;
      static bool pressed_chart=false;
      static bool pressed_form=false;
      static bool move=false;
      //--- Объявим статические переменные для индекса формы управления расширенным стандартным графическим объектом и его идентификатора
      static int  form_index=WRONG_VALUE;
      static long graph_obj_id=WRONG_VALUE;
      
      //--- Если кнопка не зажата на графике и не стоит флаг перемещения - получаем форму, над которой находится курсор
      if(!pressed_chart && !move)
         form=this.GetFormUnderCursor(id,lparam,dparam,sparam,mouse_state,graph_obj_id,form_index);
      
      //--- Если не нажата кнопка - сбрасываем все флаги и разрешаем инструментарий графика 
      if(!pressed)
        {
         pressed_chart=false;
         pressed_form=false;
         move=false;
         this.SetChartTools(::ChartID(),true);
        }
      
      //--- Если событие перемещения мышки и стоит флаг перемещения - смещаем форму за курсором (если указатель на неё валидный)
      if(id==CHARTEVENT_MOUSE_MOVE && move)
        {
         if(form!=NULL)
           {
            //--- рассчитываем смещение курсора относительно начала координат формы
            int x=this.m_mouse.CoordX()-form.OffsetX();
            int y=this.m_mouse.CoordY()-form.OffsetY();
            //--- получаем ширину и высоту графика, на котором находится форма
            int chart_width=(int)::ChartGetInteger(form.ChartID(),CHART_WIDTH_IN_PIXELS,form.SubWindow());
            int chart_height=(int)::ChartGetInteger(form.ChartID(),CHART_HEIGHT_IN_PIXELS,form.SubWindow());
            //--- Если форма не в составе расширенного стандартного графического объекта
            if(form_index==WRONG_VALUE)
              {
               //--- Корректируем рассчитанные координаты для формы в случае выхода формы за пределы графика
               if(x<0) x=0;
               if(x>chart_width-form.Width()) x=chart_width-form.Width();
               if(y<0) y=0;
               if(y>chart_height-form.Height()) y=chart_height-form.Height();
               //--- Если на графике нет панели торговли в один клик
               if(!::ChartGetInteger(form.ChartID(),CHART_SHOW_ONE_CLICK))
                 {
                  //--- рассчитаем координаты формы так, чтобы при её перемещении на кнопку включения панели торговли в один клик, форма под неё не попадала
                  if(y<17 && x<41)
                     y=17;
                 }
               //--- Если на графике отображена панель торговли в один клик
               else
                 {
                  //--- рассчитаем координаты формы так, чтобы при её перемещении на панель торговли в один клик, форма под неё не попадала
                  if(y<80 && x<192)
                     y=80;
                 }
              }
            //--- Если форма входит в состав расширенного стандартного графического объекта
            else
              {
               if(graph_obj_id>WRONG_VALUE)
                 {
                  //--- Получаем список объектов по идентификатору объекта (должен быть один объект)
                  CArrayObj *list_ext=CSelect::ByGraphicStdObjectProperty(GetListStdGraphObjectExt(),GRAPH_OBJ_PROP_ID,0,graph_obj_id,EQUAL);
                  //--- Если список получить удалось и он не пустой
                  if(list_ext!=NULL && list_ext.Total()>0)
                    {
                     //--- получаем графический объект из списка
                     CGStdGraphObj *ext=list_ext.At(0);
                     //--- Если указатель на объект получен
                     if(ext!=NULL)
                       {
                        //--- получаем тип объекта
                        ENUM_OBJECT type=ext.GraphObjectType();
                        //--- Если объект строится по экранным координатам - записываем координаты в объект
                        if(type==OBJ_LABEL || type==OBJ_BUTTON || type==OBJ_BITMAP_LABEL || type==OBJ_EDIT || type==OBJ_RECTANGLE_LABEL)
                          {
                           ext.SetXDistance(x);
                           ext.SetYDistance(y);
                          }
                        //--- иначе, если объект строится по координатам время/цена
                        else
                          {
                           //--- рассчитываем смещение координат
                           int shift=(int)::ceil(form.Width()/2)+1;
                           //--- Если форма находится на одной из опорных точек графического объекта
                           if(form_index<ext.Pivots())
                             {
                              //--- ограничиваем координаты формы так, чтобы они не выходили за пределы графика
                              if(x+shift<0)
                                 x=-shift;
                              if(x+shift>chart_width)
                                 x=chart_width-shift;
                              if(y+shift<0)
                                 y=-shift;
                              if(y+shift>chart_height)
                                 y=chart_height-shift;
                              //--- устанавливаем в объект рассчитанные координаты
                              ext.ChangeCoordsExtendedObj(x+shift,y+shift,form_index);
                             }
                           //--- Если форма - центральная для управления всеми опорными точками графического объекта
                           else
                             {
                              //--- Получаем и записываем в структуру m_data_pivot_point экранные координаты всех опорных точек объекта
                              if(this.GetPivotPointCoordsAll(ext,m_data_pivot_point))
                                {
                                 //--- В цикле по количеству опорных точек объекта
                                 for(int i=0;i<(int)this.m_data_pivot_point.Size();i++)
                                   {
                                    //--- ограничиваем экранные координаты текущей опорной точки так, чтобы они не выходили за пределы графика
                                    if(x+shift-this.m_data_pivot_point[i].ShiftX<0)
                                       x=-shift+m_data_pivot_point[i].ShiftX;
                                    if(x+shift+this.m_data_pivot_point[i].ShiftX>chart_width)
                                       x=chart_width-shift-this.m_data_pivot_point[i].ShiftX;
                                    if(y+shift+this.m_data_pivot_point[i].ShiftY<0)
                                       y=-shift-this.m_data_pivot_point[i].ShiftY;
                                    if(y+shift-this.m_data_pivot_point[i].ShiftY>chart_height)
                                       y=chart_height-shift+this.m_data_pivot_point[i].ShiftY;
                                    //--- устанавливаем в текущую опорную точку объекта рассчитанные координаты
                                    ext.ChangeCoordsExtendedObj(x+shift-this.m_data_pivot_point[i].ShiftX,y+shift-this.m_data_pivot_point[i].ShiftY,i);
                                   }
                                }
                             }
                          }
                       }
                    }
                 }
              }
            //--- Смещаем форму на полученные координаты
            form.Move(x,y,true);
           }
        }
   
      //--- Выводим на график отладочные комментарии
      Comment
        (
         (form!=NULL ? form.Name()+":" : ""),"\n",
         EnumToString((ENUM_CHART_EVENT)id),"\n",
         EnumToString(this.m_mouse.ButtonKeyState(id,lparam,dparam,sparam)),
         "\n",EnumToString(mouse_state),
         "\npressed=",pressed,", move=",move,(form!=NULL ? ", Interaction="+(string)form.Interaction() : ""),
         "\npressed_chart=",pressed_chart,", pressed_form=",pressed_form,
         "\nform_index=",form_index,", graph_obj_id=",graph_obj_id
        );
      
      //--- Если курсор не над формой
      if(form==NULL)
        {
         //--- Если нажата кнопка мышки
         if(pressed)
           {
            //--- Если кнопка всё ещё нажата и удерживается на форме - уходим
            if(pressed_form)
              {
               return;
              }
            //--- Если ещё не стоит флаг удержания кнопки на чарте - ставим флаги и разрешаем инструментарий графика
            if(!pressed_chart)
              {
               pressed_chart=true;  // Кнопка удерживается на графике
               pressed_form=false;  // Курсор не над формой
               move=false;          // смешщение запрещено
               this.SetChartTools(::ChartID(),true);
              }
           }
         //--- Если не нажата кнопка мышки
         else
           {
            //--- Получаем список расширенных стандартных графических объектов
            CArrayObj *list_ext=GetListStdGraphObjectExt();
            //--- В цикле по всем расширенным графическим объектм
            int total=list_ext.Total();
            for(int i=0;i<total;i++)
              {
               //--- получаем очередной графический объект
               CGStdGraphObj *obj=list_ext.At(i);
               if(obj==NULL)
                  continue;
               //--- и перерисовываем его без точки с окружностью
               obj.RedrawControlPointForms(0,CTRL_POINT_COLOR);
              }
           }
        }
      //--- Если курсор над формой
      else
        {
         //--- Если кнопка всё ещё нажата и удерживается на графике - уходим
         if(pressed_chart)
           {
            return;
           }
         
         //--- Если ещё не установлен флаг удержания кнопки на форме
         if(!pressed_form)
           {
            pressed_chart=false;    // Кнопка не удерживается на графике
            this.SetChartTools(::ChartID(),false);
            
            //--- Обработчик события Курсор в пределах формы, кнопки мышки не нажаты
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED)
              {
               //--- Если курсор над формой управления опорной точкой расширенного графического объекта
               if(graph_obj_id>WRONG_VALUE)
                 {
                  //--- получаем объект по его идентификатору и идентификатору графика
                  CGStdGraphObj *graph_obj=this.GetStdGraphObjectExt(graph_obj_id,form.ChartID());
                  if(graph_obj!=NULL)
                    {
                     //--- Получаем инструментарий расширенного стандартного графического объекта
                     CGStdGraphObjExtToolkit *toolkit=graph_obj.GetExtToolkit();
                     if(toolkit!=NULL)
                       {
                        //--- Рисуем на форме точку с окружностью, на всех остальных формах - удаляем
                        toolkit.DrawOneControlPoint(form);
                       }
                    }
                 }
              }
            //--- Обработчик события Курсор в пределах формы, нажата кнопка мышки (любая)
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_PRESSED)
              {
               this.SetChartTools(::ChartID(),false);
               //--- Если ещё не установлен флаг удержания формы
               if(!pressed_form)
                 {
                  pressed_form=true;      // ставим флаг нажатия на форме
                  pressed_chart=false;    // снимаем флаг нажатия на графике
                 }
              }
            //--- Заготовка обработчика события Курсор в пределах формы, прокручивается колёсико мышки
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_WHEEL)
              {
               
              }
            
            
            //--- Обработчик события Курсор в пределах активной области, кнопки мышки не нажаты
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED)
              {
               //--- Установим смещение курсора относительно начальных координат формы
               form.SetOffsetX(this.m_mouse.CoordX()-form.CoordX());
               form.SetOffsetY(this.m_mouse.CoordY()-form.CoordY());
               //--- Если курсор над активной зоной формы управления опорной точкой расширенного графического объекта
               if(graph_obj_id>WRONG_VALUE)
                 {
                  //--- получаем объект по его идентификатору и идентификатору графика
                  CGStdGraphObj *graph_obj=this.GetStdGraphObjectExt(graph_obj_id,form.ChartID());
                  if(graph_obj!=NULL)
                    {
                     //--- Получаем инструментарий расширенного стандартного графического объекта
                     CGStdGraphObjExtToolkit *toolkit=graph_obj.GetExtToolkit();
                     if(toolkit!=NULL)
                       {
                        //--- Рисуем на форме точку с окружностью, на всех остальных формах - удаляем
                        toolkit.DrawOneControlPoint(form);
                       }
                    }
                 }
              }
            //--- Обработчик события Курсор в пределах активной области, нажата кнопка мышки (любая)
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED && !move)
              {
               pressed_form=true;   // флаг удержания кнопки мышки на форме
               //--- Если зажата левая кнопка мышки
               if(this.m_mouse.IsPressedButtonLeft())
                 {
                  //--- Установим флаги и параметры формы
                  move=true;                                            // флаг перемещения
                  form.SetInteraction(true);                            // флаг взаимодействия формы со внешней средой
                  form.BringToTop();                                    // форма на передний план - поверх всех остальных
                  this.ResetAllInteractionExeptOne(form);               // Сбросим флаги взаимодействия для всех форм, кроме текущей
                  form.SetOffsetX(this.m_mouse.CoordX()-form.CoordX()); // Смещение курсора относительно координаты X
                  form.SetOffsetY(this.m_mouse.CoordY()-form.CoordY()); // Смещение курсора относительно координаты Y
                 }
              }
            //--- Заготовка обработчика события Курсор в пределах активной области, прокручивается колёсико мышки
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL)
              {
               
              }
            
            
            //--- Заготовка обработчика события Курсор в пределах области прокрутки окна, кнопки мышки не нажаты
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED)
              {
               
              }
            //--- Заготовка обработчика события Курсор в пределах области прокрутки окна, нажата кнопка мышки (любая)
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED)
              {
               
              }
            //--- Заготовка обработчика события Курсор в пределах области прокрутки окна, прокручивается колёсико мышки
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL)
              {
               
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

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

На данный момент у нас всё готово для тестирования нового функционала.


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

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

Никаких изменений в самом советнике нам делать не придётся — все изменения пока производятся лишь в классах библиотеки.

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


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


Что далее

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

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

К содержанию

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

Графика в библиотеке DoEasy (Часть 93): Готовим функционал для создания составных графических объектов
Графика в библиотеке DoEasy (Часть 94): Составные графические объекты, перемещение и удаление
Графика в библиотеке DoEasy (Часть 95): Элементы управления составными графическими объектами
Графика в библиотеке DoEasy (Часть 96): Работа с событиями мышки и графика в объектах-формах
Графика в библиотеке DoEasy (Часть 97): Независимая обработка перемещения объектов-форм
Графика в библиотеке DoEasy (Часть 98): Перемещаем опорные точки расширенных стандартных графических объектов

Прикрепленные файлы |
MQL5.zip (4447.2 KB)
Графика в библиотеке DoEasy (Часть 100): Устраняем недочёты при работе с расширенными стандартными графическими объектами Графика в библиотеке DoEasy (Часть 100): Устраняем недочёты при работе с расширенными стандартными графическими объектами
Сегодня мы немного "подчистим хвосты" — устраним явные недоработки при одновременной работе с расширенными (и стандартными) графическими объектами и объектами-формами на канвасе и исправим ошибки, замеченные при тестировании в прошлой статье. И на этом завершим этот раздел описания библиотеки.
Как разрабатывать системы на основе скользящих средних Как разрабатывать системы на основе скользящих средних
Есть множество различных способов для фильтрации сигналов, генерируемых какой-либо стратегией. Вероятно, простейший из них — это использование скользящей средней. Об этом мы и поговорим в этой статье.
WebSocket для MetaTrader 5 — Использование Windows API WebSocket для MetaTrader 5 — Использование Windows API
В этой статье мы используем WinHttp.dll, чтобы создать клиент WebSocket для MetaTrader 5-программ. В конечном итоге клиент должен быть выполнен в виде класса и протестирован во взаимодействии с WebSocket API от Binary.com.
Как и зачем разрабатывать собственную систему для алгоритмической торговли Как и зачем разрабатывать собственную систему для алгоритмической торговли
В этой статье мы рассмотрим основы языка программирования MQL. Цель статьи — помочь начинающим программистам разработать собственную систему алгоритмической торговли (торгового советника).