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

Artyom Trishkin | 28 января, 2022

Содержание


Концепция

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


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

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

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

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


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

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

//--- CGraphElementsCollection
   MSG_GRAPH_OBJ_FAILED_GET_ADDED_OBJ_LIST,           // Не удалось получить список вновь добавленных объектов
   MSG_GRAPH_OBJ_FAILED_DETACH_OBJ_FROM_LIST,         // Не удалось изъять графический объект из списка
   MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_LIST,         // Не удалось удалить графический объект из списка
   MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART,        // Не удалось удалить графический объект с графика
   MSG_GRAPH_OBJ_FAILED_ADD_OBJ_TO_DEL_LIST,          // Не удалось поместить графический объект в список удалённых объектов
   MSG_GRAPH_OBJ_FAILED_ADD_OBJ_TO_RNM_LIST,          // Не удалось поместить графический объект в список переименованных объектов

...

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

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

//--- CGraphElementsCollection
   {"Не удалось получить список вновь добавленных объектов","Failed to get the list of newly added objects"},
   {"Не удалось изъять графический объект из списка","Failed to detach graphic object from the list"},
   {"Не удалось удалить графический объект из списка","Failed to delete graphic object from the list"},
   {"Не удалось удалить графический объект с графика","Failed to delete graphic object from the chart"},
   {"Не удалось поместить графический объект в список удалённых объектов","Failed to place graphic object in the list of deleted objects"},
   {"Не удалось поместить графический объект в список переименованных объектов","Failed to place graphic object in the list of renamed objects"},

...

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


Во всех файлах классов-наследников объекта абстрактного стандартного графического объекта, хранящихся в папке
\MQL5\Include\DoEasy\Objects\Graph\Standard\, в их методах для вывода краткого описания объекта сделаем небольшую доработку:

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

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

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

В классе данных опорной точки зависимого объекта, находящемся в этом же файле, массив был объявлен с именем, включающим в себя указание координаты: m_property_x[][2] Это осталось после экспериментов с двумя массивами в одном классе — для координат X и Y. В дальнейшем от этой затеи я отказался, а наименование массива осталось некорректным. Поэтому он был переименован в m_property[][2].

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

//+------------------------------------------------------------------+
//| Класс данных опорной точки зависимого объекта                    |
//+------------------------------------------------------------------+
class CPivotPointData
  {
private:
   bool              m_axis_x;
   int               m_property[][2];
public:
//--- (1) Устанавливает (2) возвращает флаг, что опорная точка принадлежит координате X
   void              SetAxisX(const bool axis_x)         { this.m_axis_x=axis_x;             }
   bool              IsAxisX(void)                 const { return this.m_axis_x;             }
   string            AxisDescription(void)         const { return(this.m_axis_x ? "X" : "Y");}
//--- Возвращает количество опорных точек базового объекта для расчёта координаты подчинённого
   int               GetBasePivotsNum(void)  const { return ::ArrayRange(this.m_property,0);  }
//--- Добавляет новую опорную точку базового объекта для расчёта координаты подчинённого
   bool              AddNewBasePivotPoint(const string source,const int pivot_prop,const int pivot_num)
                       {
                        //--- Получаем размер массива и 
                        int pivot_index=this.GetBasePivotsNum();
                        //--- если размер массива увеличить не удалось - сообщаем об этом и возвращаем false
                        if(::ArrayResize(this.m_property,pivot_index+1)!=pivot_index+1)
                          {
                           CMessage::ToLog(source,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
                           return false;
                          }
                        //--- Возвращаем результат изменения значений вновь добавленного нового измерения массива
                        return this.ChangeBasePivotPoint(source,pivot_index,pivot_prop,pivot_num);
                       }
//--- Изменяет указанную опорную точку базового объекта для расчёта координаты подчинённого
   bool              ChangeBasePivotPoint(const string source,const int pivot_index,const int pivot_prop,const int pivot_num)
                       {
                        //--- Получаем размер массива и если он нулевой - сообщаем об этом и возвращаем false
                        int n=this.GetBasePivotsNum();
                        if(n==0)
                          {
                           CMessage::ToLog(source,(this.IsAxisX() ? MSG_GRAPH_OBJ_EXT_NOT_ANY_PIVOTS_X : MSG_GRAPH_OBJ_EXT_NOT_ANY_PIVOTS_Y));
                           return false;
                          }
                        //--- Если указанный индекс выхоит за пределы массива - сообщаем об этом и возвращаем false
                        if(pivot_index<0 || pivot_index>n-1)
                          {
                           CMessage::ToLog(source,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY);
                           return false;
                          }
                        //--- Устанавливаем переданные в метод значения в указанные ячейки массива по индексу
                        this.m_property[pivot_index][0]=pivot_prop;
                        this.m_property[pivot_index][1]=pivot_num;
                        return true;
                       }

//--- Возвращает (1) свойство, (2) модификатор свойства из массива
   int               GetProperty(const string source,const int index)     const
                       {
                        if(index<0 || index>this.GetBasePivotsNum()-1)
                          {
                           CMessage::ToLog(source,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY);
                           return WRONG_VALUE;
                          }
                        return this.m_property[index][0];   
                       }
   int               GetPropertyModifier(const string source,const int index)  const
                       {
                        if(index<0 || index>this.GetBasePivotsNum()-1)
                          {
                           CMessage::ToLog(source,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY);
                           return WRONG_VALUE;
                          }
                        return this.m_property[index][1];   
                       }

//--- Возвращает описание количества опорных точек для установки координаты
   string            GetBasePivotsNumDescription(void) const
                       {
                        return CMessage::Text(IsAxisX() ? MSG_GRAPH_OBJ_EXT_NUM_BASE_PP_TO_SET_X : MSG_GRAPH_OBJ_EXT_NUM_BASE_PP_TO_SET_Y)+
                               (string)this.GetBasePivotsNum();
                       }

//--- Конструктор/Деструктор
                     CPivotPointData(void){;}
                    ~CPivotPointData(void){;}
  };
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Класс данных опорных точек X и Y составного объекта              |
//+------------------------------------------------------------------+
class CPivotPointXY : public CObject
  {
private:
   CPivotPointData   m_pivot_point_x;            // Опорная точка X-координаты
   CPivotPointData   m_pivot_point_y;            // Опорная точка Y-координаты
public:
//--- Возвращает указатель на объект данных опорной точки (1) X-координаты, (2) Y-координаты
   CPivotPointData  *GetPivotPointDataX(void)      { return &this.m_pivot_point_x;                    }
   CPivotPointData  *GetPivotPointDataY(void)      { return &this.m_pivot_point_y;                    }
//--- Возвращает количество опорных точек базового объекта для расчёта координаты (1) X, (2) Y
   int               GetBasePivotsNumX(void) const { return this.m_pivot_point_x.GetBasePivotsNum();  }
   int               GetBasePivotsNumY(void) const { return this.m_pivot_point_y.GetBasePivotsNum();  }
//--- Добавляет новую опорную точку базового объекта для расчёта координаты X подчинённого
   bool              AddNewBasePivotPointX(const int pivot_prop,const int pivot_num)
                       {
                        return this.m_pivot_point_x.AddNewBasePivotPoint(DFUN,pivot_prop,pivot_num);
                       }
//--- Добавляет новую опорную точку базового объекта для расчёта координаты Y подчинённого
   bool              AddNewBasePivotPointY(const int pivot_prop,const int pivot_num)
                       {
                        return this.m_pivot_point_y.AddNewBasePivotPoint(DFUN,pivot_prop,pivot_num);
                       }
//--- Добавляет новые опорные точки базового объекта для расчёта координат X и Y подчинённого
   bool              AddNewBasePivotPointXY(const int pivot_prop_x,const int pivot_num_x,
                                            const int pivot_prop_y,const int pivot_num_y)
                       {
                        bool res=true;
                        res &=this.m_pivot_point_x.AddNewBasePivotPoint(DFUN,pivot_prop_x,pivot_num_x);
                        res &=this.m_pivot_point_y.AddNewBasePivotPoint(DFUN,pivot_prop_y,pivot_num_y);
                        return res;
                       }
//--- Изменяет указанную опорную точку базового объекта для расчёта координаты X подчинённого
   bool              ChangeBasePivotPointX(const int pivot_index,const int pivot_prop,const int pivot_num)
                       {
                        return this.m_pivot_point_x.ChangeBasePivotPoint(DFUN,pivot_index,pivot_prop,pivot_num);
                       }
//--- Изменяет указанную опорную точку базового объекта для расчёта координаты Y подчинённого
   bool              ChangeBasePivotPointY(const int pivot_index,const int pivot_prop,const int pivot_num)
                       {
                        return this.m_pivot_point_y.ChangeBasePivotPoint(DFUN,pivot_index,pivot_prop,pivot_num);
                       }
//--- Изменяет указанные опорные точки базового объекта для расчёта координат X и Y
   bool              ChangeBasePivotPointXY(const int pivot_index,
                                            const int pivot_prop_x,const int pivot_num_x,
                                            const int pivot_prop_y,const int pivot_num_y)
                       {
                        bool res=true;
                        res &=this.m_pivot_point_x.ChangeBasePivotPoint(DFUN,pivot_index,pivot_prop_x,pivot_num_x);
                        res &=this.m_pivot_point_y.ChangeBasePivotPoint(DFUN,pivot_index,pivot_prop_y,pivot_num_y);
                        return res;
                       }
//--- Возвращает (1) свойство для расчёта X-координаты, (2) модификатор свойства X-координаты
   int               GetPropertyX(const string source,const int index) const
                       {
                        return this.m_pivot_point_x.GetProperty(source,index);
                       }
   int               GetPropertyModifierX(const string source,const int index) const
                       {
                       return this.m_pivot_point_x.GetPropertyModifier(source,index);
                       }
//--- Возвращает (1) свойство для расчёта Y-координаты, (2) модификатор свойства Y-координаты
   int               GetPropertyY(const string source,const int index) const
                       {
                        return this.m_pivot_point_y.GetProperty(source,index);
                       }
   int               GetPropertyModifierY(const string source,const int index) const
                       {
                       return this.m_pivot_point_y.GetPropertyModifier(source,index);
                       }
//--- Возвращает описание количества опорных точек для установки координаты (1) X, (2) Y
   string            GetBasePivotsNumXDescription(void) const
                       {
                        return this.m_pivot_point_x.GetBasePivotsNumDescription();
                       }
   string            GetBasePivotsNumYDescription(void) const
                       {
                        return this.m_pivot_point_y.GetBasePivotsNumDescription();
                       }
                       
//--- Конструктор/Деструктор
                     CPivotPointXY(void){ this.m_pivot_point_x.SetAxisX(true); this.m_pivot_point_y.SetAxisX(false); }
                    ~CPivotPointXY(void){;}
  };  
//+------------------------------------------------------------------+

Каждый из этих методов возвращает результат вызова одноимённого метода соответствующего класса, хранящего данные о координатах по оси X или Y.
В методах, в их наименованиях, добавлено указание на данные какой именно координаты возвращает метод, например GetPropertyX или GetPropertyY.

Класс связанных данных опорных точек составного объекта претерпел достаточно большую доработку в основном по части наименований методов. Просто при отладке я начал путаться в наименованиях методов, которые были не совсем однозначны. Поэтому переименовал их для большей наглядности. Например, наименование метода CreateNewLinkedPivotPoint(), который добавляет новую точку привязки зависимого объекта по координатам X и Y, сбивало с толку, так как PivotPoint — точка привязки, используется для задания координаты X или Y базового объекта для расчёта координаты, к которой будет прикрепляться зависимый объект. А сама координатная точка может рассчитываться из нескольких PivotPoint. Поэтому метод был переименован в CreateNewLinkedCoord(), что прямо говорит о добавлении новой точки координат.

Для сокращения кода методов были использованы тернарные операторы. Например метод

   CPivotPointData  *GetBasePivotPointDataX(const int index) const
                       {
                        CPivotPointXY *obj=this.GetLinkedPivotPointXY(index);
                        if(obj==NULL)
                           return NULL;
                        return obj.GetPivotPointDataX();
                       }

теперь выглядит так:

   CPivotPointData  *GetBasePivotPointDataX(const int index_coord_point) const
                       {
                        CPivotPointXY *obj=this.GetLinkedCoord(index_coord_point);
                        return(obj!=NULL ? obj.GetPivotPointDataX() : NULL);
                       }

что абсолютно одно и то же, но короче.

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

//--- Добавляет новую опорную точку базового объекта для расчёта координаты X для указанной точки привязки подчинённого
   bool              AddNewBasePivotPointX(const int index_coord_point,const int pivot_prop,const int pivot_num)
                       {
                        CPivotPointData *obj=this.GetBasePivotPointDataX(index_coord_point);
                        return(obj!=NULL ? obj.AddNewBasePivotPoint(DFUN,pivot_prop,pivot_num) : false);
                       }
//--- Добавляет новую опорную точку базового объекта для расчёта координаты Y для указанной точки привязки подчинённого
   bool              AddNewBasePivotPointY(const int index_coord_point,const int pivot_prop,const int pivot_num)
                       {
                        CPivotPointData *obj=this.GetBasePivotPointDataY(index_coord_point);
                        return(obj!=NULL ? obj.AddNewBasePivotPoint(DFUN,pivot_prop,pivot_num) : false);
                       }
//--- Добавляет новые опорные точки базового объекта для расчёта координат X и Y для указанной точки привязки подчинённого
   bool              AddNewBasePivotPointXY(const int index_coord_point,
                                            const int pivot_prop_x,const int pivot_num_x,
                                            const int pivot_prop_y,const int pivot_num_y)
                       {
                        CPivotPointData *objx=this.GetBasePivotPointDataX(index_coord_point);
                        if(objx==NULL)
                           return false;
                        CPivotPointData *objy=this.GetBasePivotPointDataY(index_coord_point);
                        if(objy==NULL)
                           return false;
                        bool res=true;
                        res &=objx.AddNewBasePivotPoint(DFUN,pivot_prop_x,pivot_num_x);
                        res &=objy.AddNewBasePivotPoint(DFUN,pivot_prop_y,pivot_num_y);
                        return res;
                       }

//--- Изменяет указанную опорную точку базового объекта для расчёта координаты X для указанной точки привязки подчинённого
   bool              ChangeBasePivotPointX(const int index_coord_point,const int pivot_index,const int pivot_prop,const int pivot_num)
                       {
                        CPivotPointData *obj=this.GetBasePivotPointDataX(index_coord_point);
                        return(obj!=NULL ? obj.ChangeBasePivotPoint(DFUN,pivot_index,pivot_prop,pivot_num) : false);
                       }
//--- Изменяет указанную опорную точку базового объекта для расчёта координаты Y для указанной точки привязки подчинённого
   bool              ChangeBasePivotPointY(const int index_coord_point,const int pivot_index,const int pivot_prop,const int pivot_num)
                       {
                        CPivotPointData *obj=this.GetBasePivotPointDataY(index_coord_point);
                        return(obj!=NULL ? obj.ChangeBasePivotPoint(DFUN,pivot_index,pivot_prop,pivot_num) : false);
                       }
//--- Изменяет указанные опорные точки базового объекта для расчёта координат X и Y указанной точки привязки
   bool              ChangeBasePivotPointXY(const int index_coord_point,
                                            const int pivot_index,
                                            const int pivot_prop_x,const int pivot_num_x,
                                            const int pivot_prop_y,const int pivot_num_y)
                       {
                        CPivotPointData *objx=this.GetBasePivotPointDataX(index_coord_point);
                        if(objx==NULL)
                           return false;
                        CPivotPointData *objy=this.GetBasePivotPointDataY(index_coord_point);
                        if(objy==NULL)
                           return false;
                        bool res=true;
                        res &=objx.ChangeBasePivotPoint(DFUN,pivot_index,pivot_prop_x,pivot_num_x);
                        res &=objy.ChangeBasePivotPoint(DFUN,pivot_index,pivot_prop_y,pivot_num_y);
                        return res;
                       }

//--- Возвращает свойство для расчёта X-координаты для указанной точки привязки
   int               GetPropertyX(const int index_coord_point,const int index) const
                       {
                        CPivotPointData *obj=this.GetBasePivotPointDataX(index_coord_point);
                        return(obj!=NULL ? obj.GetProperty(DFUN,index) : WRONG_VALUE);
                       }
//--- Возвращает модификатор свойства X-координаты для указанной точки привязки
   int               GetPropertyModifierX(const int index_coord_point,const int index) const
                       {
                        CPivotPointData *obj=this.GetBasePivotPointDataX(index_coord_point);
                        return(obj!=NULL ? obj.GetPropertyModifier(DFUN,index) : WRONG_VALUE);
                       }

//--- Возвращает свойство для расчёта Y-координаты для указанной точки привязки
   int               GetPropertyY(const int index_coord_point,const int index) const
                       {
                        CPivotPointData *obj=this.GetBasePivotPointDataY(index_coord_point);
                        return(obj!=NULL ? obj.GetProperty(DFUN,index) : WRONG_VALUE);
                       }
//--- Возвращает модификатор свойства Y-координаты для указанной точки привязки
   int               GetPropertyModifierY(const int index_coord_point,const int index) const
                       {
                        CPivotPointData *obj=this.GetBasePivotPointDataY(index_coord_point);
                       return(obj!=NULL ? obj.GetPropertyModifier(DFUN,index) : WRONG_VALUE);
                       }

//--- Возвращает описание количества опорных точек базового объекта для расчёта координаты X по индексу
   string            GetBasePivotsNumXDescription(const int index_coord_point) const
                       {
                        CPivotPointData *obj=this.GetBasePivotPointDataX(index_coord_point);
                        return(obj!=NULL ? obj.GetBasePivotsNumDescription() : "WRONG_VALUE");
                       }
//--- Возвращает описание количества опорных точек базового объекта для расчёта координаты Y по индексу
   string            GetBasePivotsNumYDescription(const int index_coord_point) const
                       {
                        CPivotPointData *obj=this.GetBasePivotPointDataY(index_coord_point);
                        return(obj!=NULL ? obj.GetBasePivotsNumDescription() : "WRONG_VALUE");
                       }

//--- Конструктор/Деструктор
                     CLinkedPivotPoint(void){;}
                    ~CLinkedPivotPoint(void){;}
  };  
//+------------------------------------------------------------------+


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

//--- Возвращает флаг поддержания объектом данного свойства
   virtual bool      SupportProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property) { return true;             }
   virtual bool      SupportProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property)  { return true;             }
   virtual bool      SupportProperty(ENUM_GRAPH_OBJ_PROP_STRING property)  { return true;             }

//--- Возвращает описание (1) целочисленного, (2) вещественного и (3) строкового свойства
   string            GetPropertyDescription(ENUM_GRAPH_OBJ_PROP_INTEGER property,const int index=0);
   string            GetPropertyDescription(ENUM_GRAPH_OBJ_PROP_DOUBLE property,const int index=0);
   string            GetPropertyDescription(ENUM_GRAPH_OBJ_PROP_STRING property,const int index=0);

//--- Возвращает описание положения точки привязки графического объекта
   virtual string    AnchorDescription(void)                const { return (string)this.GetProperty(GRAPH_OBJ_PROP_ANCHOR,0); }

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

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

OnChartEvent: Координата времени: 
 - Опорная точка 0: 2022.01.24 20:59
 - Опорная точка 1: 2022.01.26 22:00

...

OnChartEvent: Координата цены: 
 - Опорная точка 0: 1.13284
 - Опорная точка 1: 1.11846

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

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

//--- Возвращает (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);

//--- Возвращает объект данных опорных точек
   CLinkedPivotPoint*GetLinkedPivotPoint(void)        { return &this.m_linked_pivots;  }
   
//--- Добавляет новую опорную точку для расчёта координат X и Y в текущий объект
   bool              AddNewLinkedCoord(const int pivot_prop_x,const int pivot_num_x,const int pivot_prop_y,const int pivot_num_y)
                       {
                        //--- Если текущий объект не привязан к базовому - выводим об этом сообщение и возвращаем false
                        if(this.BaseObjectID()==0)
                          {
                           CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_NOT_ATACHED_TO_BASE);
                           return false;
                          }
                        //--- Возвращаем результат добавления новой связанной опорной точки из класса CLinkedPivotPoint в текущий объект
                        return this.m_linked_pivots.CreateNewLinkedCoord(pivot_prop_x,pivot_num_x,pivot_prop_y,pivot_num_y);
                       }
//--- Добавляет новую опорную точку для расчёта координат X и Y в указанный объект
   bool              AddNewLinkedCoord(CGStdGraphObj *obj,const int pivot_prop_x,const int pivot_num_x,const int pivot_prop_y,const int pivot_num_y)
                       {
                        //--- Если текущий объект не является расширенным - выводим об этом сообщение и возвращаем false
                        if(this.TypeGraphElement()!=GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)
                          {
                           CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_NOT_EXT_OBJ);
                           return false;
                          }
                        //--- Если передан нулевой указатель на объект - возвращаем false
                        if(obj==NULL)
                           return false;
                        //--- Возвращаем результат добавления новой связанной опорной точки из класса CLinkedPivotPoint в указанный объект
                        return obj.AddNewLinkedCoord(pivot_prop_x,pivot_num_x,pivot_prop_y,pivot_num_y);
                       }


Переименуем методы GetLinkedPivotsNum() и объявим новые приватные методы для установки координат в подчинённые графические объекты:

//--- Возвращает количество опорных точек базового объекта для расчёта координаты (1) X, (2) Y в текущем объекте
   int               GetBasePivotsNumX(const int index)           { return this.m_linked_pivots.GetBasePivotsNumX(index);  }
   int               GetBasePivotsNumY(const int index)           { return this.m_linked_pivots.GetBasePivotsNumY(index);  }
//--- Возвращает количество опорных точек базового объекта для расчёта координаты (1) X, (2) Y в указанном объекте
   int               GetBasePivotsNumX(CGStdGraphObj *obj,const int index) const { return(obj!=NULL ? obj.GetBasePivotsNumX(index): 0); }
   int               GetBasePivotsNumY(CGStdGraphObj *obj,const int index) const { return(obj!=NULL ? obj.GetBasePivotsNumY(index): 0); }
//--- Возвращает количество связанных опорных точек базового объекта для расчёта координат в (1) текущем, (2) объекте
   int               GetLinkedCoordsNum(void)               const { return this.m_linked_pivots.GetNumLinkedCoords();      }
   int               GetLinkedPivotsNum(CGStdGraphObj *obj) const { return(obj!=NULL ? obj.GetLinkedCoordsNum() : 0);      }
   
private:
//--- Устанавливает координату X (1) из указанного свойства базового объекта в указанный подчинённый объект, (2) из базового объекта
   void              SetCoordXToDependentObj(CGStdGraphObj *obj,const int prop_from,const int modifier_from,const int modifier_to);
   void              SetCoordXFromBaseObj(const int prop_from,const int modifier_from,const int modifier_to);
//--- Устанавливает координату Y (1) из указанного свойства базового объекта в указанный подчинённый объект, (2) из базового объекта
   void              SetCoordYToDependentObj(CGStdGraphObj *obj,const int prop_from,const int modifier_from,const int modifier_to);
   void              SetCoordYFromBaseObj(const int prop_from,const int modifier_from,const int modifier_to);
//--- Устанавливает в указанный подчинённый объект (1) целочисленное, (2) вещественное, (3) строковое свойство
   void              SetDependentINT(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_INTEGER prop,const long value,const int modifier);
   void              SetDependentDBL(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_DOUBLE prop,const double value,const int modifier);
   void              SetDependentSTR(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_STRING prop,const string value,const int modifier);

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


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

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

Флаг выбранности объекта установим в false — чтобы вновь добавленный объект не был выбран, и сразу же запретим доступность объекта, установив соответствующий флаг тоже в false. Затем зададим объекту тип "расширенный стандартный графический объект". Таким образом мы не сможем выбирать мышкой на графике эти объекты, и они будут доступны в списке расширенных стандартных графических объектов — чтобы можно было программно их выбрать по типу и наименованию базового графического объекта.

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

//+------------------------------------------------------------------+
//|Устанавливает координату X из указанного свойства базового объекта|
//| в указанный подчинённый объект                                   |
//+------------------------------------------------------------------+
void CGStdGraphObj::SetCoordXToDependentObj(CGStdGraphObj *obj,const int prop_from,const int modifier_from,const int modifier_to)
  {
   int prop=WRONG_VALUE;
   switch(obj.TypeGraphObject())
     {
      case OBJ_LABEL             :
      case OBJ_BUTTON            :
      case OBJ_BITMAP_LABEL      :
      case OBJ_EDIT              :
      case OBJ_RECTANGLE_LABEL   :
      case OBJ_CHART             :
        prop=GRAPH_OBJ_PROP_XDISTANCE; 
        break;
      default:
        prop=GRAPH_OBJ_PROP_TIME;
        break;
     }
   if(prop_from<GRAPH_OBJ_PROP_INTEGER_TOTAL)
     {
      this.SetDependentINT(obj,(ENUM_GRAPH_OBJ_PROP_INTEGER)prop,this.GetProperty((ENUM_GRAPH_OBJ_PROP_INTEGER)prop_from,modifier_from),modifier_to);
     }
   else if(prop_from<GRAPH_OBJ_PROP_INTEGER_TOTAL+GRAPH_OBJ_PROP_DOUBLE_TOTAL)
     {
      //--- Присваивать целочисленному значению координаты X значение вещественного свойства - плохая затея, только если знаешь что делаешь
      this.SetDependentINT(obj,(ENUM_GRAPH_OBJ_PROP_INTEGER)prop,(long)this.GetProperty((ENUM_GRAPH_OBJ_PROP_DOUBLE)prop_from,modifier_from),modifier_to);
     }
   else if(prop_from<GRAPH_OBJ_PROP_INTEGER_TOTAL+GRAPH_OBJ_PROP_DOUBLE_TOTAL+GRAPH_OBJ_PROP_STRING_TOTAL)
     {
      //--- Присваивать целочисленному значению координаты X значение строкового свойства - плохая затея, только если знаешь что делаешь
      this.SetDependentINT(obj,(ENUM_GRAPH_OBJ_PROP_INTEGER)prop,(long)this.GetProperty((ENUM_GRAPH_OBJ_PROP_STRING)prop_from,modifier_from),modifier_to);
     }
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//|Устанавливает координату Y из указанного свойства базового объекта|
//| в указанный подчинённый объект                                   |
//+------------------------------------------------------------------+
void CGStdGraphObj::SetCoordYToDependentObj(CGStdGraphObj *obj,const int prop_from,const int modifier_from,const int modifier_to)
  {
   int prop=WRONG_VALUE;
   switch(obj.TypeGraphObject())
     {
      case OBJ_LABEL             :
      case OBJ_BUTTON            :
      case OBJ_BITMAP_LABEL      :
      case OBJ_EDIT              :
      case OBJ_RECTANGLE_LABEL   :
      case OBJ_CHART             :
        prop=GRAPH_OBJ_PROP_YDISTANCE;
        break;
      default:
        prop=GRAPH_OBJ_PROP_PRICE;
        break;
     }
   if(prop_from<GRAPH_OBJ_PROP_INTEGER_TOTAL)
     {
      if(prop==GRAPH_OBJ_PROP_YDISTANCE)
         this.SetDependentINT(obj,(ENUM_GRAPH_OBJ_PROP_INTEGER)prop,this.GetProperty((ENUM_GRAPH_OBJ_PROP_INTEGER)prop_from,modifier_from),modifier_to);
      else
         //--- Присваивать вещественному значению координаты Y значение целочисленного свойства - допустимо, только если знаешь что делаешь
         this.SetDependentDBL(obj,(ENUM_GRAPH_OBJ_PROP_DOUBLE)prop,this.GetProperty((ENUM_GRAPH_OBJ_PROP_INTEGER)prop_from,modifier_from),modifier_to);
     }
   else if(prop_from<GRAPH_OBJ_PROP_INTEGER_TOTAL+GRAPH_OBJ_PROP_DOUBLE_TOTAL)
     {
      if(prop==GRAPH_OBJ_PROP_YDISTANCE)
         //--- Присваивать целочисленному значению координаты Y значение вещественного свойства - плохая затея, только если знаешь что делаешь
         this.SetDependentINT(obj,(ENUM_GRAPH_OBJ_PROP_INTEGER)prop,(long)this.GetProperty((ENUM_GRAPH_OBJ_PROP_DOUBLE)prop_from,modifier_from),modifier_to);
      else
         this.SetDependentDBL(obj,(ENUM_GRAPH_OBJ_PROP_DOUBLE)prop,this.GetProperty((ENUM_GRAPH_OBJ_PROP_DOUBLE)prop_from,modifier_from),modifier_to);
     }
   else if(prop_from<GRAPH_OBJ_PROP_INTEGER_TOTAL+GRAPH_OBJ_PROP_DOUBLE_TOTAL+GRAPH_OBJ_PROP_STRING_TOTAL)
     {
      //--- Присваивать целочисленному или вещественному значению координаты Y значение строкового свойства - плохая затея, только если знаешь что делаешь
      if(prop==GRAPH_OBJ_PROP_YDISTANCE)
         this.SetDependentINT(obj,(ENUM_GRAPH_OBJ_PROP_INTEGER)prop,(long)this.GetProperty((ENUM_GRAPH_OBJ_PROP_STRING)prop_from,modifier_from),modifier_to);
      else
         this.SetDependentDBL(obj,(ENUM_GRAPH_OBJ_PROP_DOUBLE)prop,(double)this.GetProperty((ENUM_GRAPH_OBJ_PROP_STRING)prop_from,modifier_from),modifier_to);
     }
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Устанавливает в указанный подчинённый объект                     |
//| целочисленное свойство                                           |
//+------------------------------------------------------------------+
void CGStdGraphObj::SetDependentINT(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_INTEGER prop,const long value,const int modifier)
  {
   if(obj==NULL || obj.BaseObjectID()==0)
      return;
   switch(prop)
     {
      case GRAPH_OBJ_PROP_TIMEFRAMES            :  obj.SetVisibleOnTimeframes((int)value,false);         break;   // Видимость объекта на таймфреймах
      case GRAPH_OBJ_PROP_BACK                  :  obj.SetFlagBack(value,false);                         break;   // Объект на заднем плане
      case GRAPH_OBJ_PROP_ZORDER                :  obj.SetZorder(value,false);                           break;   // Приоритет графического объекта на получение события нажатия мышки на графике
      case GRAPH_OBJ_PROP_HIDDEN                :  obj.SetFlagHidden(value,false);                       break;   // Запрет на показ имени графического объекта в списке объектов терминала
      case GRAPH_OBJ_PROP_SELECTED              :  obj.SetFlagSelected(value,false);                     break;   // Выделенность объекта
      case GRAPH_OBJ_PROP_SELECTABLE            :  obj.SetFlagSelectable(value,false);                   break;   // Доступность объекта
      case GRAPH_OBJ_PROP_TIME                  :  obj.SetTime(value,modifier);                          break;   // Координата времени
      case GRAPH_OBJ_PROP_COLOR                 :  obj.SetColor((color)value);                           break;   // Цвет
      case GRAPH_OBJ_PROP_STYLE                 :  obj.SetStyle((ENUM_LINE_STYLE)value);                 break;   // Стиль
      case GRAPH_OBJ_PROP_WIDTH                 :  obj.SetWidth((int)value);                             break;   // Толщина линии
      case GRAPH_OBJ_PROP_FILL                  :  obj.SetFlagFill(value);                               break;   // Заливка объекта цветом
      case GRAPH_OBJ_PROP_READONLY              :  obj.SetFlagReadOnly(value);                           break;   // Возможность редактирования текста в объекте Edit
      case GRAPH_OBJ_PROP_LEVELS                :  obj.SetLevels((int)value);                            break;   // Количество уровней
      case GRAPH_OBJ_PROP_LEVELCOLOR            :  obj.SetLevelColor((color)value,modifier);             break;   // Цвет линии-уровня
      case GRAPH_OBJ_PROP_LEVELSTYLE            :  obj.SetLevelStyle((ENUM_LINE_STYLE)value,modifier);   break;   // Стиль линии-уровня
      case GRAPH_OBJ_PROP_LEVELWIDTH            :  obj.SetLevelWidth((int)value,modifier);               break;   // Толщина линии-уровня
      case GRAPH_OBJ_PROP_ALIGN                 :  obj.SetAlign((ENUM_ALIGN_MODE)value);                 break;   // Горизонтальное выравнивание текста в объекте "Поле ввода" (OBJ_EDIT)
      case GRAPH_OBJ_PROP_FONTSIZE              :  obj.SetFontSize((int)value);                          break;   // Размер шрифта
      case GRAPH_OBJ_PROP_RAY_LEFT              :  obj.SetFlagRayLeft(value);                            break;   // Луч продолжается влево
      case GRAPH_OBJ_PROP_RAY_RIGHT             :  obj.SetFlagRayRight(value);                           break;   // Луч продолжается вправо
      case GRAPH_OBJ_PROP_RAY                   :  obj.SetFlagRay(value);                                break;   // Вертикальная линия продолжается на все окна графика
      case GRAPH_OBJ_PROP_ELLIPSE               :  obj.SetFlagEllipse(value);                            break;   // Отображение полного эллипса для объекта "Дуги Фибоначчи"
      case GRAPH_OBJ_PROP_ARROWCODE             :  obj.SetArrowCode((uchar)value);                       break;   // Код стрелки для объекта "Стрелка"
      case GRAPH_OBJ_PROP_ANCHOR                :  obj.SetAnchor((int)value);                            break;   // Положение точки привязки графического объекта
      case GRAPH_OBJ_PROP_XDISTANCE             :  obj.SetXDistance((int)value);                         break;   // Дистанция в пикселях по оси X от угла привязки
      case GRAPH_OBJ_PROP_YDISTANCE             :  obj.SetYDistance((int)value);                         break;   // Дистанция в пикселях по оси Y от угла привязки
      case GRAPH_OBJ_PROP_DIRECTION             :  obj.SetDirection((ENUM_GANN_DIRECTION)value);         break;   // Тренд объекта Ганна
      case GRAPH_OBJ_PROP_DEGREE                :  obj.SetDegree((ENUM_ELLIOT_WAVE_DEGREE)value);        break;   // Уровень волновой разметки Эллиотта
      case GRAPH_OBJ_PROP_DRAWLINES             :  obj.SetFlagDrawLines(value);                          break;   // Отображение линий для волновой разметки Эллиотта
      case GRAPH_OBJ_PROP_STATE                 :  obj.SetFlagState(value);                              break;   // Состояние кнопки (Нажата/Отжата)
      case GRAPH_OBJ_PROP_CHART_OBJ_CHART_ID    :  obj.SetChartObjChartID(value);                        break;   // Идентификатор объекта "График" (OBJ_CHART)
      case GRAPH_OBJ_PROP_CHART_OBJ_PERIOD      :  obj.SetChartObjPeriod((ENUM_TIMEFRAMES)value);        break;   // Период для объекта "График"
      case GRAPH_OBJ_PROP_CHART_OBJ_DATE_SCALE  :  obj.SetChartObjChartScale((int)value);                break;   // Признак отображения шкалы времени для объекта "График"
      case GRAPH_OBJ_PROP_CHART_OBJ_PRICE_SCALE :  obj.SetFlagChartObjPriceScale(value);                 break;   // Признак отображения ценовой шкалы для объекта "График"
      case GRAPH_OBJ_PROP_CHART_OBJ_CHART_SCALE :  obj.SetFlagChartObjDateScale(value);                  break;   // Масштаб для объекта "График"
      case GRAPH_OBJ_PROP_XSIZE                 :  obj.SetXSize((int)value);                             break;   // Ширина объекта по оси X в пикселях
      case GRAPH_OBJ_PROP_YSIZE                 :  obj.SetYSize((int)value);                             break;   // Высота объекта по оси Y в пикселях
      case GRAPH_OBJ_PROP_XOFFSET               :  obj.SetXOffset((int)value);                           break;   // X-координата левого верхнего угла прямоугольной области видимости
      case GRAPH_OBJ_PROP_YOFFSET               :  obj.SetYOffset((int)value);                           break;   // Y-координата левого верхнего угла прямоугольной области видимости
      case GRAPH_OBJ_PROP_BGCOLOR               :  obj.SetBGColor((color)value);                         break;   // Цвет фона для OBJ_EDIT, OBJ_BUTTON, OBJ_RECTANGLE_LABEL
      case GRAPH_OBJ_PROP_CORNER                :  obj.SetCorner((ENUM_BASE_CORNER)value);               break;   // Угол графика для привязки графического объекта
      case GRAPH_OBJ_PROP_BORDER_TYPE           :  obj.SetBorderType((ENUM_BORDER_TYPE)value);           break;   // Тип рамки для объекта "Прямоугольная рамка"
      case GRAPH_OBJ_PROP_BORDER_COLOR          :  obj.SetBorderColor((color)value);                     break;   // Цвет рамки для объекта OBJ_EDIT и OBJ_BUTTON
      case GRAPH_OBJ_PROP_BASE_ID               :  obj.SetBaseObjectID(value);                           break;   // Идентификатор базового объекта
      case GRAPH_OBJ_PROP_GROUP                 :  obj.SetGroup((int)value);                             break;   // Группа графических объектов
      case GRAPH_OBJ_PROP_CHANGE_HISTORY        :  obj.SetAllowChangeMemory((bool)value);                break;   // Флаг хранения истории изменений
      case GRAPH_OBJ_PROP_ID                    :  // Идентификатор объекта
      case GRAPH_OBJ_PROP_TYPE                  :  // Тип графического объекта (ENUM_OBJECT)
      case GRAPH_OBJ_PROP_ELEMENT_TYPE          :  // Тип графического элемента (ENUM_GRAPH_ELEMENT_TYPE)
      case GRAPH_OBJ_PROP_SPECIES               :  // Вид графического объекта (ENUM_GRAPH_OBJ_SPECIES)
      case GRAPH_OBJ_PROP_BELONG                :  // Принадлежность графического объекта
      case GRAPH_OBJ_PROP_CHART_ID              :  // Идентификатор графика
      case GRAPH_OBJ_PROP_WND_NUM               :  // Номер подокна графика
      case GRAPH_OBJ_PROP_NUM                   :  // Номер объекта в списке
      case GRAPH_OBJ_PROP_CREATETIME            :  // Время создания объекта
      default  : break;
     }
  }
//+------------------------------------------------------------------+

Если передан невалидный указатель на объект, либо объект не является подчинённым (не привязан к базовому) — выходим. Далее просто устанавливаем в объект переданное в метод свойство. Некоторые свойства объекта менять нельзя, поэтому они находятся в конце списка переключателя switch, и не обрабатываются никак.

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

//+------------------------------------------------------------------+
//|Устанавливает в указанный подчинённый объект вещественное свойство|
//+------------------------------------------------------------------+
void CGStdGraphObj::SetDependentDBL(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_DOUBLE prop,const double value,const int modifier)
  {
   if(obj==NULL || obj.BaseObjectID()==0)
      return;
   switch(prop)
     {
      case GRAPH_OBJ_PROP_PRICE                 : obj.SetPrice(value,modifier);        break;   // Координата цены
      case GRAPH_OBJ_PROP_LEVELVALUE            : obj.SetLevelValue(value,modifier);   break;   // Значение уровня
      case GRAPH_OBJ_PROP_SCALE                 : obj.SetScale(value);                 break;   // Масштаб (свойство объектов Ганна и объекта "Дуги Фибоначчи")
      case GRAPH_OBJ_PROP_ANGLE                 : obj.SetAngle(value);                 break;   // Угол
      case GRAPH_OBJ_PROP_DEVIATION             : obj.SetDeviation(value);             break;   // Отклонение для канала стандартного отклонения
      default: break;
     }
  }
//+------------------------------------------------------------------+

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

//+------------------------------------------------------------------+
//| Устанавливает в указанный подчинённый объект строковое свойство  |
//+------------------------------------------------------------------+
void CGStdGraphObj::SetDependentSTR(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_STRING prop,const string value,const int modifier)
  {
   if(obj==NULL || obj.BaseObjectID()==0)
      return;
   obj.SetProperty(prop,modifier,value);
   switch(prop)
     {
      case GRAPH_OBJ_PROP_TEXT                  : obj.SetText(value);                  break;   // Описание объекта (текст, содержащийся в объекте)
      case GRAPH_OBJ_PROP_TOOLTIP               : obj.SetTooltip(value);               break;   // Текст всплывающей подсказки
      case GRAPH_OBJ_PROP_LEVELTEXT             : obj.SetLevelText(value,modifier);    break;   // Описание уровня
      case GRAPH_OBJ_PROP_FONT                  : obj.SetFont(value);                  break;   // Шрифт
      case GRAPH_OBJ_PROP_BMPFILE               : obj.SetBMPFile(value,modifier);      break;   // Имя BMP-файла для объекта "Графическая метка"
      case GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL      : obj.SetChartObjSymbol(value);        break;   // Символ для объекта "График"
      case GRAPH_OBJ_PROP_BASE_NAME             : obj.SetBaseName(value);              break;   // Имя базового объекта
      case GRAPH_OBJ_PROP_NAME                  : // Имя объекта
      default :  break;
     }
  }
//+------------------------------------------------------------------+

Оба метода идентичны методу, устанавливающему целочисленное свойство.


Перемещение и удаление составного графического объекта

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

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

//+------------------------------------------------------------------+
//| Проверяет изменения свойств объекта                              |
//+------------------------------------------------------------------+
void CGStdGraphObj::PropertiesCheckChanged(void)
  {
   CGBaseObj::ClearEventsList();
   bool changed=false;
   int begin=0, end=GRAPH_OBJ_PROP_INTEGER_TOTAL;
   for(int i=begin; i<end; i++)
     {
      ENUM_GRAPH_OBJ_PROP_INTEGER prop=(ENUM_GRAPH_OBJ_PROP_INTEGER)i;
      if(!this.SupportProperty(prop)) continue;
      for(int j=0;j<Prop.CurrSize(prop);j++)
        {
         if(this.GetProperty(prop,j)!=this.GetPropertyPrev(prop,j))
           {
            changed=true;
            this.CreateAndAddNewEvent(GRAPH_OBJ_EVENT_CHANGE,this.ChartID(),prop,this.Name());
           }
        }
     }

   begin=end; end+=GRAPH_OBJ_PROP_DOUBLE_TOTAL;
   for(int i=begin; i<end; i++)
     {
      ENUM_GRAPH_OBJ_PROP_DOUBLE prop=(ENUM_GRAPH_OBJ_PROP_DOUBLE)i;
      if(!this.SupportProperty(prop)) continue;
      for(int j=0;j<Prop.CurrSize(prop);j++)
        {
         if(this.GetProperty(prop,j)!=this.GetPropertyPrev(prop,j))
           {
            changed=true;
            this.CreateAndAddNewEvent(GRAPH_OBJ_EVENT_CHANGE,this.ChartID(),prop,this.Name());
           }
        }
     }

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

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

Составной графический объект мы можем удалить с графика только удаляя базовый объект, к которому привязаны все подчинённые.
Такую ситуацию (удаление базового объекта) будем обрабатывать в классе-коллекции графических элементов в файле
\MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh.

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

//--- Обновляет список (1) всех графических объектов, (2) на указанном чарте, заполняет данные о количестве новых и устанавливает флаг события
   void              Refresh(void);
   void              Refresh(const long chart_id);
//--- Обработчик событий
   void              OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam);

private:
//--- Обрабатывает удаление расширенных графических объектов
   void              DeleteExtendedObj(CGStdGraphObj *obj);
//--- Создаёт новый графический объект, возвращает указатель на объект управления чартами

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

//+------------------------------------------------------------------+
//| Обрабатывает удаление расширенных графических объектов           |
//+------------------------------------------------------------------+
void CGraphElementsCollection::DeleteExtendedObj(CGStdGraphObj *obj)
  {
   if(obj==NULL)
      return;
   //--- Запомним идентификатор графика графического объекта и количество зависимых объектов в его списке
   long chart_id=obj.ChartID();
   int total=obj.GetNumDependentObj();
   //--- Если список зависимых объектов не пустой (значит это базовый объект)
   if(total>0)
     {
      //--- Пройдёмся в цикле по всем зависимым объектам и удалим их
      for(int n=total-1;n>WRONG_VALUE;n--)
        {
         //--- Получаем очередной графический объект
         CGStdGraphObj *dep=obj.GetDependentObj(n);
         if(dep==NULL)
            continue;
         //--- Если его не получилось удалить с графика - выводим об этом сообщение в журнал
         if(!::ObjectDelete(dep.ChartID(),dep.Name()))
            CMessage::ToLog(DFUN+dep.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART);
        }
      //--- По окончании цикла обновляем график дляотображения изменений и выходим из метода
      ::ChartRedraw(chart_id);
      return;
     }
   //--- Если это зависимый объект
   else if(obj.BaseObjectID()>0)
     {
      //--- Получаем имя базового объекта и его идентификатор
      string base_name=obj.BaseName();
      long base_id=obj.BaseObjectID();
      //--- Получаем базовый объект из списка-коллекции графических объектов
      CGStdGraphObj *base=GetStdGraphObject(base_name,chart_id);
      if(base==NULL)
         return;
      //--- получаем количество зависимых объектов в его списке
      int count=base.GetNumDependentObj();
      //--- Пройдёмся в цикле по всем его зависимым объектам и удалим их
      for(int n=count-1;n>WRONG_VALUE;n--)
        {
         //--- Получаем очередной графический объект
         CGStdGraphObj *dep=base.GetDependentObj(n);
         //--- Если указатель получить не удалось, или этот объект уже удалён с графика - идём к следующему
         if(dep==NULL || !this.IsPresentGraphObjOnChart(dep.ChartID(),dep.Name()))
            continue;
         //--- Если графический объект не получилось удалить с графика -
         //--- выводим об этом сообщение в журнал и идём к следующему
         if(!::ObjectDelete(dep.ChartID(),dep.Name()))
           {
            CMessage::ToLog(DFUN+dep.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART);
            continue;
           }
        }
      //--- Удалим базовый объект с графика и из списка
      if(!::ObjectDelete(base.ChartID(),base.Name()))
         CMessage::ToLog(DFUN+base.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART);
     }
   //--- Обновляем график для отображения изменений
   ::ChartRedraw(chart_id);
  }
//+------------------------------------------------------------------+

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

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

//+------------------------------------------------------------------+
//| Обновляет список всех графических объектов                       |
//+------------------------------------------------------------------+
void CGraphElementsCollection::Refresh(void)
  {
   this.RefreshForExtraObjects();
//--- Объявим переменные для поиска графиков
   long chart_id=0;
   int i=0;
//--- В цикле по всем открытым графикам терминала (не более 100)
   while(i<CHARTS_MAX)
     {
      //--- Получим идентификатор графика
      chart_id=::ChartNext(chart_id);
      if(chart_id<0)
         break;
      //--- Получаем указатель на объект управления графическими объектами
      //--- и обновляем список графических объектов по идентификатору чарта
      CChartObjectsControl *obj_ctrl=this.RefreshByChartID(chart_id);
      //--- Если указатель получить не удалось - идём к следующему графику
      if(obj_ctrl==NULL)
         continue;
      //--- Если есть событие изменения количества объектов на графике
      if(obj_ctrl.IsEvent())
        {
         //--- Если добавлен графический объект на график
         if(obj_ctrl.Delta()>0)
           {
            //--- Получаем список добавленных графических объектов и перемещаем их в список-коллекцию
            //--- (если объект поместить в коллекцию не удалось - идём к следующему объекту)
            if(!this.AddGraphObjToCollection(DFUN_ERR_LINE,obj_ctrl))
               continue;
           }
         //--- Если удалён графический объект
         else if(obj_ctrl.Delta()<0)
           {
            int index=WRONG_VALUE;
            //--- В цикле по количеству удалённых графических объектов
            for(int j=0;j<-obj_ctrl.Delta();j++)
              {
               // Найдём лишний объект в списке
               CGStdGraphObj *obj=this.FindMissingObj(chart_id,index);
               if(obj!=NULL)
                 {
                  //--- Получим параметры удалённого объекта
                  long   lparam=obj.ChartID();
                  string sparam=obj.Name();
                  double dparam=(double)obj.TimeCreate();
                  //--- Если это расширенный графический объект
                  if(obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)
                    {                            
                     this.DeleteExtendedObj(obj);
                    }                            
                  //--- Переместим объект класса графического объекта в список удалённых объектов
                  //--- и отправим событие на график управляющей программы
                  if(this.MoveGraphObjToDeletedObjList(index))
                     ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_DELETE,lparam,dparam,sparam);
                 }
              }
           }
        }
      //--- Увеличиваем индекс цикла
      i++;
     }
  }
//+------------------------------------------------------------------+

И этого достаточно для обработки удаления составного стандартного графического объекта.

Протестируем что у нас получилось.


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

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

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

   if(id==CHARTEVENT_CLICK)
     {
      if(!IsCtrlKeyPressed())
         return;
      //--- Получаем координаты щелчка по графику
      datetime time=0;
      double price=0;
      int sw=0;
      if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,sw,time,price))
        {
         //--- Получаем координаты правой точки для трендовой линии
         datetime time2=iTime(Symbol(),PERIOD_CURRENT,1);
         double price2=iOpen(Symbol(),PERIOD_CURRENT,1);
         
         //--- Создаём объект "Трендовая линия"
         string name_base="TrendLineExt";
         engine.CreateLineTrend(name_base,0,true,time,price,time2,price2);
         //--- Получаем объект из списка графических объектов по имени и идентификатору графика и распечатываем его свойства в журнал
         CGStdGraphObj *obj=engine.GraphGetStdGraphObjectExt(name_base,ChartID());
         
         //--- Создаём объект "Левая ценовая метка"
         string name_dep="PriceLeft";
         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="PriceRight";
         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);
        }
     }

То, что здесь мы создаём объекты "Левая ценовая метка" и "Правая ценовая метка" как не расширенные, в этом нет ничего страшного, так как теперь в методе AddDependentObj() все прикреплённые объекты однозначно получают статус расширенного графического объекта.

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


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

Что дальше

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

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

К содержанию

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

Графика в библиотеке DoEasy (Часть 89): Программное создание стандартных графических объектов. Базовый функционал
Графика в библиотеке DoEasy (Часть 90): События стандартных графических объектов. Базовый функционал
Графика в библиотеке DoEasy (Часть 91): События стандартных графических объектов в программе. История изменения имени объекта
Графика в библиотеке DoEasy (Часть 92): Класс памяти стандартных графических объектов. История изменения свойств объекта
Графика в библиотеке DoEasy (Часть 93): Готовим функционал для создания составных графических объектов