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

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

MetaTrader 5Примеры | 4 марта 2022, 13:17
1 744 0
Artyom Trishkin
Artyom Trishkin

Содержание


Концепция

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

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

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


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


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

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

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

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


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

Откроем файл \MQL5\Include\DoEasy\Defines.mqh и внесём в него некоторые доработки.

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

//--- Идентификаторы типов отложенных запросов
#define PENDING_REQUEST_ID_TYPE_ERR    (1)                        // Тип отложенного запроса, созданного по коду возврата сервера
#define PENDING_REQUEST_ID_TYPE_REQ    (2)                        // Тип отложенного запроса, созданного по запросу
//--- Параметры таймсерий
#define SERIES_DEFAULT_BARS_COUNT      (1000)                     // Требуемое количество данных таймсерий по умолчанию
#define PAUSE_FOR_SYNC_ATTEMPTS        (16)                       // Количество миллисекунд паузы между попытками синхронизации
#define ATTEMPTS_FOR_SYNC              (5)                        // Количество попыток получения факта синхронизации с сервером
//--- Параметры тиковых серий
#define TICKSERIES_DEFAULT_DAYS_COUNT  (1)                        // Требуемое количество дней для тиковых данных в сериях по умолчанию
#define TICKSERIES_MAX_DATA_TOTAL      (200000)                   // Максимальное количество хранимых тиковых данных одного символа
//--- Параметры серий снимков стакана цен
#define MBOOKSERIES_DEFAULT_DAYS_COUNT (1)                        // Требуемое количество дней для снимков стакана цен в сериях по умолчанию
#define MBOOKSERIES_MAX_DATA_TOTAL     (200000)                   // Максимальное количество хранимых снимков стакана цен одного символа
//--- Параметры канваса
#define PAUSE_FOR_CANV_UPDATE          (16)                       // Частота обновления канваса
#define CLR_CANV_NULL                  (0x00FFFFFF)               // Ноль для канваса с альфа-каналом
#define OUTER_AREA_SIZE                (16)                       // Размер одной стороны внешней области вокруг рабочего пространства формы
//--- Параметры графических объектов
#define PROGRAM_OBJ_MAX_ID             (10000)                    // Максимальное значение идентификатора графического объекта, принадлежащего программе
#define CTRL_POINT_RADIUS              (5)                        // Радиус контрольной точки на форме управления опорными точками графического объекта
#define CTRL_POINT_COLOR               (clrDodgerBlue)            // Цвет контрольной точки на форме управления опорными точками графического объекта
#define CTRL_FORM_SIZE                 (40)                       // Размер формы контрольной точки управления опорными точками графического объекта
//+------------------------------------------------------------------+
//| Перечисления                                                     |
//+------------------------------------------------------------------+

Макроподстановку с наименованием CTRL_POINT_SIZE переименуем в CTRL_POINT_RADIUS, так как это не полный размер окружности, а её радиус. Просто наименование этой макроподстановки немного вводило в заблуждение при расчёте активной области объекта-формы.

В файле класса объекта графического элемента \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh немного доработаем метод создания графического объекта-элемента. При вызове метода CreateBitmapLabel() класса CCanvas, к сожалению, нет возврата кода ошибки. Поэтому мы перед вызовом этого метода сбросим код последней ошибки, а если графическую метку создать не удалось — выведем в журнал сообщение с кодом ошибки. Это немного облегчит отладку.

//+------------------------------------------------------------------+
//| Создаёт графический объект-элемент                               |
//+------------------------------------------------------------------+
bool CGCnvElement::Create(const long chart_id,     // Идентификатор графика
                          const int wnd_num,       // Подокно графика
                          const string name,       // Имя элемента
                          const int x,             // Координата X
                          const int y,             // Координата Y
                          const int w,             // Ширина
                          const int h,             // Высота
                          const color colour,      // Цвет фона
                          const uchar opacity,     // Непрозрачность
                          const bool redraw=false) // Флаг необходимости перерисовки
                         
  {
   ::ResetLastError();
   if(this.m_canvas.CreateBitmapLabel(chart_id,wnd_num,name,x,y,w,h,COLOR_FORMAT_ARGB_NORMALIZE))
     {
      this.Erase(CLR_CANV_NULL);
      this.m_canvas.Update(redraw);
      this.m_shift_y=(int)::ChartGetInteger(chart_id,CHART_WINDOW_YDISTANCE,wnd_num);
      return true;
     }
   CMessage::ToLog(DFUN,::GetLastError(),true);
   return false;
  }
//+------------------------------------------------------------------+

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

В общем-то, эта доработка тоже не укажет нам на истинный код ошибки в случае, если длина имени ресурса будет превышать 63 символа:

ERR_RESOURCE_NAME_IS_TOO_LONG     4018     Имя ресурса превышает 63 символа

а вернёт код ошибки

ERR_RESOURCE_NOT_FOUND    4016    Ресурс с таким именем в EX5 не найден

но это всё же лучше, и сразу наталкивает на мысль "а почему не создан графический ресурс?"...


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

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

//--- (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);
//--- Возвращает количество (1) опорных точек базового объекта, (2) созданных объектов-форм управления контрольными точками
   int               GetNumPivotsBaseObj(void)                          const { return this.m_base_pivots;                    }
   int               GetNumControlPointForms(void)                      const { return this.m_list_forms.Total();             }
//--- Создаёт объекты-формы на опорных точках базового объекта
   bool              CreateAllControlPointForm(void);
//--- Рисует на форме контрольную точку
   void              DrawControlPoint(CForm *form,const uchar opacity,const color clr);
//--- Удаляет все объекты-формы из списка
   void              DeleteAllControlPointForm(void);


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

//+------------------------------------------------------------------+
//| Создаёт объект-форму на опорной точке базового объекта           |
//+------------------------------------------------------------------+
CForm *CGStdGraphObjExtToolkit::CreateNewControlPointForm(const int index)
  {
   string name=this.m_base_name+"_CP_"+(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());
  }
//+------------------------------------------------------------------+

Именно здесь (и в файле тестового советника) мне пришлось укоротить имя создаваемого объекта-формы, так как имя графического ресурса получалось более 63-х символов, и объект не создавался. Причина кроется в методе создания динамического ресурса в классе CCanvas, где имя создаваемого ресурса складывается из символов "::" + переданное в метод имя (которое мы указываем в вышерассмотренном методе) + идентификатор графика + количество миллисекунд, прошедших с момента старта системы + псевдослучайное число:

//+------------------------------------------------------------------+
//| Create dynamic resource                                          |
//+------------------------------------------------------------------+
bool CCanvas::Create(const string name,const int width,const int height,ENUM_COLOR_FORMAT clrfmt)
  {
   Destroy();
//--- prepare data array
   if(width>0 && height>0 && ArrayResize(m_pixels,width*height)>0)
     {
      //--- generate resource name
      m_rcname="::"+name+(string)ChartID()+(string)(GetTickCount()+MathRand());
      //--- initialize data with zeros
      ArrayInitialize(m_pixels,0);
      //--- create dynamic resource
      if(ResourceCreate(m_rcname,m_pixels,width,height,0,0,0,clrfmt))
        {
         //--- successfully created
         //--- complete initialization
         m_width =width;
         m_height=height;
         m_format=clrfmt;
         //--- succeed
         return(true);
        }
     }
//--- error - destroy object
   Destroy();
   return(false);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Создаёт объекты-формы на опорных точках базового объекта         |
//+------------------------------------------------------------------+
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);                                   // Объект перемещаемый
      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);// Рисование очерчивающего прямоугольника для визуального отображения расположения активной зоны формы
      this.DrawControlPoint(form,0,CTRL_POINT_COLOR);          // Рисуем окружность и точку в центре формы
      form.Done();                                             // Фиксируем изначальное состояние объекта-формы (его внешний вид)
     }
   //--- Перерисовываем график для отображения изменений (в случае успеха) и возвращаем итоговый результат
   if(res)
      ::ChartRedraw(this.m_base_chart_id);
   return res;
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Рисует на форме контрольную точку                                |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::DrawControlPoint(CForm *form,const uchar opacity,const color clr)
  {
   if(form==NULL)
      return;
   form.DrawCircle((int)::floor(form.Width()/2),(int)::floor(form.Height()/2),CTRL_POINT_RADIUS,clr,opacity);// Рисуем окружность в центре формы
   form.DrawCircleFill((int)::floor(form.Width()/2),(int)::floor(form.Height()/2),2,clr,opacity);            // Рисуем точку в центре формы
  }
//+------------------------------------------------------------------+

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

Из обработчика событий удалим обработку перемещения курсора мышки она нам здесь не нужна:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
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);
     }
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      for(int i=0;i<this.m_list_forms.Total();i++)
        {
         CForm *form=this.m_list_forms.At(i);
         if(form==NULL)
            continue;
         form.OnChartEvent(id,lparam,dparam,sparam);
        }
      ::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);
//--- Возвращает объект данных опорных точек
   CLinkedPivotPoint*GetLinkedPivotPoint(void)        { return &this.m_linked_pivots;  }

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

//--- Возвращает количество связанных опорных точек базового объекта для расчёта координат в (1) текущем, (2) указанном объекте
   int               GetLinkedCoordsNum(void)               const { return this.m_linked_pivots.GetNumLinkedCoords();      }
   int               GetLinkedPivotsNum(CGStdGraphObj *obj) const { return(obj!=NULL ? obj.GetLinkedCoordsNum() : 0);      }
//--- Возвращает форму управления опорной точкой объекта
   CForm            *GetControlPointForm(const int index);
//--- Возвращает количество объектов-форм управления контрольными точками
   int               GetNumControlPointForms(void);
//--- Перерисовывает форму управления контрольной точкой расширенного стандартного графического объекта
   void              RedrawControlPointForms(const uchar opacity,const color clr);

private:


Добавим метод, устанавливающий время и цену по экранным координатам:

//--- Символ для объекта "График" 
   string            ChartObjSymbol(void)          const { return this.GetProperty(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL,0);                    }
   bool              SetChartObjSymbol(const string symbol)
                       {
                        if(!::ObjectSetString(CGBaseObj::ChartID(),CGBaseObj::Name(),OBJPROP_SYMBOL,symbol))
                           return false;
                        this.SetProperty(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL,0,symbol);
                        return true;
                       }
//--- Устанавливает время и цену по экранным координатам
   bool              SetTimePrice(const int x,const int y,const int modifier)
                       {
                        bool res=true;
                        ENUM_OBJECT type=this.GraphObjectType();
                        if(type==OBJ_LABEL || type==OBJ_BUTTON || type==OBJ_BITMAP_LABEL || type==OBJ_EDIT || type==OBJ_RECTANGLE_LABEL)
                          {
                           res &=this.SetXDistance(x);
                           res &=this.SetYDistance(y);
                          }
                        else
                          {
                           int subwnd=0;
                           datetime time=0;
                           double price=0;
                           if(::ChartXYToTimePrice(this.ChartID(),x,y,subwnd,time,price))
                             {
                              res &=this.SetTime(time,modifier);
                              res &=this.SetPrice(price,modifier);
                             }
                          }
                        return res;
                       }
  
//--- Возвращает флаги видимости объекта на таймфреймах

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

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

//+------------------------------------------------------------------+
//| Возвращает форму управления опорной точкой объекта               |
//+------------------------------------------------------------------+
CForm *CGStdGraphObj::GetControlPointForm(const int index)
  {
   return(this.ExtToolkit!=NULL ? this.ExtToolkit.GetControlPointForm(index) : NULL);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Возвращает количество объектов-форм                              |
//| управления контрольными точками                                  |
//+------------------------------------------------------------------+
int CGStdGraphObj::GetNumControlPointForms(void)
  {
   return(this.ExtToolkit!=NULL ? this.ExtToolkit.GetNumControlPointForms() : 0);
  }
//+------------------------------------------------------------------+

Метод аналогичен вышерассмотренному: если объект инструментария расширенного стандартного графического объекта существует, то возвращается количество объектов-форм. Иначе — возвращается 0.

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

//+------------------------------------------------------------------+
//| Перерисовывает форму управления контрольной точкой               |
//| расширенного стандартного графического объекта                   |
//+------------------------------------------------------------------+
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++)
     {
      //--- получаем очередную объект-форму
      CForm *form=this.ExtToolkit.GetControlPointForm(i);
      if(form==NULL)
         continue;
      //--- рисуем на ней точку и окружность с указанной непрозрачностью и цветом
      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;
  }
//+------------------------------------------------------------------+

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

Из обработчика событий удалим обработку движения мышки она здесь не нужна:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CGStdGraphObj::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(GraphElementType()!=GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)
      return;
   string name=this.Name();
   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,name);
     }
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      if(ExtToolkit!=NULL)
         ExtToolkit.OnChartEvent(id,lparam,dparam,name);
     }
  }
//+------------------------------------------------------------------+


Доработаем класс-коллекцию графических объектов в файле \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh.

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

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

//--- Возвращает указатель на форму, находящуюся под курсором
   CForm            *GetFormUnderCursor(const int id, 
                                        const long &lparam, 
                                        const double &dparam, 
                                        const string &sparam,
                                        ENUM_MOUSE_FORM_STATE &mouse_state,
                                        long &obj_ext_id,
                                        int &form_index);

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

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

//--- Возвращает (1) существующий, (2) удалённый графический объект по имени и идентификатору графика
   CGStdGraphObj    *GetStdGraphObject(const string name,const long chart_id);
   CGStdGraphObj    *GetStdDelGraphObject(const string name,const long chart_id);
//--- Возвращает существующий (1) расширенный, (2) стандартный графический объект по его идентификатору
   CGStdGraphObj    *GetStdGraphObjectExt(const long id,const long chart_id);
   CGStdGraphObj    *GetStdGraphObject(const long id,const long chart_id);
//--- Возвращает список (1) объектов управления графиками, (2) удалённых графических объектов
   CArrayObj        *GetListChartsControl(void)                                                          { return &this.m_list_charts_control;  }
   CArrayObj        *GetListDeletedObj(void)                                                             { return &this.m_list_deleted_obj;     }

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

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

//+------------------------------------------------------------------+
//| Возвращает существующий расширенный стандартный                  |
//| графический объект по его идентификатору                         |
//+------------------------------------------------------------------+
CGStdGraphObj *CGraphElementsCollection::GetStdGraphObjectExt(const long id,const long chart_id)
  {
   CArrayObj *list=this.GetListStdGraphObjectExt();
   list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_CHART_ID,0,chart_id,EQUAL);
   list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_ID,0,id,EQUAL);
   return(list!=NULL && list.Total()>0 ? list.At(0) : NULL);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Возвращает существующий стандартный                              |
//| графический объект по его идентификатору                         |
//+------------------------------------------------------------------+
CGStdGraphObj *CGraphElementsCollection::GetStdGraphObject(const long id,const long chart_id)
  {
   CArrayObj *list=this.GetList(GRAPH_OBJ_PROP_CHART_ID,0,chart_id);
   list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_ID,0,id,EQUAL);
   return(list!=NULL && list.Total()>0 ? list.At(0) : NULL);
  }
//+------------------------------------------------------------------+

Здесь: получаем список графических объектов по идентификатору графика. В полученном списке оставляем объект с указанным идентификатором объекта.
Если полученный список валидный и не пустой — возвращаем содержащийся в нём указатель на искомый объект
. Иначе — возвращаем NULL.

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

//+------------------------------------------------------------------+
//| Возвращает указатель на форму, находящуюся под курсором          |
//+------------------------------------------------------------------+
CForm *CGraphElementsCollection::GetFormUnderCursor(const int id, 
                                                    const long &lparam, 
                                                    const double &dparam, 
                                                    const string &sparam,
                                                    ENUM_MOUSE_FORM_STATE &mouse_state,
                                                    long &obj_ext_id,
                                                    int &form_index)
  {
//--- Устанавливаем идентификатор расширенного стандартного графического объекта в -1 
//--- и индекс точки привязки, которой управляет форма, в -1
   obj_ext_id=WRONG_VALUE;
   form_index=WRONG_VALUE;
//--- Инициализируем состояние мышки относительно формы
   mouse_state=MOUSE_FORM_STATE_NONE;
//--- Объявим указатели на объекты класса-коллекции графических элементов
   CGCnvElement *elm=NULL;
   CForm *form=NULL;
//--- Получим список объектов, для которых установлен флаг взаимодействия (должен быть всего один объект)
   CArrayObj *list=CSelect::ByGraphCanvElementProperty(GetListCanvElm(),CANV_ELEMENT_PROP_INTERACTION,true,EQUAL);
//--- Если список получить удалось и он не пустой
   if(list!=NULL && list.Total()>0)
     {
      //--- Получаем единственный в нём графический элемент
      elm=list.At(0);
      //--- Если этот элемент - объект-форма
      if(elm.TypeGraphElement()==GRAPH_ELEMENT_TYPE_FORM)
        {
         //--- Присваиваем указатель на элемент указателю на объект-форму
         form=elm;
         //--- Получаем состояние мышки относительно формы
         mouse_state=form.MouseFormState(id,lparam,dparam,sparam);
         //--- Если курсор в пределах формы - возвращаем указатель на форму
         if(mouse_state>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL)
            return form;
        }
     }
//--- Если нет ни одного объекта-формы с установленным флагом взаимодействия -
//--- в цикле по всем объектам класса-коллекции графических элементов
   int total=this.m_list_all_canv_elm_obj.Total();
   for(int i=0;i<total;i++)
     {
      //--- получаем очередной элемент
      elm=this.m_list_all_canv_elm_obj.At(i);
      if(elm==NULL)
         continue;
      //--- если полученный элемент - объект-форма
      if(elm.TypeGraphElement()==GRAPH_ELEMENT_TYPE_FORM)
        {
         //--- Присваиваем указатель на элемент указателю на объект-форму
         form=elm;
         //--- Получаем состояние мышки относительно формы
         mouse_state=form.MouseFormState(id,lparam,dparam,sparam);
         //--- Если курсор в пределах формы - возвращаем указатель на форму
         if(mouse_state>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL)
            return form;
        }
     }
//--- Если нет ни одного объекта-формы из списка-коллекции -
//--- Получаем список расширенных стандартных графических объектов
   list=this.GetListStdGraphObjectExt();
   if(list!=NULL)
     {
      //--- в цикле по всем расширенным стандартным графическим объектам
      for(int i=0;i<list.Total();i++)
        {
         //--- получаем очередной графический объект,
         CGStdGraphObj *obj_ext=list.At(i);
         if(obj_ext==NULL)
            continue;
         //--- получаем объект его инструментария,
         CGStdGraphObjExtToolkit *toolkit=obj_ext.GetExtToolkit();
         if(toolkit==NULL)
            continue;
         //--- обрабатываем событие изменения графика для текущего графического объекта
         obj_ext.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
         //--- Получаем общее количество объектов-форм, созданных для текущего графического объекта
         total=toolkit.GetNumControlPointForms();
         //--- В цикле по всем объектам-формам
         for(int j=0;j<total;j++)
           {
            //--- получаем очередной объект-форму,
            form=toolkit.GetControlPointForm(j);
            if(form==NULL)
               continue;
            //--- получаем состояние мышки относительно формы
            mouse_state=form.MouseFormState(id,lparam,dparam,sparam);
            //--- Если курсор в пределах формы,
            if(mouse_state>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL)
              {
               //--- записываем идентификатор объекта и индекс формы
               //--- и возвращаем указатель на форму
               obj_ext_id=obj_ext.ObjectID();
               form_index=j;
               return form;
              }
           }
        }
     }
//--- Ничего не нашли - возвращаем NULL
   return NULL;
  }
//+------------------------------------------------------------------+

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

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

Рассмотрим доработки и изменения в обработчике событий:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
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(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);
                          }
                       }
                    }
                 }
              }
            //--- Смещаем форму на полученные координаты
            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.DrawControlPoint(form,255,CTRL_POINT_COLOR);
                       }
                    }
                 }
              }
            //--- Обработчик события Курсор в пределах формы, нажата кнопка мышки (любая)
            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.DrawControlPoint(form,255,CTRL_POINT_COLOR);
                       }
                    }
                 }
              }
            //--- Обработчик события Курсор в пределах активной области, нажата кнопка мышки (любая)
            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\Part98\ под новым именем TestDoEasyPart98.mq5.

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

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart98.mq5 |
//|                                  Copyright 2021, MetaQuotes Ltd. |
//|                             https://mql5.com/ru/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2021, MetaQuotes Ltd."
#property link      "https://mql5.com/ru/users/artmedia70"
#property version   "1.00"
//--- includes
#include <DoEasy\Engine.mqh>
//--- defines
#define        FORMS_TOTAL (3)   // Количество создаваемых форм
#define        START_X     (4)   // Начальная координата X фигуры
#define        START_Y     (4)   // Начальная координата Y фигуры
#define KEY_LEFT           (188) // Влево
#define KEY_RIGHT          (190) // Вправо
#define KEY_ORIGIN         (191) // Изначальные свойства
//--- input parameters
sinput   bool              InpMovable     =  true;          // Movable forms flag
sinput   ENUM_INPUT_YES_NO InpUseColorBG  =  INPUT_YES;     // Use chart background color to calculate shadow color
sinput   color             InpColorForm3  =  clrCadetBlue;  // Third form shadow color (if not background color) 
//--- global variables
CEngine        engine;
color          array_clr[];
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Установка глобальных переменных советника
   ArrayResize(array_clr,2);        // Массив цветов градиентной заливки
   array_clr[0]=C'26,100,128';      // Исходный ≈Тёмно-лазурный цвет
   array_clr[1]=C'35,133,169';      // Осветлённый исходный цвет
//--- Создадим массив с текущим символом и установим его для использования в библиотеке
   string array[1]={Symbol()};
   engine.SetUsedSymbols(array);
   //--- Создадим объект-таймсерию для текущего символа и периода и выведем его описание в журнал
   engine.SeriesCreate(Symbol(),Period());
   engine.GetTimeSeriesCollection().PrintShort(false); // Краткие описания
//--- Создадим объекты-формы
   CForm *form=NULL;
   for(int i=0;i<FORMS_TOTAL;i++)
     {
      //--- При создании объекта передаём в него все требуемые параметры
      form=new CForm("Form_0"+string(i+1),30,(form==NULL ? 100 : form.BottomEdge()+20),100,30);
      if(form==NULL)
         continue;
      //--- Установим форме флаги активности, и перемещаемости
      form.SetActive(true);
      form.SetMovable(true);
      //--- Установим форме её идентификатор и номер в списке объектов
      form.SetID(i);
      form.SetNumber(0);   // (0 - означает главный объект-форма) К главному объекту могут прикрепляться второстепенные, которыми он будет управлять
      //--- Установим непрозрачность 200
      form.SetOpacity(245);
      //--- Цвет фона формы зададим как первый цвет из массива цветов
      form.SetColorBackground(array_clr[0]);
      //--- Цвет очерчивающей рамки формы
      form.SetColorFrame(clrDarkBlue);
      //--- Установим флаг рисования тени
      form.SetShadow(false);
      //--- Рассчитаем цвет тени как цвет фона графика, преобразованный в монохромный
      color clrS=form.ChangeColorSaturation(form.ColorBackground(),-100);
      //--- Если в настройках задано использовать цвет фона графика, то затемним монохромный цвет на 20 единиц
      //--- Иначе - будем использовать для рисования тени заданный в настройках цвет
      color clr=(InpUseColorBG ? form.ChangeColorLightness(clrS,-20) : InpColorForm3);
      //--- Нарисуем тень формы со смещением от формы вправо-вниз на три пикселя по всем осям
      //--- Непрозрачность тени при этом установим равной 200, а радиус размытия равный 4
      form.DrawShadow(3,3,clr,200,4);
      //--- Зальём фон формы вертикальным градиентом
      form.Erase(array_clr,form.Opacity(),true);
      //--- Нарисуем очерчивающий прямоугольник по краям формы
      form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,form.ColorFrame(),form.Opacity());
      form.Done();
      
      //--- Выведем текст с описанием типа градиента и обновим форму
      //--- Параметры текста: координаты текста в центре формы и точка привязки - тоже по центру
      //--- Создаём новый кадр текстовой анимации с идентификатором 0 и выводим текст на форму
      form.TextOnBG(0,TextByLanguage("Тест 0","Test 0")+string(i+1),form.Width()/2,form.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',255,true,true);
      //--- Добавим форму в список
      if(!engine.GraphAddCanvElmToCollection(form))
         delete form;
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Также в обработчике OnChartEvent() укоротим длину имени создаваемых по клику мышки графических объектов (ранее имя было "TrendLineExt") — чтобы не выйти за пределы в 63 символа при создании графического ресурса:

   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="TLineExt";
         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);
        }
     }

//--- Обработки событий коллекции графических элементов

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


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


Что дальше

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

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

К содержанию

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

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

Прикрепленные файлы |
MQL5.zip (4298.5 KB)
Математика в трейдинге: Коэффициенты Шарпа и Сортино Математика в трейдинге: Коэффициенты Шарпа и Сортино
Доходность является самым очевидным показателем, который используют инвесторы и начинающие трейдеры для анализа эффективности торговли. Профессиональные трейдеры пользуются более надежными инструментами для анализа стратегии, среди них — коэффициенты Шарпа и Сортино.
Советы профессионального программиста (Часть III): Логирование. Подключение к системе сбора и анализа логов Seq Советы профессионального программиста (Часть III): Логирование. Подключение к системе сбора и анализа логов Seq
Реализация класса Logger для унификации (структурирования) сообщений, выводимых в журнал эксперта. Подключение к системе сбора и анализа логов Seq. Наблюдение за сообщениями в онлайн режиме.
Что можно сделать с помощью скользящих средних Что можно сделать с помощью скользящих средних
В данной статье мне захотелось собрать некоторые способы применения индикатора "Скользящая средняя". Практически к каждому способу, если требуется анализ кривых, сделаны индикаторы, визуализирующие полезную идею. В большинстве случаев идеи подсмотрены у других авторов, однако, собранные все вместе, они помогут точнее видеть основные направления и — надеюсь — принимать более правильные торговые решения. Уровень знания языка MQL5 — начальный.
Уроки по DirectX (Часть I): Рисуем первый треугольник Уроки по DirectX (Часть I): Рисуем первый треугольник
Это вводная статья по DirectX, которая описывает особенности работы с API. Помогает разобраться с порядком инициализации его компонентов. Приводит пример написания скрипта на MQL, выводящего треугольник с помощью DirectX.