Русский 中文 Español Deutsch 日本語 Português
Graphics in DoEasy library (Part 95): Composite graphical object controls

Graphics in DoEasy library (Part 95): Composite graphical object controls

MetaTrader 5Examples | 18 March 2022, 06:41
8 634 0
Artyom Trishkin
Artyom Trishkin

Contents


Concept

In this article, I will continue the development of composite graphical objects. These are standard graphical objects consisting of several ones and combined into a single graphical object. In the library, graphical objects included into a composite one are defined as extended standard graphical objects. Such objects have some additional properties and functionality allowing them to be incorporated and incorporate other graphical objects.
The concept of a composite graphical object requires the functionality for keeping the object at the point it is attached to another object and adjusting its location when changing or relocating a parent object.
In the previous article, I started creating the handlers of composite graphical object events, implemented the handling of a composite graphical object removal and started developing its relocation handler.

Today, I will slightly digress from relocating a composite graphical object and implement the handler of change events on a chart featuring a composite graphical object. Besides, I will focus on the controls for managing a composite graphical object.
Why? I am going to implement real-time creation of composite graphical objects — attaching a subordinate object to a base one by dragging the subordinate object to the base one. The base graphical object tracks if another object is being dragged to it with a mouse. The object attachment mechanism is enabled at a certain distance from one of its chart anchor points . The lines connecting the anchor point of the attached object with the anchor point of the base one are displayed visually indicating that the dragged object is ready to be attached to the base one. To achieve this, each anchor point of the graphical object should feature a form object of a certain size. Entering the form object area is to activate the attachment mechanism, while the lines, indicating that the objects are ready for interaction, are displayed on the form itself. Such forms are invisibly present on each pivot point of the graphical object. The area size can be seen only for debugging purposes by enabling the drawing of a rectangle along the form edges:

Besides, the form displays the graphical object anchor points that will appear only when the mouse cursor hovers over the form active area. Thus, we will be able to move and modify the extended graphical object by hovering the mouse cursor over the form area, rather than by highlighting it by a mouse click. As soon as we hover the cursor over the active area of the form (marked by rectangles on the image above), the labels appear in the graphical object anchor point (the blue dot at the center of the circle). If we start dragging the form by mouse, the appropriate pivot point of the graphical object will follow the cursor modifying the object itself together with the composite graphical object.
If the mouse cursor enters the form active area with the pressed mouse button, this means (if verified) that we apply another graphical object to the form activating the mechanism of bounding one object to another. Thus, the forms allow us to accomplish several objectives at once.

I am not going to implement attaching one object to another here since the preparations are not finished yet. Instead, I will create the forms, attach them to the graphical object anchor points and implement the mechanism of moving them along the object pivot point coordinates when changing the chart — relocating it or changing its display scale. This should be done since the form object has the coordinates in screen pixels, while most graphical objects are shown in time/price values.


Improving library classes

In \MQL5\Include\DoEasy\Data.mqh, add new message indices:

//--- CLinkedPivotPoint
   MSG_GRAPH_OBJ_EXT_NOT_ANY_PIVOTS_X,                // Not a single pivot point is set for the object along the X axis
   MSG_GRAPH_OBJ_EXT_NOT_ANY_PIVOTS_Y,                // Not a single pivot point is set for the object along the Y axis
   MSG_GRAPH_OBJ_EXT_NOT_ATACHED_TO_BASE,             // The object is not attached to the basic graphical object
   MSG_GRAPH_OBJ_EXT_FAILED_CREATE_PP_DATA_OBJ,       // Failed to create a data object for the X and Y pivot points
   MSG_GRAPH_OBJ_EXT_NUM_BASE_PP_TO_SET_X,            // Number of base object pivot points for calculating the X coordinate: 
   MSG_GRAPH_OBJ_EXT_NUM_BASE_PP_TO_SET_Y,            // Number of base object pivot points for calculating the Y coordinate: 

//--- CGStdGraphObjExtToolkit
   MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA,     // Failed to change the size of the pivot point time data array
   MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA,    // Failed to change the size of the pivot point price data array
   MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM,   // Failed to create a form object to manage a pivot point
   
  };
//+------------------------------------------------------------------+

and the message texts corresponding to the newly added indices:

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


Replace the macro substitution in \MQL5\Include\DoEasy\Defines.mqh

#define CLR_DEFAULT                    (0xFF000000)               // Default symbol background color in the navigator

with the more comprehensible

#define CLR_MW_DEFAULT                 (0xFF000000)               // Default symbol background color in the Market Watch

and the macro substitution

#define NULL_COLOR                     (0x00FFFFFF)               // Zero for the canvas with the alpha channel

with the more comprehensible

#define CLR_CANV_NULL                  (0x00FFFFFF)               // Zero for the canvas with the alpha channel

and add the new macro substitutions for setting the default values for the form objects to be created here:

//--- Graphical object parameters
#define PROGRAM_OBJ_MAX_ID             (10000)                    // Maximum value of an ID of a graphical object belonging to a program
#define CTRL_POINT_SIZE                (5)                        // Radius of the control point on the form for managing graphical object pivot points
#define CTRL_FORM_SIZE                 (40)                       // Size of the control point form for managing graphical object pivot points
//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+


Replace the old macro substitution names in all files.

Simply press Ctrl+Shift+H, enter the following values and check the boxes shown below:


Click "Replace in Files". The editor makes replacements in all library files.

Replace NULL_COLOR with CLR_CANV_NULL the same way.

The new macro substitution names are more illustrative and indicative of their function reducing the amount of potential errors (for instance, I entered CLR_DEFAULT for setting a transparent canvas background and spent a lot of time trying to understand why it did not work).

In addition, I also performed some small changes in all class files of the base graphical object descendants (by simply adding a comma to the text of the method displaying a short object description in the journal):

//+------------------------------------------------------------------+
//| Display a short description of the object in the journal         |
//+------------------------------------------------------------------+
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)
     );
  }
//+------------------------------------------------------------------+

This is a purely "cosmetic" improvement.
It has been implemented in all files in \MQL5\Include\DoEasy\Objects\Graph\Standard\.


Extended standard graphical object toolkit class

Let's start creating a toolkit for handling extended graphical objects. This will be a class featuring all the necessary methods for creating form objects and working with them. Each extended graphical object is to have a pointer to the object of the appropriate class. If necessary (if this is a base object within a composite graphical object), the class object is created dynamically when creating an extended object and deleted when the latter is deleted.

The object receives the necessary parameters of the base graphical object (its coordinates, type, name, etc.). The base object coordinates are tracked and the form object coordinates are adjusted inside the object. Eventually, the form objects will enable managing the base object.

But first things first...

In \MQL5\Include\DoEasy\Objects\Graph\, create the new Extend\ folder containing the CGStdGraphObjExtToolkit.mqh file of the CGStdGraphObjExtToolkit class inherited from the base CObject class for constructing the MQL5 standard library:

//+------------------------------------------------------------------+
//|                                      CGStdGraphObjExtToolkit.mqh |
//|                                  Copyright 2021, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2021, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "..\..\Graph\Form.mqh"
//+------------------------------------------------------------------+
//| Extended standard graphical                                      |
//| object toolkit class                                             |
//+------------------------------------------------------------------+
class CGStdGraphObjExtToolkit : public CObject
  {
  }

The form object class file should be included in the class file.

In the private section of the class, declare the variables storing all the necessary base object properties, the properties for constructing forms, the list for storing them, and the methods for creating a form object and returning its screen coordinates:

//+------------------------------------------------------------------+
//| Extended standard graphical                                      |
//| object toolkit class                                             |
//+------------------------------------------------------------------+
class CGStdGraphObjExtToolkit : public CObject
  {
private:
   long              m_base_chart_id;           // Base graphical object chart ID
   int               m_base_subwindow;          // Base graphical object chart subwindow
   ENUM_OBJECT       m_base_type;               // Base object type
   string            m_base_name;               // Base object name
   int               m_base_pivots;             // Number of base object reference points
   datetime          m_base_time[];             // Time array of base object reference points
   double            m_base_price[];            // Price array of base object reference points
   int               m_base_x;                  // Base object X coordinate
   int               m_base_y;                  // Base object Y coordinate
   int               m_ctrl_form_size;          // Size of forms for managing reference points
   int               m_shift;                   // Shift coordinates for adjusting the form location
   CArrayObj         m_list_forms;              // List of form objects for managing reference points
//--- Create a form object on a base object reference point
   CForm            *CreateNewControlPointForm(const int index);
//--- Return X and Y screen coordinates of the specified reference point of the graphical object
   bool              GetControlPointCoordXY(const int index,int &x,int &y);
public:

Let's use the arrays to store price and time coordinates since a single graphical object may have multiple pivot points. So, the coordinates of each point will be stored in the appropriate array cells. The first point coordinate — by the array 0 index, the second point coordinate — by the array 1 index, the third one — by index 2, etc.
Shifting the form coordinates allows us to locate the form precisely at the center of the object pivot point. This shift covers the half of the point size. If the form size is multiple of two, for example 10, it is adjusted by adding 1, i.e. 11. This enables placing the form precisely at the center of the graphical object pivot point so that no side exceeds another one even by a single pixel.
The list of forms is to store all created forms. The access to them is granted by the pointer. The method for calculating the screen coordinates allows us to know the coordinates of the screen where the form is to be placed. This enables its accurate placement on the graphical object pivot point.

In the public section of the class, declare all the necessary methods for handling the class:

public:
//--- Set the parameters of the base object of a composite graphical object
   void              SetBaseObj(const ENUM_OBJECT base_type,const string base_name,
                                const long base_chart_id,const int base_subwindow,
                                const int base_pivots,const int ctrl_form_size,
                                const int base_x,const int base_y,
                                const datetime &base_time[],const double &base_price[]);
//--- Set the base object (1) time, (2) price, (3) time and price coordinates
   void              SetBaseObjTime(const datetime time,const int index);
   void              SetBaseObjPrice(const double price,const int index);
   void              SetBaseObjTimePrice(const datetime time,const double price,const int index);
//--- Set the base object (1) X, (2) Y, (3) X and Y screen coordinates
   void              SetBaseObjCoordX(const int value)                        { this.m_base_x=value;                          }
   void              SetBaseObjCoordY(const int value)                        { this.m_base_y=value;                          }
   void              SetBaseObjCoordXY(const int value_x,const int value_y)   { this.m_base_x=value_x; this.m_base_y=value_y; }
//--- (1) Set and (2) return the size of the form of pivot point management control points
   void              SetControlFormSize(const int size);
   int               GetControlFormSize(void)                          const { return this.m_ctrl_form_size;                }
//--- Return the pointer to the pivot point form by (1) index and (2) name
   CForm            *GetControlPointForm(const int index)                     { return this.m_list_forms.At(index);           }
   CForm            *GetControlPointForm(const string name,int &index);
//--- Return the number of the base object pivot points
   int               GetNumPivotsBaseObj(void)                          const { return this.m_base_pivots;                    }
//--- Create form objects on the base object pivot points
   bool              CreateAllControlPointForm(void);
//--- Remove all form objects from the list
   void              DeleteAllControlPointForm(void);
   
//--- Event handler
   void              OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam);

//--- Constructor/destructor
                     CGStdGraphObjExtToolkit(const ENUM_OBJECT base_type,const string base_name,
                                             const long base_chart_id,const int base_subwindow,
                                             const int base_pivots,const int ctrl_form_size,
                                             const int base_x,const int base_y,
                                             const datetime &base_time[],const double &base_price[])
                       {
                        this.m_list_forms.Clear();
                        this.SetBaseObj(base_type,base_name,base_chart_id,base_subwindow,base_pivots,ctrl_form_size,base_x,base_y,base_time,base_price);
                        this.CreateAllControlPointForm();
                       }
                     CGStdGraphObjExtToolkit(){;}
                    ~CGStdGraphObjExtToolkit(){;}
  };
//+------------------------------------------------------------------+

In the class constructor, clear the list of form objects, set all the necessary base object values (passed in the constructor parameters) to the class variables and create the form objects for managing the base object at each pivot point of the base graphical object.

The method setting the parameters of the base object of a composite graphical object:

//+------------------------------------------------------------------+
//| Set the base object parameters of the                            |
//| composite graphical object                                       |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::SetBaseObj(const ENUM_OBJECT base_type,const string base_name,
                                         const long base_chart_id,const int base_subwindow,
                                         const int base_pivots,const int ctrl_form_size,
                                         const int base_x,const int base_y,
                                         const datetime &base_time[],const double &base_price[])
  {
   this.m_base_chart_id=base_chart_id;       // Base graphical object chart ID
   this.m_base_subwindow=base_subwindow;     // Base graphical object chart subwindow
   this.m_base_type=base_type;               // Base object type
   this.m_base_name=base_name;               // Base object name
   this.m_base_pivots=base_pivots;           // Number of base object reference points
   this.m_base_x=base_x;                     // Base object X coordinate
   this.m_base_y=base_y;                     // Base object Y coordinate
   this.SetControlFormSize(ctrl_form_size);  // Size of forms for managing reference points
   
   if(this.m_base_type==OBJ_LABEL            || this.m_base_type==OBJ_BUTTON  ||
      this.m_base_type==OBJ_BITMAP_LABEL     || this.m_base_type==OBJ_EDIT    ||
      this.m_base_type==OBJ_RECTANGLE_LABEL  || this.m_base_type==OBJ_CHART)
      return;
   
   if(::ArraySize(base_time)==0)
     {
      CMessage::ToLog(DFUN+"base_time: ",MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY);
      return;
     }
   if(::ArraySize(base_price)==0)
     {
      CMessage::ToLog(DFUN+"base_price: ",MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY);
      return;
     }
   if(::ArrayResize(this.m_base_time,this.m_base_pivots)!=this.m_base_pivots)
     {
      CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA);
      return;
     }
   if(::ArrayResize(this.m_base_price,this.m_base_pivots)!=this.m_base_pivots)
     {
      CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA);
      return;
     }
   for(int i=0;i<this.m_base_pivots;i++)
     {
      this.m_base_time[i]=base_time[i];      // Time (i) of the base object pivot point
      this.m_base_price[i]=base_price[i];    // Price (i) of the base object pivot point
     }
  }
//+------------------------------------------------------------------+

The method receives all the necessary values of the base graphical object properties. Next, check the base object type. If this is an object not based on the price/time coordinates, exit the method. We do not handle such objects yet. Next, check the size of the arrays of the base object coordinates passed to the method. If they are of zero size (the method has received an empty array), inform of that and exit the method. Next, change the size of the coordinate internal arrays according to the passed ones. If failed to change the array, inform of that and leave. In the end, simply copy the input arrays to the internal ones element by element.

The method setting the size of reference points for managing pivot points:

//+------------------------------------------------------------------+
//|Set the size of reference points for managing pivot points        |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::SetControlFormSize(const int size)
  {
   this.m_ctrl_form_size=(size>254 ? 255 : size<5 ? 5 : size%2==0 ? size+1 : size);
   this.m_shift=(int)ceil(m_ctrl_form_size/2)+1;
  }
//+------------------------------------------------------------------+

The method receives the required form size. If the size exceeds 254, set it to 255 (odd value), if the passed size is less than 5, set it to 5 (this will be the minimal form size value). Otherwise, if the passed size is equal to two, add one to it and use it. If none of the checked values is true, use the size passed to the method.
Next, calculate the coordinate shift since the form should be set so that the pivot point of the graphical object is located precisely at its center. To achieve this, we should substract the shift value from the coordinate value. Divide the calculate form size into two, take the closest higher integer value and add one.

The method setting the time coordinate of the base object:

//+------------------------------------------------------------------+
//| Set the time coordinate of the base object                       |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::SetBaseObjTime(const datetime time,const int index)
  {
   if(index>this.m_base_pivots-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY);
      return;
     }
   this.m_base_time[index]=time;
  }
//+------------------------------------------------------------------+

The method receives the pivot point time and the object pivot point index. If the index exceeds the number of pivot points in the object, inform of the request out of the array range and leave. As a result, the time value, passed to the method, is set in the cell corresponding to the index in the time array.
The method is necessary for specifying the base object time in the class object when the object is changed.

The method setting the price coordinate of the base object:

//+------------------------------------------------------------------+
//| Set the coordinate of the base object price                      |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::SetBaseObjPrice(const double price,const int index)
  {
   if(index>this.m_base_pivots-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY);
      return;
     }
   this.m_base_price[index]=price;
  }
//+------------------------------------------------------------------+

The method is identical to the one considered above except that here we add the price of the base object pivot point, specified by the index, to the class price array.

The method setting the time and price coordinate of the base object:

//+------------------------------------------------------------------+
//| Set the time and price coordinates of the base object            |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::SetBaseObjTimePrice(const datetime time,const double price,const int index)
  {
   if(index>this.m_base_pivots-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY);
      return;
     }
   this.m_base_time[index]=time;
   this.m_base_price[index]=price;
  }
//+------------------------------------------------------------------+

The method is identical to the two methods considered above except that both the price and time are set in the class arrays.

The method returning the X and Y coordinates of the specified pivot point of the graphical object in screen coordinates:

//+------------------------------------------------------------------+
//| Return the X and Y coordinates of the specified pivot point      |
//| of the graphical object in screen coordinates                    |
//+------------------------------------------------------------------+
bool CGStdGraphObjExtToolkit::GetControlPointCoordXY(const int index,int &x,int &y)
  {
   switch(this.m_base_type)
     {
      case OBJ_LABEL             :
      case OBJ_BUTTON            :
      case OBJ_BITMAP_LABEL      :
      case OBJ_EDIT              :
      case OBJ_RECTANGLE_LABEL   :
      case OBJ_CHART             :
        x=this.m_base_x;
        y=this.m_base_y;
        break;
      default:
        if(!::ChartTimePriceToXY(this.m_base_chart_id,this.m_base_subwindow,this.m_base_time[index],this.m_base_price[index],x,y))
          {
           x=0;
           y=0;
           return false;
          }
     }
   return true;
  }
//+------------------------------------------------------------------+

The method receives the index of the required pivot point of the base graphical object. This is the pivot point the screen coordinates (in pixels from the upper left screen corner) and two variables (via the link), which are to receive the form screen coordinates, should be received for. If the object is already located within the screen coordinates, these coordinates are returned.
If the object is located within the price/time coordinates, calculate them using the ChartTimePriceToXY() function. If failed to convert the coordinates to the screen ones, set the coordinates to zero and return false.
As a result, return true
.

The method returning the pointer to the pivot point form by name:

//+------------------------------------------------------------------+
//| Return the pointer to the pivot point form by name               |
//+------------------------------------------------------------------+
CForm *CGStdGraphObjExtToolkit::GetControlPointForm(const string name,int &index)
  {
   index=WRONG_VALUE;
   for(int i=0;i<this.m_list_forms.Total();i++)
     {
      CForm *form=this.m_list_forms.At(i);
      if(form==NULL)
         continue;
      if(form.Name()==name)
        {
         index=i;
         return form;
        }
     }
   return NULL;
  }
//+------------------------------------------------------------------+

The method receives the name of the desired form and the variable via the link featuring the index of the detected form in the list of form objects.
In the loop by the list of form objects, get the next object. If its name matches the desired one, write the loop index to the variable and return the pointer to the detected object. Upon the loop completion, return NULL — the form is not found, the index is equal to -1. This value has been set before the loop start.

The method creating a form object on a base object reference point:

//+------------------------------------------------------------------+
//| Create a form object on a base object reference point            |
//+------------------------------------------------------------------+
CForm *CGStdGraphObjExtToolkit::CreateNewControlPointForm(const int index)
  {
   string name=this.m_base_name+"_TKPP_"+(index<this.m_base_pivots ? (string)index : "X");
   CForm *form=this.GetControlPointForm(index);
   if(form!=NULL)
      return NULL;
   int x=0, y=0;
   if(!this.GetControlPointCoordXY(index,x,y))
      return NULL;
   return new CForm(this.m_base_chart_id,this.m_base_subwindow,name,x-this.m_shift,y-this.m_shift,this.GetControlFormSize(),this.GetControlFormSize());
  }
//+------------------------------------------------------------------+

The method receives the index of the required pivot point the form object should be created at.
Create the form object name consisting of the base object name + "ToolKit Pivot Point" (_TKPP) abbreviation + pivot point index. When creating the index description, check its value. If it is less than the number of base object pivot points (calculation starts from zero), use the string representation of the index passed to the method. Otherwise, use the X icon. Why do we need this? Later, it will be possible to attach subordinate objects to the base one not only at its pivot points but also between them. Besides, we need to create a control form at the base object line center, beyond which the entire object is relocated, to relocate it. Therefore, the form name should feature the ability to create the form not only for pivot points but for other points as well.
Next, check the presence of the form in the list by index passed to the method. If the form object is already present by the index in the list (the pointer to it is not equal to NULL), return NULL.
Next, transform the pivot point coordinates by its index to the screen coordinates and return the result of creating a form object on received coordinates. The shift value is subtracted from both coordinates for precise positioning of the form center on the pivot point.
I could simply set the value for the form anchor point but our library convention states that the anchor point of all forms remains unchanged — in its upper left corner. Therefore, the shift for the form object positioning is used wherever necessary.

The method creating form objects on the base object pivot points:

//+------------------------------------------------------------------+
//| Create form objects on the base object pivot points              |
//+------------------------------------------------------------------+
bool CGStdGraphObjExtToolkit::CreateAllControlPointForm(void)
  {
   bool res=true;
   //--- In the loop by the number of base object pivot points
   for(int i=0;i<this.m_base_pivots;i++)
     {
      //--- Create a new form object on the current pivot point corresponding to the loop index
      CForm *form=this.CreateNewControlPointForm(i);
      //--- If failed to create the form, inform of that and add 'false' to the final result
      if(form==NULL)
        {
         CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM);
         res &=false;
        }
      //--- If failed to add the form to the list, inform of that, remove the created form and add 'false' to the final result
      if(!this.m_list_forms.Add(form))
        {
         CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
         delete form;
         res &=false;
        }
      //--- Set all the necessary properties for the created form object
      form.SetBelong(GRAPH_OBJ_BELONG_PROGRAM); // Object is created programmatically
      form.SetActive(true);                     // Form object is active
      form.SetMovable(true);                    // Movable object
      form.SetActiveAreaShift(0,0,0,0);         // Object active area - the entire form
      form.SetFlagSelected(false,false);        // Object is not selected
      form.SetFlagSelectable(false,false);      // Object cannot be selected by mouse
      form.Erase(CLR_CANV_NULL,0);              // Fill in the form with transparent color and set the full transparency
      //form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,clrSilver); // Draw an outlining rectangle for visual display of the form location
      form.DrawCircle((int)floor(form.Width()/2),(int)floor(form.Height()/2),CTRL_POINT_SIZE,clrDodgerBlue);   // Draw a circle in the form center
      form.DrawCircleFill((int)floor(form.Width()/2),(int)floor(form.Height()/2),2,clrDodgerBlue);             // Draw a point in the form center
      form.Done();                              // Save the initial form object state (its appearance)
     }
   //--- Redraw the chart for displaying changes (if successful) and return the final result
   if(res)
      ::ChartRedraw(this.m_base_chart_id);
   return res;
  }
//+------------------------------------------------------------------+

The entire logic here is described in the code comments. In brief, in the loop by the number of base object pivot points, create a new form object for each pivot point, add it to the list of form objects and set the coordinates of the appropriate base object pivot point to each form. Each form features the circle in the center and the point specifying that this is the object of managing the base object pivot point.

Such objects are to be initially invisible and to appear only when the mouse cursor hovers over the form area. For now, I will make them visible to test their behavior during chart changes. In subsequent articles, I will stick to my plan — the objects will be initially invisible and appear only when the mouse hovers over their active area (i.e. the entire form object size).

The method removing all form objects from the list:

//+------------------------------------------------------------------+
//| Remove all form objects from the list                            |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::DeleteAllControlPointForm(void)
  {
   this.m_list_forms.Clear();
  }
//+------------------------------------------------------------------+

Simply use the Clear() method fully clearing the entire list.

In the event handler, process form object events according to an occurred event:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
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);
     }
  }
//+------------------------------------------------------------------+

Currently, we handle only the chart change event. In the loop by all form objects, get the next form from the list. If failed to receive its screen coordinates according to the pivot point it is drawn on, move on to the next form. Set the new screen coordinates to the form and update the form. Upon the loop completion, redraw the chart for displaying changes.

Since the toolkit object of the expanded standard graphical object is stored in the object of the standard graphical object class, we need to improve the class in \MQL5\Include\DoEasy\Objects\Graph\Standard\GStdGraphObj.mqh.

First, include the files of the form object and newly created toolkit object of the extended standard graphical object to the file:

//+------------------------------------------------------------------+
//|                                                 GStdGraphObj.mqh |
//|                                  Copyright 2021, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2021, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "..\GBaseObj.mqh"
#include "..\..\..\Services\Properties.mqh"
#include "..\..\Graph\Form.mqh"
#include "..\..\Graph\Extend\CGStdGraphObjExtToolkit.mqh"
//+------------------------------------------------------------------+
//| Class of the dependent object pivot point data                   |
//+------------------------------------------------------------------+

In the private section of the class of the abstract standard graphical object, declare the pointer to the toolkit object of the extended standard graphical object:

//+------------------------------------------------------------------+
//| The class of the abstract standard graphical object              |
//+------------------------------------------------------------------+
class CGStdGraphObj : public CGBaseObj
  {
private:
   CArrayObj         m_list;                                            // List of subordinate graphical objects
   CProperties      *Prop;                                              // Pointer to the property object
   CLinkedPivotPoint m_linked_pivots;                                   // Linked pivot points
   CGStdGraphObjExtToolkit *ExtToolkit;                                 // Pointer to the extended graphical object toolkit
   int               m_pivots;                                          // Number of object reference points
//--- Read and set (1) the time and (2) the price of the specified object pivot point
   void              SetTimePivot(const int index);
   void              SetPricePivot(const int index);
//--- Read and set (1) color, (2) style, (3) width, (4) value, (5) text of the specified object level
   void              SetLevelColor(const int index);
   void              SetLevelStyle(const int index);
   void              SetLevelWidth(const int index);
   void              SetLevelValue(const int index);
   void              SetLevelText(const int index);
//--- Read and set the BMP file name for the "Bitmap Level" object. Index: 0 - ON, 1 - OFF
   void              SetBMPFile(const int index);

public:

In the public section, write the method returning the pointer to the toolkit object:

public:
//--- Set object's (1) integer, (2) real and (3) string properties
   void              SetProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index,long value)     { this.Prop.Curr.SetLong(property,index,value);    }
   void              SetProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index,double value)    { this.Prop.Curr.SetDouble(property,index,value);  }
   void              SetProperty(ENUM_GRAPH_OBJ_PROP_STRING property,int index,string value)    { this.Prop.Curr.SetString(property,index,value);  }
//--- Return object’s (1) integer, (2) real and (3) string property from the properties array
   long              GetProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index)          const { return this.Prop.Curr.GetLong(property,index);   }
   double            GetProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index)           const { return this.Prop.Curr.GetDouble(property,index); }
   string            GetProperty(ENUM_GRAPH_OBJ_PROP_STRING property,int index)           const { return this.Prop.Curr.GetString(property,index); }
//--- Set object's previous (1) integer, (2) real and (3) string properties
   void              SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index,long value) { this.Prop.Prev.SetLong(property,index,value);    }
   void              SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index,double value){ this.Prop.Prev.SetDouble(property,index,value);  }
   void              SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING property,int index,string value){ this.Prop.Prev.SetString(property,index,value);  }
//--- Return object’s (1) integer, (2) real and (3) string property from the previous properties array
   long              GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index)      const { return this.Prop.Prev.GetLong(property,index);   }
   double            GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index)       const { return this.Prop.Prev.GetDouble(property,index); }
   string            GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING property,int index)       const { return this.Prop.Prev.GetString(property,index); }
   
//--- Return (1) itself, (2) properties and (3) the change history
   CGStdGraphObj    *GetObject(void)                                       { return &this;            }
   CProperties      *Properties(void)                                      { return this.Prop;        }
   CChangeHistory   *History(void)                                         { return this.Prop.History;}
   CGStdGraphObjExtToolkit *GetExtToolkit(void)                            { return this.ExtToolkit;  }
//--- Return the flag of the object supporting this property
   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;             }

In the public section of the class, declare the handler of graphical object events. In the constructor, set the default value of NULL for the pointer to the toolkit object, while in the class destructor, check the pointer validity and first, remove all forms from the toolkit object and then delete the object itself:

private:
//--- Set the X coordinate (1) from the specified property of the base object to the specified subordinate object, (2) from the base object
   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);
//--- Set the Y coordinate (1) from the specified property of the base object to the specified subordinate object, (2) from the base object
   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);
//--- Set the (1) integer, (2) real and (3) string property to the specified subordinate property
   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:
//--- Event handler
   void              OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- Default constructor
                     CGStdGraphObj(){ this.m_type=OBJECT_DE_TYPE_GSTD_OBJ; this.m_species=WRONG_VALUE; this.ExtToolkit=NULL; }
//--- Destructor
                    ~CGStdGraphObj()
                       {
                        if(this.Prop!=NULL)
                           delete this.Prop;
                        if(this.ExtToolkit!=NULL)
                          {
                           this.ExtToolkit.DeleteAllControlPointForm();
                           delete this.ExtToolkit;
                          }
                       }
protected:
//--- Protected parametric constructor
                     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:
//+--------------------------------------------------------------------+ 
//|Methods of simplified access and setting graphical object properties|
//+--------------------------------------------------------------------+

In the block of methods for a simplified access and setting graphical object properties, write the method returning the number of graphical object pivot points:

public:
//+--------------------------------------------------------------------+ 
//|Methods of simplified access and setting graphical object properties|
//+--------------------------------------------------------------------+
//--- Number of object reference points
   int               Pivots(void)                  const { return this.m_pivots;                                                          }
//--- Object index in the list
   int               Number(void)                  const { return (int)this.GetProperty(GRAPH_OBJ_PROP_NUM,0);                            }
   void              SetNumber(const int number)         { this.SetProperty(GRAPH_OBJ_PROP_NUM,0,number);                                 }


In the protected parametric constructor, check the graphical element type. If this is an extended graphical object, create a new toolkit object and save the pointer to it in the ExtToolkit variable. At the end of the constructor listing, initialize the toolkit object:

//+------------------------------------------------------------------+
//| Protected parametric constructor                                 |
//+------------------------------------------------------------------+
CGStdGraphObj::CGStdGraphObj(const ENUM_OBJECT_DE_TYPE obj_type,
                             const ENUM_GRAPH_ELEMENT_TYPE elm_type,
                             const ENUM_GRAPH_OBJ_BELONG belong,
                             const ENUM_GRAPH_OBJ_SPECIES species,
                             const long chart_id,const int pivots,
                             const string name)
  {
//--- Create the property object with the default values
   this.Prop=new CProperties(GRAPH_OBJ_PROP_INTEGER_TOTAL,GRAPH_OBJ_PROP_DOUBLE_TOTAL,GRAPH_OBJ_PROP_STRING_TOTAL);
   this.ExtToolkit=(elm_type==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED ? new CGStdGraphObjExtToolkit() : NULL);
//--- Set the number of pivot points and object levels
   this.m_pivots=pivots;
   int levels=(int)::ObjectGetInteger(chart_id,name,OBJPROP_LEVELS);

//--- Set the property array dimensionalities according to the number of pivot points and levels
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_TIME,this.m_pivots);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_PRICE,this.m_pivots);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELCOLOR,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELSTYLE,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELWIDTH,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELVALUE,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELTEXT,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_BMPFILE,2);
   
//--- Set the object (1) type, type of graphical (2) object, (3) element, (4) subwindow affiliation and (5) index, as well as (6) chart symbol Digits
   this.m_type=obj_type;
   this.SetName(name);
   CGBaseObj::SetChartID(chart_id);
   CGBaseObj::SetTypeGraphObject(CGBaseObj::GraphObjectType(obj_type));
   CGBaseObj::SetTypeElement(elm_type);
   CGBaseObj::SetBelong(belong);
   CGBaseObj::SetSpecies(species);
   CGBaseObj::SetSubwindow(chart_id,name);
   CGBaseObj::SetDigits((int)::SymbolInfoInteger(::ChartSymbol(chart_id),SYMBOL_DIGITS));
   
//--- Save the integer properties inherent in all graphical objects but not present in the current one
   this.SetProperty(GRAPH_OBJ_PROP_CHART_ID,0,CGBaseObj::ChartID());                // Chart ID
   this.SetProperty(GRAPH_OBJ_PROP_WND_NUM,0,CGBaseObj::SubWindow());               // Chart subwindow index
   this.SetProperty(GRAPH_OBJ_PROP_TYPE,0,CGBaseObj::TypeGraphObject());            // Graphical object type (ENUM_OBJECT)
   this.SetProperty(GRAPH_OBJ_PROP_ELEMENT_TYPE,0,CGBaseObj::TypeGraphElement());   // Graphical element type (ENUM_GRAPH_ELEMENT_TYPE)
   this.SetProperty(GRAPH_OBJ_PROP_BELONG,0,CGBaseObj::Belong());                   // Graphical object affiliation
   this.SetProperty(GRAPH_OBJ_PROP_SPECIES,0,CGBaseObj::Species());                 // Graphical object species
   this.SetProperty(GRAPH_OBJ_PROP_GROUP,0,0);                                      // Graphical object group
   this.SetProperty(GRAPH_OBJ_PROP_ID,0,0);                                         // Object ID
   this.SetProperty(GRAPH_OBJ_PROP_BASE_ID,0,0);                                    // Base object ID
   this.SetProperty(GRAPH_OBJ_PROP_NUM,0,0);                                        // Object index in the list
   this.SetProperty(GRAPH_OBJ_PROP_CHANGE_HISTORY,0,false);                         // Flag of storing the change history
   this.SetProperty(GRAPH_OBJ_PROP_BASE_NAME,0,this.Name());                        // Base object name
   
//--- Save the properties inherent in all graphical objects and present in a graphical object
   this.PropertiesRefresh();
   
//--- Save basic properties in the parent object
   this.m_create_time=(datetime)this.GetProperty(GRAPH_OBJ_PROP_CREATETIME,0);
   this.m_back=(bool)this.GetProperty(GRAPH_OBJ_PROP_BACK,0);
   this.m_selected=(bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTED,0);
   this.m_selectable=(bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTABLE,0);
   this.m_hidden=(bool)this.GetProperty(GRAPH_OBJ_PROP_HIDDEN,0);

//--- Initialize the extended graphical object toolkit
   if(this.GraphElementType()==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)
     {
      datetime times[];
      double prices[];
      if(::ArrayResize(times,this.Pivots())!=this.Pivots())
         CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA);
      if(::ArrayResize(prices,this.Pivots())!=this.Pivots())
         CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA);
      for(int i=0;i<this.Pivots();i++)
        {
         times[i]=this.Time(i);
         prices[i]=this.Price(i);
        }
      this.ExtToolkit.SetBaseObj(this.TypeGraphObject(),this.Name(),this.ChartID(),this.SubWindow(),this.Pivots(),CTRL_FORM_SIZE,this.XDistance(),this.YDistance(),times,prices);
      this.ExtToolkit.CreateAllControlPointForm();
      this.SetFlagSelected(false,false);
      this.SetFlagSelectable(false,false);
     }

//--- Save the current properties to the previous ones
   this.PropertiesCopyToPrevData();
  }
//+-------------------------------------------------------------------+

When initializing the toolkit object, first declare the arrays of time and price properties, adjust their size depending on the number of graphical object pivot points and set price and time values from the object pivot points corresponding to the loop index.
Next, call the toolkit object initialization method and pass the necessary graphical object parameters to it together with newly filled price and time property arrays. After the initialization, call the method for creating form objects on the graphical object pivot points. Finally, set the status of the non-selected object for the graphical object and disable the ability to select it with a mouse.

In the method checking the object property changes, namely in the code block handling the extended standard graphical object, add the code block for moving control points (form objects) to the new screen coordinates when changing the location of pivot points of the extended standard graphical object:

//+------------------------------------------------------------------+
//| Check object property changes                                    |
//+------------------------------------------------------------------+
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 subordinate objects are attached to the base one (in a composite graphical object)
      if(this.m_list.Total()>0)
        {
         //--- In the loop by the number of added graphical objects,
         for(int i=0;i<this.m_list.Total();i++)
           {
            //--- get the next graphical object,
            CGStdGraphObj *dep=m_list.At(i);
            if(dep==NULL)
               continue;
            //--- get the data object of its pivot points,
            CLinkedPivotPoint *pp=dep.GetLinkedPivotPoint();
            if(pp==NULL)
               continue;
            //--- get the number of coordinate points the object is attached to
            int num=pp.GetNumLinkedCoords();
            //--- In the loop by the object coordinate points,
            for(int j=0;j<num;j++)
              {
               //--- get the number of coordinate points of the base object for setting the X coordinate
               int numx=pp.GetBasePivotsNumX(j);
               //--- In the loop by each coordinate point for setting the X coordinate,
               for(int nx=0;nx<numx;nx++)
                 {
                  //--- get the property for setting the X coordinate, its modifier
                  //--- and set it in the object selected as the current one in the main loop
                  int prop_from=pp.GetPropertyX(j,nx);
                  int modifier_from=pp.GetPropertyModifierX(j,nx);
                  this.SetCoordXToDependentObj(dep,prop_from,modifier_from,nx);
                 }
               //--- Get the number of coordinate points of the base object for setting the Y coordinate
               int numy=pp.GetBasePivotsNumY(j);
               //--- In the loop by each coordinate point for setting the Y coordinate,
               for(int ny=0;ny<numy;ny++)
                 {
                  //--- get the property for setting the Y coordinate, its modifier
                  //--- and set it in the object selected as the current one in the main loop
                  int prop_from=pp.GetPropertyY(j,ny);
                  int modifier_from=pp.GetPropertyModifierY(j,ny);
                  this.SetCoordYToDependentObj(dep,prop_from,modifier_from,ny);
                 }
              }
            dep.PropertiesCopyToPrevData();
           }
         //--- Move reference control points to new coordinates
         if(ExtToolkit!=NULL)
           {
            for(int i=0;i<this.Pivots();i++)
              {
               ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i);
              }
            ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
            long   lparam=0;
            double dparam=0;
            string sparam="";
            ExtToolkit.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
           }
         //--- Upon completion of the loop of handling all bound objects, redraw the chart to display all the changes
         ::ChartRedraw(m_chart_id);
        }
      //--- Save the current properties as the previous ones
      this.PropertiesCopyToPrevData();
     }
  }
//+------------------------------------------------------------------+

If we move one of the graphical object pivot points or the entire object, the screen coordinates of its pivot points change. This means we need to move the toolkit class form objects to the new screen coordinates as well. Therefore, first I pass the new graphical object coordinates to the toolkit object (in the loop by the number of pivot points for the price/time coordinates and separate coordinates in pixels). Then I call the handler of the toolkit object events by passing the ID of the chart change event to it. This will make the handler of the toolkit object events recalculate the screen coordinates of all forms and relocate them to a new place according to the new price and time coordinates of the graphical object.

In the method adding a subordinate standard graphical object to the list, fix a small error — a subordinate graphical object alters its own properties, therefore the new properties should be saved as the previous ones right away to avoid generating new events of changing these graphical objects when clicking on it:

//+------------------------------------------------------------------+
//| Add a subordinate standard graphical object to the list          |
//+------------------------------------------------------------------+
bool CGStdGraphObj::AddDependentObj(CGStdGraphObj *obj)
  {
   //--- If the current object is not an extended one, inform of that and return 'false'
   if(this.TypeGraphElement()!=GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)
     {
      CMessage::ToLog(MSG_GRAPH_OBJ_NOT_EXT_OBJ);
      return false;
     }
   //--- If failed to add the pointer to the passed object into the list, inform of that and return 'false'
   if(!this.m_list.Add(obj))
     {
      CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_ADD_DEP_EXT_OBJ_TO_LIST);
      return false;
     }
   //--- Object added to the list - set its number in the list,
   //--- name and ID of the current object as the base one,
   //--- set the flags of object availability and selection
   //--- and the graphical element type - standard extended graphical object
   obj.SetNumber(this.m_list.Total()-1);
   obj.SetBaseName(this.Name());
   obj.SetBaseObjectID(this.ObjectID());
   obj.SetFlagSelected(false,false);
   obj.SetFlagSelectable(false,false);
   obj.SetTypeElement(GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED);
   obj.PropertiesCopyToPrevData();
   return true;
  }
//+------------------------------------------------------------------+


The handler of the abstract standard graphical object events:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CGStdGraphObj::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(GraphElementType()!=GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)
      return;
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      if(ExtToolkit==NULL)
         return;
      for(int i=0;i<this.Pivots();i++)
        {
         ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i);
        }
      ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
      ExtToolkit.OnChartEvent(id,lparam,dparam,sparam);
     }
  }
//+------------------------------------------------------------------+

For now, the handler processes only the chart change event.

If the object is not extended, leave the handler. If the chart change event has been detected, check the validity of the pointer to the toolkit object of the extended standard graphical object. If no toolkit is created, exit. Next, in the loop by the number of the graphical object pivot points, set the new graphical object price/time coordinates to the toolkit object. Next, set its new screen coordinates and call the handler of the toolkit object events, in which all forms are relocated to the new screen coordinates based on the price/time coordinates newly passed to the toolkit object.


When removing an extended standard graphical object from the chart, we also need to remove the form objects of its toolkit object in case such an object was created for the graphical object. Removing graphical objects from the chart is handled in the collection class of graphical elements in \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh.

In the method handling the removal of extended graphical objects, add the code block for removing all form objects from the toolkit object:

//+------------------------------------------------------------------+
//| Handle the removal of extended graphical objects                 |
//+------------------------------------------------------------------+
void CGraphElementsCollection::DeleteExtendedObj(CGStdGraphObj *obj)
  {
   if(obj==NULL)
      return;
   //--- Save the ID of the graphical object chart and the number of subordinate objects in its list
   long chart_id=obj.ChartID();
   int total=obj.GetNumDependentObj();
   //--- If the list of subordinate objects is not empty (this is the base object)
   if(total>0)
     {
      CGStdGraphObjExtToolkit *toolkit=obj.GetExtToolkit();
      if(toolkit!=NULL)
        {
         toolkit.DeleteAllControlPointForm();
        }
      //--- In the loop, move along all dependent objects and remove them
      for(int n=total-1;n>WRONG_VALUE;n--)
        {
         //--- Get the next graphical object
         CGStdGraphObj *dep=obj.GetDependentObj(n);
         if(dep==NULL)
            continue;
         //--- If failed to remove it from the chart, display the appropriate message in the journal
         if(!::ObjectDelete(dep.ChartID(),dep.Name()))
            CMessage::ToLog(DFUN+dep.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART);
        }
      //--- Upon the loop completion, update the chart to display the changes and exit the method
      ::ChartRedraw(chart_id);
      return;
     }
   //--- If this is a subordinate object
   else if(obj.BaseObjectID()>0)
     {
      //--- Get the base object name and its ID
      string base_name=obj.BaseName();
      long base_id=obj.BaseObjectID();
      //--- Get the base object from the graphical object collection list
      CGStdGraphObj *base=GetStdGraphObject(base_name,chart_id);
      if(base==NULL)
         return;
      //--- get the number of dependent objects in its list
      int count=base.GetNumDependentObj();
      //--- In the loop, move along all its dependent objects and remove them
      for(int n=count-1;n>WRONG_VALUE;n--)
        {
         //--- Get the next graphical object
         CGStdGraphObj *dep=base.GetDependentObj(n);
         //--- If failed to get the pointer or the object has already been removed from the chart, move on to the next one
         if(dep==NULL || !this.IsPresentGraphObjOnChart(dep.ChartID(),dep.Name()))
            continue;
         //--- If failed to delete the graphical object from the chart,
         //--- display the appropriate message in the journal and move on to the next one
         if(!::ObjectDelete(dep.ChartID(),dep.Name()))
           {
            CMessage::ToLog(DFUN+dep.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART);
            continue;
           }
        }
      //--- Remove the base object from the chart and from the list
      if(!::ObjectDelete(base.ChartID(),base.Name()))
         CMessage::ToLog(DFUN+base.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART);
     }
   //--- Update the chart for displaying the changes
   ::ChartRedraw(chart_id);
  }
//+------------------------------------------------------------------+

Here we simply retrieve the pointer to the toolkit object of the extended graphical object. If the pointer is valid, call the method for removing all created forms of the toolkit object of the extended standard graphical object I have previously considered.

In the handler of the graphical element collection class events, add handling the chart changes for extended standard graphical objects:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CGraphElementsCollection::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   CGStdGraphObj *obj=NULL;
   ushort idx=ushort(id-CHARTEVENT_CUSTOM);
   if(id==CHARTEVENT_OBJECT_CHANGE  || id==CHARTEVENT_OBJECT_DRAG    || id==CHARTEVENT_OBJECT_CLICK   ||
      idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG   || idx==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Calculate the chart ID
      //--- If the event ID corresponds to an event from the current chart, the chart ID is received from ChartID
      //--- If the event ID corresponds to a user event, the chart ID is received from lparam
      //--- Otherwise, the chart ID is assigned to -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);
      //--- Get the object, whose properties were changed or which was relocated,
      //--- from the collection list by its name set in sparam
      obj=this.GetStdGraphObject(sparam,chart_id);
      //--- If failed to get the object by its name, it is not on the list,
      //--- which means its name has been changed
      if(obj==NULL)
        {
         //--- Let's search the list for the object that is not on the chart
         obj=this.FindMissingObj(chart_id);
         //--- If failed to find the object here as well, exit
         if(obj==NULL)
            return;
         //--- Get the name of the renamed graphical object on the chart, which is not in the collection list
         string name_new=this.FindExtraObj(chart_id);
         //--- set a new name for the collection list object, which does not correspond to any graphical object on the chart,
         //--- and send an event with the new name of the object to the control program chart
         if(obj.SetNamePrev(obj.Name()) && obj.SetName(name_new))
            ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_RENAME,obj.ChartID(),obj.TimeCreate(),obj.Name());
        }
      //--- Update the properties of the obtained object
      //--- and check their change
      obj.PropertiesRefresh();
      obj.PropertiesCheckChanged();
     }
//--- Handle chart changes for extended standard objects
   if(id==CHARTEVENT_CHART_CHANGE || idx==CHARTEVENT_CHART_CHANGE)
     {
      CArrayObj *list=this.GetListStdGraphObjectExt();
      if(list!=NULL)
        {
         for(int i=0;i<list.Total();i++)
           {
            obj=list.At(i);
            if(obj==NULL)
               continue;
            obj.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
           }
        }
     }
  }
//+------------------------------------------------------------------+

Here, if the chart change event has been detected, get the list of all extended standard graphical objects. In the loop by their number, get the next object, call its event handler and pass the "Chart changed" event value to it.


Test

To perform the test, I will use the EA from the previous article and save it in \MQL5\Experts\TestDoEasy\Part95\ as TestDoEasyPart95.mq5.

The only changes are slightly different names of price label objects attached to the base trend line object. Subordinate object names in the block of creating a composite graphical object of the EA event handler now feature the "Ext" affix so that the names correspond to the extended graphical object type:

   if(id==CHARTEVENT_CLICK)
     {
      if(!IsCtrlKeyPressed())
         return;
      //--- Get the chart click coordinates
      datetime time=0;
      double price=0;
      int sw=0;
      if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,sw,time,price))
        {
         //--- Get the right point coordinates for a trend line
         datetime time2=iTime(Symbol(),PERIOD_CURRENT,1);
         double price2=iOpen(Symbol(),PERIOD_CURRENT,1);
         
         //--- Create the "Trend line" object
         string name_base="TrendLineExt";
         engine.CreateLineTrend(name_base,0,true,time,price,time2,price2);
         //--- Get the object from the list of graphical objects by chart name and ID and pass its properties to the journal
         CGStdGraphObj *obj=engine.GraphGetStdGraphObjectExt(name_base,ChartID());
         
         //--- Create the "Left price label" object
         string name_dep="PriceLeftExt";
         engine.CreatePriceLabelLeft(name_dep,0,false,time,price);
         //--- Get the object from the list of graphical objects by chart name and ID and
         CGStdGraphObj *dep=engine.GraphGetStdGraphObject(name_dep,ChartID());
         //--- add it to the list of graphical objects bound to the "Trend line" object
         obj.AddDependentObj(dep);
         //--- Set its pivot point by X and Y axis to the trend line left point
         dep.AddNewLinkedCoord(GRAPH_OBJ_PROP_TIME,0,GRAPH_OBJ_PROP_PRICE,0);
         
         //--- Create the "Right price label" object
         name_dep="PriceRightExt";
         engine.CreatePriceLabelRight(name_dep,0,false,time2,price2);
         //--- Get the object from the list of graphical objects by chart name and ID and
         dep=engine.GraphGetStdGraphObject(name_dep,ChartID());
         //--- add it to the list of graphical objects bound to the "Trend line" object
         obj.AddDependentObj(dep);
         //--- Set its pivot point by X and Y axis to the trend line right point
         dep.AddNewLinkedCoord(GRAPH_OBJ_PROP_TIME,1,GRAPH_OBJ_PROP_PRICE,1);
        }
     }
   engine.GetGraphicObjCollection().OnChartEvent(id,lparam,dparam,sparam);

Let's create a composite graphical object. The form objects are to be set to its pivot points during the creation.
These form objects have the coordinates in pixels from the upper left screen corner. Thus, if we move the chart, these screen coordinates should be recalculated so that the objects are set on the appropriate graphical object pivot points. This is what we are going to check.

Compile the EA and launch it on the chart:


We can see that the objects take their designated places when the chart changes. However, this happens with a lag.
When removing a graphical object, the appropriate form objects are removed as well.

What can we do with the lag? In fact, we do not need to see the movements live — these moves will always remain hidden when moving the chart (they are displayed now to handle an event response). The graphical object line itself is moved when the form objects are dragged by a mouse. Any interaction with the forms is performed on a fixed chart. So this result is quite sufficient, especially considering that the chart is updated only after the loop completion rather than at each loop iteration. In order to reduce the load, we can control the chart change completion displaying the changes and showing the object afterwards (this is only possible if the cursor hovers over the form object active area when it should become visible).

What's next?

In the next article, I will continue my work on composite graphical object events.

All files of the current library version, test EA and chart event control indicator for MQL5 are attached below for you to test and download. Leave your questions, comments and suggestions in the comments.

Back to contents

*Previous articles within the series:

Graphics in DoEasy library (Part 93): Preparing functionality for creating composite graphical objects
Graphics in DoEasy library (Part 94): Moving and deleting composite graphical objects

Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/10387

Attached files |
MQL5.zip (4207.98 KB)
Learn how to design a trading system by Momentum Learn how to design a trading system by Momentum
In my previous article, I mentioned the importance of identifying the trend which is the direction of prices. In this article I will share one of the most important concepts and indicators which is the Momentum indicator. I will share how to design a trading system based on this Momentum indicator.
Data Science and Machine Learning (Part 01): Linear Regression Data Science and Machine Learning (Part 01): Linear Regression
It's time for us as traders to train our systems and ourselves to make decisions based on what number says. Not on our eyes, and what our guts make us believe, this is where the world is heading so, let us move perpendicular to the direction of the wave.
MVC design pattern and its application (Part 2): Diagram of interaction between the three components MVC design pattern and its application (Part 2): Diagram of interaction between the three components
This article is a continuation and completion of the topic discussed in the previous article: the MVC pattern in MQL programs. In this article, we will consider a diagram of possible interaction between the three components of the pattern.
Graphics in DoEasy library (Part 94): Moving and deleting composite graphical objects Graphics in DoEasy library (Part 94): Moving and deleting composite graphical objects
In this article, I will start the development of various composite graphical object events. We will also partially consider moving and deleting a composite graphical object. In fact, here I am going to fine-tune the things I implemented in the previous article.