Graphics in DoEasy library (Part 99): Moving an extended graphical object using a single control point

12 May 2022, 15:51
Artyom Trishkin
0
1 893

Contents


Concept

In the previous article, I implemented the ability to move pivot points of an extended graphical object using control forms. However, I do not yet have the functionality to move such graphical object as a whole. Any standard graphical object can be moved entirely when moving its central point. Similarly, here I will make a single central point for managing a graphical object to be able to move the entire graphical object (rather than its pivot points) by moving the point. To perform the test, I have selected a composite graphical object consisting of a trend line featuring price label objects at its ends. With this in mind, the entire work will be done for graphical objects having two pivot points for relocating its ends and a single central point for moving the entire graphical object (two points for modifying object ends and a central one for moving it). Later, I will create the same forms with control points for graphical objects having more than three control points.

In addition, I will slightly optimize the code for calculating screen coordinates of the graphical object pivot points in terms of dividing it into separate methods to simplify the understanding of the basic logic. After all, it is much easier to read the code featuring the call of the method returning a certain value (while, in turn, there is another method inside, which calculates something as well) rather than set the entire code of such methods in the main calculation block making it bulky and difficult to understand.

Not all things implemented here will always work as intended. But the objective of the article is to describe the process of developing and creating the code to obtain the necessary result. I believe, it is much more interesting to go almost all the way from planning the functionality to its implementation than just reading a dry presentation about "how everything turned out in the end".

Since the function of getting the ChartTimePriceToXY() screen coordinates returns the coordinates of the visible part of the chart only, we will not be able to calculate the screen coordinates of the line point outside the chart limits. The function will always return 0 if we request the X coordinate in screen time pixels located beyond the left part of the visible chart. Therefore, when moving a composite graphical object along the screen and when its left part goes beyond the left border of the screen, the left pivot point of the object will remain at coordinate 0 in chart pixels. This will distort the graphical object. The same applies to the right side of the graphical object and the right side of the chart screen (as well as the top and bottom ones). Therefore, I will introduce a limitation for a composite graphical object restricting its moving beyond the visible part of the chart. This will prevent the distortion of the graphical object in case any of its sides "bumps into" the screen edge when being moved.


Improving library classes

Since the object form used to display a control point for managing pivot points of an extended graphical object is an important object among the library objects but the forms are not included into the graphical object collection, we need to define a new type for such forms. All essential library objects have their own library object type names allowing us to define which object is currently active. Let's define the type for the form objects meant for managing pivot points within extended graphical objects of the library.

In \MQL5\Include\DoEasy\Defines.mqh, add the new type to the enumeration of library object types:

//+------------------------------------------------------------------+
//| List of library object types                                     |
//+------------------------------------------------------------------+
enum ENUM_OBJECT_DE_TYPE
  {
//--- Graphics
   OBJECT_DE_TYPE_GBASE =  COLLECTION_ID_LIST_END+1,              // "Base object of all library graphical objects" object type
   OBJECT_DE_TYPE_GELEMENT,                                       // "Graphical element" object type
   OBJECT_DE_TYPE_GFORM,                                          // Form object type
   OBJECT_DE_TYPE_GFORM_CONTROL,                                  // "Form for managing pivot points of graphical object" object type
   OBJECT_DE_TYPE_GSHADOW,                                        // Shadow object type
//--- Animation
   OBJECT_DE_TYPE_GFRAME,                                         // "Single animation frame" object type
   OBJECT_DE_TYPE_GFRAME_TEXT,                                    // "Single text animation frame" object type
   OBJECT_DE_TYPE_GFRAME_QUAD,                                    // "Single rectangular animation frame" object type
   OBJECT_DE_TYPE_GFRAME_GEOMETRY,                                // "Single geometric animation frame" object type
   OBJECT_DE_TYPE_GANIMATIONS,                                    // "Animations" object type
//--- Managing graphical objects
   ...
   ...
   ...
  }


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

   MSG_LIB_SYS_REQUEST_OUTSIDE_LONG_ARRAY,            // Request outside long array
   MSG_LIB_SYS_REQUEST_OUTSIDE_DOUBLE_ARRAY,          // Request outside double array
   MSG_LIB_SYS_REQUEST_OUTSIDE_STRING_ARRAY,          // Request outside string array
   MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY,                 // Request outside the array
   MSG_LIB_SYS_FAILED_CONV_GRAPH_OBJ_COORDS_TO_XY,    // Failed to convert graphical object coordinates to screen ones
   MSG_LIB_SYS_FAILED_CONV_TIMEPRICE_COORDS_TO_XY,    // Failed to convert time/price coordinates to screen ones

and text messages corresponding to the newly added indices:

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


If an error of converting time/price coordinates to screen coordinates is detected when developing the functionality for moving graphical objects, such error is reported to exclude the chain from the logic error search.

The ChartTimePriceToXY() function, which may potentially cause the coordinates conversion error, is also used in the chart window object class in \MQL5\Include\DoEasy\Objects\Chart\ChartWnd.mqh. Let's add the display of the coordinates conversion error message in the journal to the TimePriceToXY() method:

//+------------------------------------------------------------------+
//| Convert chart coordinates from the time/price representation     |
//| to X and Y coordinates                                           |
//+------------------------------------------------------------------+
bool CChartWnd::TimePriceToXY(const datetime time,const double price)
  {
   ::ResetLastError();
   if(!::ChartTimePriceToXY(this.m_chart_id,this.WindowNum(),time,price,this.m_wnd_coord_x,this.m_wnd_coord_y))
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_CONV_TIMEPRICE_COORDS_TO_XY);
      CMessage::ToLog(DFUN,::GetLastError(),true);
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

First, display the "Failed to convert time/price coordinates to screen ones" message followed by the error description together with the error code.


Since I now have declared the new library object type for control points for managing pivot points of an extended standard graphical object, I need to create the class of such object inherited from the form object class. Within the class, I am going to add some variables and methods for simplifying handling such objects.

Set it in the toolkit of an extended standard graphical object in \MQL5\Include\DoEasy\Objects\Graph\Extend\CGStdGraphObjExtToolkit.mqh:

//+------------------------------------------------------------------+
//|                                      CGStdGraphObjExtToolkit.mqh |
//|                                  Copyright 2022, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, 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"
//+------------------------------------------------------------------+
//| Class of the form for managing pivot points of a graphical object|
//+------------------------------------------------------------------+
class CFormControl : public CForm
  {
private:
   bool              m_drawn;                   // Flag indicating that the pivot point is drawn on the form
   int               m_pivot_point;             // Pivot point managed by the form
public:
//--- (1) Return and (2) set the drawn point flag
   bool              IsControlAlreadyDrawn(void)               const { return this.m_drawn;                       }
   void              SetControlPointDrawnFlag(const bool flag)       { this.m_drawn=flag;                         }
//--- (1) Return and (2) set the pivot point managed by the form
   int               GraphObjPivotPoint(void)                  const { return this.m_pivot_point;                 }
   void              SetGraphObjPivotPoint(const int index)          { this.m_pivot_point=index;                  }
//--- Constructor
                     CFormControl(void)                              { this.m_type=OBJECT_DE_TYPE_GFORM_CONTROL;  }
                     CFormControl(const long chart_id,const int subwindow,const string name,const int pivot_point,const int x,const int y,const int w,const int h) :
                        CForm(chart_id,subwindow,name,x,y,w,h)
                          {
                           this.m_type=OBJECT_DE_TYPE_GFORM_CONTROL;
                           this.m_pivot_point=pivot_point;
                          }
  };
//+------------------------------------------------------------------+
//| Extended standard graphical                                      |
//| object toolkit class                                             |
//+------------------------------------------------------------------+
class CGStdGraphObjExtToolkit : public CObject

The m_drawn private class member variable is to store the flag informing that the point has already been drawn on the form. Why do we need such a variable? If the mouse cursor is removed from the active zone of the control form for managing the control points of a graphical object, then we need to delete the point drawn on the form. Currently, the drawn point is constantly removed on all such forms if the mouse cursor is not hovering over the active area of the form. Why should we burden the system with constantly redrawing all such forms if we are first able to look at the form redraw flag? The flag will inform us of the point being drawn or removed. In the future, I am going to develop some visual effects for drawing such points (in addition to some other things), so it is better to have the flag that is set immediately after running the visual effect handler, rather than try to define that the drawing has already been completed.

The m_pivot_point private class member variable is to store the pivot point index managed by the form. A graphical object has several control points. For example, a trend line has three points — two ones at the end of the lines for independent changing of the line ends location and a central point for moving the entire object. The indices stored in form objects will correspond to the indices of pivot points of the line: 0 and 1 — for the points along the line edges and 2 — for the central one. Other graphical objects may have completely different control points but all indices are to correspond to object pivot points + an additional one (although not in all cases, this will be discussed in subsequent articles) for moving the entire object.

The public class methods are used to set/return the values of the variables described above. The class also has two constructors. In the default constructor, the new OBJECT_DE_TYPE_GFORM_CONTROL type added in the current article is set in the object type.
The parametric constructor passes all the values passed to the parent class constructor plus an additional variable — index of the graphical object pivot point managed by the created form.

Now all pivot point control forms in the CGStdGraphObjExtToolkit class will be of CFormControl type, therefore we need to replace the type of the CForm form object with CFormControl and add the new methods for handling the control forms for managing graphical object pivot points:

//+------------------------------------------------------------------+
//| 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
   CFormControl     *CreateNewControlPointForm(const int index);
//--- Return X and Y (1) screen coordinates of the specified reference point of the graphical object
   bool              GetControlPointCoordXY(const int index,int &x,int &y);
//--- Set the parameters of a form object for managing pivot points
   void              SetControlFormParams(CFormControl *form,const int index);
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
   CFormControl     *GetControlPointForm(const int index)                     { return this.m_list_forms.At(index);           }
   CFormControl     *GetControlPointForm(const string name,int &index);
//--- Return the number of (1) base object pivot points and (2) newly created form objects for managing control points
   int               GetNumPivotsBaseObj(void)                          const { return this.m_base_pivots;                    }
   int               GetNumControlPointForms(void)                      const { return this.m_list_forms.Total();             }
//--- Create form objects on the base object pivot points
   bool              CreateAllControlPointForm(void);
//--- (1) Draw a control point on the form, (2) draw a control point on the form and delete it on all other forms
   void              DrawControlPoint(CFormControl *form,const uchar opacity,const color clr);
   void              DrawOneControlPoint(CFormControl *form,const uchar opacity=255,const color clr=CTRL_POINT_COLOR);
//--- (1) Draw using a default color, (remove) a control point on the form
   void              DrawControlPoint(CFormControl *form)                     { this.DrawControlPoint(form,255,CTRL_POINT_COLOR);}
   void              ClearControlPoint(CFormControl *form)                    { this.DrawControlPoint(form,0,CTRL_POINT_COLOR);  }
//--- 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(){;}
  };
//+------------------------------------------------------------------+


Improve the GetControlPointCoordXY() method returning the X and Y coordinates of the specified pivot point of the graphical object in screen coordinates.

Previously, the method simply returned the calculated coordinates of the specified graphical object pivot point. Now we need to take into account that graphical objects may have a different number of pivot points and a different location of the central pivot point. So, let's make the calculation for different types of objects in switch(). In addition, we should consider the coordinates of which pivot point we want to get — one of those located along the edges of the object or a central one. If the index of the pivot point passed to the method is less than the total number of graphical object pivot points, the pivot point coordinates are requested. Otherwise, the coordinates of the central pivot point are requested.

For now, I will implement receiving X and Y coordinates only for the graphical objects having two pivot points at the edges and a single central point:

//+------------------------------------------------------------------+
//| 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)
  {
//--- Declare form objects, from which we are to receive their screen coordinates
   CFormControl *form0=NULL, *form1=NULL;
//--- Set X and Y to zero - these values will be received in case of a failure
   x=0;
   y=0;
//--- Depending on the graphical object type
   switch(this.m_base_type)
     {
      //--- Objects drawn using screen coordinates
      case OBJ_LABEL             :
      case OBJ_BUTTON            :
      case OBJ_BITMAP_LABEL      :
      case OBJ_EDIT              :
      case OBJ_RECTANGLE_LABEL   :
      case OBJ_CHART             :
      case OBJ_EVENT             :
        //--- Write object screen coordinates and return 'true'
        x=this.m_base_x;
        y=this.m_base_y;
        return true;
      
      //--- Lines (vertical and horizontal)
      case OBJ_VLINE             : break;
      case OBJ_HLINE             : break;
      //--- Lines
      case OBJ_TREND             :
      case OBJ_TRENDBYANGLE      :
      case OBJ_CYCLES            :
      case OBJ_ARROWED_LINE      :
      //--- Channels
      case OBJ_CHANNEL           :
      case OBJ_STDDEVCHANNEL     :
      case OBJ_REGRESSION        :
      //--- Gann
      case OBJ_GANNLINE          :
      case OBJ_GANNGRID          :
      //--- Fibo
      case OBJ_FIBO              :
      case OBJ_FIBOTIMES         :
      case OBJ_FIBOFAN           :
      case OBJ_FIBOARC           :
      case OBJ_FIBOCHANNEL       :
      case OBJ_EXPANSION         :
        //--- Calculate coordinates for forms on the line pivot points
        if(index<this.m_base_pivots)
           return(::ChartTimePriceToXY(this.m_base_chart_id,this.m_base_subwindow,this.m_base_time[index],this.m_base_price[index],x,y) ? true : false);
        //--- Calculate the coordinates for the central form located between the line pivot points
        else
          {
           form0=this.GetControlPointForm(0);
           form1=this.GetControlPointForm(1);
           if(form0==NULL || form1==NULL)
              return false;
           x=(form0.CoordX()+this.m_shift+form1.CoordX()+this.m_shift)/2;
           y=(form0.CoordY()+this.m_shift+form1.CoordY()+this.m_shift)/2;
           return true;
          }

      //--- Channels
      case OBJ_PITCHFORK         : break;
      //--- Gann
      case OBJ_GANNFAN           : break;
      //--- Elliott
      case OBJ_ELLIOTWAVE5       : break;
      case OBJ_ELLIOTWAVE3       : break;
      //--- Shapes
      case OBJ_RECTANGLE         : break;
      case OBJ_TRIANGLE          : break;
      case OBJ_ELLIPSE           : break;
      //--- Arrows
      case OBJ_ARROW_THUMB_UP    : break;
      case OBJ_ARROW_THUMB_DOWN  : break;
      case OBJ_ARROW_UP          : break;
      case OBJ_ARROW_DOWN        : break;
      case OBJ_ARROW_STOP        : break;
      case OBJ_ARROW_CHECK       : break;
      case OBJ_ARROW_LEFT_PRICE  : break;
      case OBJ_ARROW_RIGHT_PRICE : break;
      case OBJ_ARROW_BUY         : break;
      case OBJ_ARROW_SELL        : break;
      case OBJ_ARROW             : break;
      //--- Graphical objects with time/price coordinates
      case OBJ_TEXT              : break;
      case OBJ_BITMAP            : break;
      //---
      default                    : break;
     }
   return false;
  }
//+------------------------------------------------------------------+

The pivot point calculation is performed using the values set in the m_base_time and m_base_price arrays of the line pivot point coordinates. In order to calculate the central point coordinates, use the coordinates of form objects attached to pivot points at the line edges. In case of a successful coordinate calculation, the method immediately returns true. In all other cases, it either returns false or applies break to stop the code execution in case of switch and move to the end of the method where false is returned.

In the method returning the pointer to the pivot point form by name, replace CForm with CFormControl:

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

In the method creating a form object on a base object pivot point, replace CForm with CFormControl and set the parameters for a successfully created form object:

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


In the method creating form objects on the base object pivot points, replace CForm with CFormControl and remove the strings for setting the parameters of a created form object since the parameters are now set immediately when creating the object in the method considered above:

//+------------------------------------------------------------------+
//| 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
      CFormControl *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;
        }
     }
//--- Redraw the chart for displaying changes (if successful) and return the final result
   if(res)
      ::ChartRedraw(this.m_base_chart_id);
   return res;
  }
//+------------------------------------------------------------------+

Now the loop runs by the number of base object pivot points plus an additional one. In other words, the number of created forms exceeds the number of pivot points the graphical object has by one. The last form is a central one and is meant for moving the entire graphical object.

The method setting the parameters of a form object for managing pivot points:

//+------------------------------------------------------------------+
//| Set the parameters of a form object for managing pivot points    |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::SetControlFormParams(CFormControl *form,const int index)
  {
   form.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);                // Object is created programmatically
   form.SetActive(true);                                    // Form object is active
   form.SetMovable(true);                                   // Movable object
   int x=(int)::floor((form.Width()-CTRL_POINT_RADIUS*2)/2);// Shift the active area from the form edge
   form.SetActiveAreaShift(x,x,x,x);                        // Object active area is located at the center of the form, its size is equal to two CTRL_POINT_RADIUS values
   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.DrawRectangle(x,x,form.Width()-x-1,form.Height()-x-1,clrSilver);// Draw an outlining rectangle for visual display of the form active area location
   form.SetID(index+1);                                     // Set the form ID
   form.SetControlPointDrawnFlag(false);                    // Set the flag that the pivot point is not drawn on the form
   form.Done();                                             // Save the initial form object state (its appearance)
  }
//+------------------------------------------------------------------+

Here we have the strings of the code transferred from the method considered above. Besides, there is a flag of a point drawn on the form and of the form ID.

In the method drawing the control point on the form, relocate the form center calculation to a separate string to avoid duplicating calculations. Upon the method completion, set the flag of a drawn point on the form:

//+------------------------------------------------------------------+
//| Draw a control point on the form                                 |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::DrawControlPoint(CFormControl *form,const uchar opacity,const color clr)
  {
   if(form==NULL)
      return;
   int c=int(::floor(form.Width()/2));                      // Form center (coordinates)
   form.DrawCircle(c,c,CTRL_POINT_RADIUS,clr,opacity);      // Draw a circle in the form center
   form.DrawCircleFill(c,c,2,clr,opacity);                  // Draw a circle in the form center
   form.SetControlPointDrawnFlag(opacity>0 ? true : false); // Set the flag that the pivot point is drawn on the form
  }
//+------------------------------------------------------------------+


Currently, if we hover the mouse over the form for managing a graphical object pivot point, a point appears on it. The point is removed only after the cursor leaves the form. But if we bring all the control points of the object closer so that the forms built at the ends of the graphical object and the central form begin to overlap each other, then moving the cursor away from one form causes the cursor to move to another form located nearby. Thus, we can make so that all points on all object forms are displayed:

If we grab the form and start moving it, the object pivot point starts moving as well. Forms that are visible due to an error remain where they were before the relocation started. This behavior is incorrect. Therefore, we need the method drawing a point on one graphical object form object and deleting points on other form objects of the same object simultaneously.

The method drawing a control point on the form and removing them on all the remaining forms:

//+------------------------------------------------------------------+
//| Draw a control point on the form,                                |
//| remove it on all other forms                                     |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::DrawOneControlPoint(CFormControl *form,const uchar opacity=255,const color clr=CTRL_POINT_COLOR)
  {
   this.DrawControlPoint(form,opacity,clr);
   for(int i=0;i<this.GetNumControlPointForms();i++)
     {
      CFormControl *ctrl=this.GetControlPointForm(i);
      if(ctrl==NULL || ctrl.ID()==form.ID())
         continue;
      this.ClearControlPoint(ctrl);
     }
  }
//+------------------------------------------------------------------+

Here the method receives the pointer to the form the cursor is hovering over. Draw the point on the form. Then, in the loop by all object forms, select the form and, if the form has not been passed to the method, delete the point from it.

In the event handler, replace the CForm form type with CFormControl:

//+------------------------------------------------------------------+
//| 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++)
        {
         CFormControl *form=this.m_list_forms.At(i);
         if(form==NULL)
            continue;
         int x=0, y=0;
         if(!this.GetControlPointCoordXY(i,x,y))
            continue;
         form.SetCoordX(x-this.m_shift);
         form.SetCoordY(y-this.m_shift);
         form.Update();
        }
      ::ChartRedraw(this.m_base_chart_id);
     }
  }
//+------------------------------------------------------------------+


Let's make some improvements for the method code optimization in the class of the abstract standard graphical object in \MQL5\Include\DoEasy\Objects\Graph\Standard\GStdGraphObj.mqh. We have similar code fragments in different methods, so it makes sense to implement such code blocks as separate methods and call them where needed making the code easier to read.

In the public and private class sections, declare new methods that are to contain the repeating code fragments:

//--- Return (1) the list of dependent objects, (2) dependent graphical object by index and (3) the number of dependent objects
   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();    }
//--- Return the name of the dependent object by index
   string            NameDependent(const int index);
//--- Add the dependent graphical object to the list
   bool              AddDependentObj(CGStdGraphObj *obj);
//--- Change X and Y coordinates of the current and all dependent objects
   bool              ChangeCoordsExtendedObj(const int x,const int y,const int modifier,bool redraw=false);
//--- Set X and Y coordinates into the appropriate pivot points of a specified subordinate object
   bool              SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj);
   
//--- Return the pivot point data object
   CLinkedPivotPoint*GetLinkedPivotPoint(void)        { return &this.m_linked_pivots;  }

...

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 X and Y coordinates into the appropriate pivot point of a specified subordinate object
   void              SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj,CLinkedPivotPoint *pivot_point,const int index);
//--- 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


In the method checking the changes in the object properties, remove the specified code block (the code will be moved to a separate method):

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

Add calling a new method instead of a removed block:

      //--- 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;
            //--- Set X and Y coordinates to all pivot points of a subordinate object and
            //--- save the current properties of a subordinate graphical object as the previous ones
            if(this.SetCoordsXYtoDependentObj(dep))
               dep.PropertiesCopyToPrevData();
           }
         //--- Move reference control points to new coordinates
         if(this.ExtToolkit!=NULL)
           {
            for(int i=0;i<this.Pivots();i++)
              {
               this.ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i);
              }
            this.ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
            long   lparam=0;
            double dparam=0;
            string sparam="";
            this.ExtToolkit.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
           }
         //--- Upon completion of the loop of handling all bound objects, redraw the chart to display all the changes
         ::ChartRedraw(m_chart_id);
        }


Since the current point deletion logic implies that if the cursor is not set on any of the forms, each such form is constantly redrawn (which is suboptimal and resource-intensive), let's implement the check verifying that the point deletion form should be redrawn only if the point should actually be deleted and it is still present in the method redrawing the form for managing a control point of an extended standard graphical object. Also, I am going to replace the form object type with the new one:

//+------------------------------------------------------------------+
//| Redraw the form for managing a control point                     |
//| of an extended standard graphical object                         |
//+------------------------------------------------------------------+
void CGStdGraphObj::RedrawControlPointForms(const uchar opacity,const color clr)
  {
//--- Leave if the object has no toolkit of an extended standard graphical object
   if(this.ExtToolkit==NULL)
      return;
//--- Get the number of pivot point management forms
   int total_form=this.GetNumControlPointForms();
//--- In the loop by the number of pivot point management forms
   for(int i=0;i<total_form;i++)
     {
      //--- get the next form object
      CFormControl *form=this.ExtToolkit.GetControlPointForm(i);
      if(form==NULL)
         continue;
      //--- Draw a point and a circle with a specified non-transparency and color
      //--- If a point should be completely transparent (deleted)
      //--- and the form still has the point, delete the point,
      if(opacity==0 && form.IsControlAlreadyDrawn())
         this.ExtToolkit.DrawControlPoint(form,0,clr);
      //--- otherwise, draw the point with a specified non-transparency and color
      else
         this.ExtToolkit.DrawControlPoint(form,opacity,clr);
     }
   
//--- Get the total number of bound graphical objects
   int total_dep=this.GetNumDependentObj();
//--- In the loop by all bound graphical objects,
   for(int i=0;i<total_dep;i++)
     {
      //--- get the next graphical object from the list
      CGStdGraphObj *dep=this.GetDependentObj(i);
      if(dep==NULL)
         continue;
      //--- call the method for it
      dep.RedrawControlPointForms(opacity,clr);
     }
  }
//+------------------------------------------------------------------+

The point deletion is now performed only if it should actually be deleted (point non-transparency is set to zero) and if the point is still present (drawn point flag is set).

Also, let's rework the method changing the X and Y coordinates of the current and all dependent objects by deleting the code segments to be replaced with calling the new method:

//+----------------------------------------------------------------------+
//| Change X and Y coordinates of the current and all dependent objects  |
//+----------------------------------------------------------------------+
bool CGStdGraphObj::ChangeCoordsExtendedObj(const int x,const int y,const int modifier,bool redraw=false)
  {
//--- Set new coordinates for the pivot point specified in 'modifier'
   if(!this.SetTimePrice(x,y,modifier))
      return false;
//--- If the object is not a composite graphical object
//--- or if subordinate graphical objects are not attached to the object,
//--- there is nothing else to do here, return 'true'
   if(this.ExtToolkit==NULL || this.m_list.Total()==0)
      return true;
//--- Get the graphical object bound to the 'modifier' point
   CGStdGraphObj *dep=this.GetDependentObj(modifier);
   if(dep==NULL)
      return false;
//--- Get the object of pivot point data of the bound graphical object
   CLinkedPivotPoint *pp=dep.GetLinkedPivotPoint();
   if(pp==NULL)
      return false;
//--- 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 to the subordinate graphical object attached to the 'modifier' point
         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 to the subordinate graphical object attached to the 'modifier' point
         int prop_from=pp.GetPropertyY(j,ny);
         int modifier_from=pp.GetPropertyModifierY(j,ny);
         this.SetCoordYToDependentObj(dep,prop_from,modifier_from,ny);
        }
     }
//--- Save the current properties of a subordinate graphical object as the previous ones
   dep.PropertiesCopyToPrevData();
//--- Move a reference control point to new coordinates
   this.ExtToolkit.SetBaseObjTimePrice(this.Time(modifier),this.Price(modifier),modifier);
   this.ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
//--- If the flag is active, redraw the chart
   if(redraw)
      ::ChartRedraw(m_chart_id);
//--- All is successful - return 'true'
   return true;
  }
//+------------------------------------------------------------------+

Now the method is much simpler:

//+----------------------------------------------------------------------+
//| Change X and Y coordinates of the current and all dependent objects  |
//+----------------------------------------------------------------------+
bool CGStdGraphObj::ChangeCoordsExtendedObj(const int x,const int y,const int modifier,bool redraw=false)
  {
//--- Set new coordinates for the pivot point specified in 'modifier'
   if(!this.SetTimePrice(x,y,modifier))
      return false;
//--- If the object is a composite graphical object,
//--- and subordinate graphical objects are attached to the object
   if(this.ExtToolkit!=NULL && this.m_list.Total()>0)
     {
      //--- Get the graphical object bound to the 'modifier' point
      CGStdGraphObj *dep=this.GetDependentObj(modifier);
      if(dep==NULL)
         return false;
      //--- Set X and Y coordinates to all pivot points of a subordinate object and
      //--- save the current properties of a subordinate graphical object as the previous ones
      if(this.SetCoordsXYtoDependentObj(dep))
         dep.PropertiesCopyToPrevData();
     }
//--- Move a reference control point to new coordinates
   this.ExtToolkit.SetBaseObjTimePrice(this.Time(modifier),this.Price(modifier),modifier);
   this.ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
//--- If the flag is active, redraw the chart
   if(redraw)
      ::ChartRedraw(m_chart_id);
//--- All is successful - return 'true'
   return true;
  }
//+------------------------------------------------------------------+


Implement the method setting X and Y coordinates into the appropriate pivot point of a specified subordinate object:

//+------------------------------------------------------------------+
//| Set X and Y coordinates to the associated pivot point            |
//| of a specified subordinate object by index                       |
//+------------------------------------------------------------------+
void CGStdGraphObj::SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj,CLinkedPivotPoint *pivot_point,const int index)
  {
//--- get the number of coordinate points of the base object for setting the X coordinate
   int numx=pivot_point.GetBasePivotsNumX(index);
//--- 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 to the subordinate graphical object attached to the 'index' point
      int prop_from=pivot_point.GetPropertyX(index,nx);
      int modifier_from=pivot_point.GetPropertyModifierX(index,nx);
      this.SetCoordXToDependentObj(dependent_obj,prop_from,modifier_from,nx);
     }
//--- Get the number of coordinate points of the base object for setting the Y coordinate
   int numy=pivot_point.GetBasePivotsNumY(index);
   //--- 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 to the subordinate graphical object attached to the 'index' point
      int prop_from=pivot_point.GetPropertyY(index,ny);
      int modifier_from=pivot_point.GetPropertyModifierY(index,ny);
      this.SetCoordYToDependentObj(dependent_obj,prop_from,modifier_from,ny);
     }
  }
//+------------------------------------------------------------------+

In fact, these are the very code blocks removed from the class methods. I have considered the code logic in the previous articles. Besides, it is described in the code comments. So, I believe, it requires no explanations.

Implementing the method setting X and Y coordinates into associated pivot points of a specified subordinate object:

//+------------------------------------------------------------------+
//| Set X and Y coordinates to associated pivot points               |
//| of the specified subordinate object                              |
//+------------------------------------------------------------------+
bool CGStdGraphObj::SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj)
  {
//--- Get the object of pivot point data of the bound graphical object
   CLinkedPivotPoint *pp=dependent_obj.GetLinkedPivotPoint();
   if(pp==NULL)
      return false;
//--- get the number of coordinate points the object is attached to
   int num=pp.GetNumLinkedCoords();
//--- In the loop by the object coordinate points,
//--- set X and Y to all pivot points of a subordinate object
   for(int j=0;j<num;j++)
      this.SetCoordsXYtoDependentObj(dependent_obj,pp,j);
   return true;
  }
//+------------------------------------------------------------------+

The method allows setting coordinates to all pivot points of a subordinate object. If other graphical object are added to the composite graphical object, the method sets the specified coordinates in them.


Let's add improvements into the collection class of graphical elements \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh.

Since the ChartTimePriceToXY() standard function returns two coordinates at once — X and Y, I will create the structure in the private section to store them. In addition to the coordinates, the structure will also store the coordinate shifts relative to the central point. Since the graphical object may have several pivot points, declare the array with the created structure type in order to save the coordinates of each pivot point for the graphical object. In this case, each array cell will feature the X and Y screen coordinates converted from the "time/price" ones, as well as shifts of the pivot point coordinates relative to the graphical object central point.

In the private section of the class, create the structure and declare the necessary array:

//+------------------------------------------------------------------+
//| Collection of graphical objects                                  |
//+------------------------------------------------------------------+
#resource "\\"+PATH_TO_EVENT_CTRL_IND;          // Indicator for controlling graphical object events packed into the program resources
class CGraphElementsCollection : public CBaseObj
  {
private:
   //--- Pivot point data structure
   struct SDataPivotPoint
     {
      public:
         int         X;                         // Pivot point X coordinate
         int         Y;                         // Pivot point Y coordinate
         int         ShiftX;                    // Pivot point X coordinate shift from the central one
         int         ShiftY;                    // Pivot point Y coordinate shift from the central one
     };
   SDataPivotPoint   m_data_pivot_point[];      // Pivot point data structure array
   CArrayObj         m_list_charts_control;     // List of chart management objects
   CListObj          m_list_all_canv_elm_obj;   // List of all graphical elements on canvas
   CListObj          m_list_all_graph_obj;      // List of all graphical objects
   CArrayObj         m_list_deleted_obj;        // List of removed graphical objects
   CMouseState       m_mouse;                   // "Mouse status" class object
   bool              m_is_graph_obj_event;      // Event flag in the list of graphical objects
   int               m_total_objects;           // Number of graphical objects
   int               m_delta_graph_obj;         // Difference in the number of graphical objects compared to the previous check

In the private section of the class, declare the method returning the screen coordinates of each pivot point of a graphical object:

private:
//--- Find an object present in the collection but not on a chart
   CGStdGraphObj    *FindMissingObj(const long chart_id);
   CGStdGraphObj    *FindMissingObj(const long chart_id,int &index);
//--- Find the graphical object present on a chart but not in the collection
   string            FindExtraObj(const long chart_id);
//--- Remove the graphical object class object from the graphical object collection list: (1) specified object, (2) by chart ID
   bool              DeleteGraphObjFromList(CGStdGraphObj *obj);
   void              DeleteGraphObjectsFromList(const long chart_id);
//--- Move the graphical object class object to the list of removed graphical objects: (1) specified object, (2) by index
   bool              MoveGraphObjToDeletedObjList(CGStdGraphObj *obj);
   bool              MoveGraphObjToDeletedObjList(const int index);
//--- Move all objects by chart ID to the list of removed graphical objects
   void              MoveGraphObjectsToDeletedObjList(const long chart_id);
//--- Remove the object of managing charts from the list
   bool              DeleteGraphObjCtrlObjFromList(CChartObjectsControl *obj);
//--- Set the flags of scrolling the chart with the mouse, context menu and crosshairs tool for the specified chart
   void              SetChartTools(const long chart_id,const bool flag);
//--- Return the screen coordinates of each pivot point of the graphical object
   bool              GetPivotPointCoordsAll(CGStdGraphObj *obj,SDataPivotPoint &array_pivots[]);
public:

Outside the class body, implement the method:

//+------------------------------------------------------------------+
//| Return screen coordinates                                        |
//| of each graphical object pivot point                             |
//+------------------------------------------------------------------+
bool CGraphElementsCollection::GetPivotPointCoordsAll(CGStdGraphObj *obj,SDataPivotPoint &array_pivots[])
  {
//--- If failed to increase the array of structures to match the number of pivot points,
//--- inform of that in the journal and return 'false'
   if(::ArrayResize(array_pivots,obj.Pivots())!=obj.Pivots())
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      return false;
     }
//--- In the loop by the number of graphical object pivot points
   for(int i=0;i<obj.Pivots();i++)
     {
      //--- Convert the object coordinates into screen ones. If failed, inform of that and return 'false'
      if(!::ChartTimePriceToXY(obj.ChartID(),obj.SubWindow(),obj.Time(i),obj.Price(i),array_pivots[i].X,array_pivots[i].Y))
       {
        CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_CONV_GRAPH_OBJ_COORDS_TO_XY);
        return false;
       }
     }
//--- Depending on the graphical object type
   switch(obj.TypeGraphObject())
     {
      //--- One pivot point in screen coordinates
      case OBJ_LABEL             :
      case OBJ_BUTTON            :
      case OBJ_BITMAP_LABEL      :
      case OBJ_EDIT              :
      case OBJ_RECTANGLE_LABEL   :
      case OBJ_CHART             : break;
      
      //--- One pivot point (price only)
      case OBJ_HLINE             : break;
      
      //--- One pivot point (time only)
      case OBJ_VLINE             : break;
      case OBJ_EVENT             : break;
      
      //--- Two pivot points and a central one
      //--- Lines
      case OBJ_TREND             :
      case OBJ_TRENDBYANGLE      :
      case OBJ_CYCLES            :
      case OBJ_ARROWED_LINE      :
      //--- Channels
      case OBJ_CHANNEL           :
      case OBJ_STDDEVCHANNEL     :
      case OBJ_REGRESSION        :
      //--- Gann
      case OBJ_GANNLINE          :
      case OBJ_GANNGRID          :
      //--- Fibo
      case OBJ_FIBO              :
      case OBJ_FIBOTIMES         :
      case OBJ_FIBOFAN           :
      case OBJ_FIBOARC           :
      case OBJ_FIBOCHANNEL       :
      case OBJ_EXPANSION         :
        //--- Calculate the shifts of all pivot points from the central one and write them to the structure array
        array_pivots[0].ShiftX=(array_pivots[1].X-array_pivots[0].X)/2; 
        array_pivots[0].ShiftY=(array_pivots[1].Y-array_pivots[0].Y)/2;
        array_pivots[1].ShiftX=(array_pivots[0].X-array_pivots[1].X)/2;
        array_pivots[1].ShiftY=(array_pivots[0].Y-array_pivots[1].Y)/2;
        return true;

      //--- Channels
      case OBJ_PITCHFORK         : break;
      //--- Gann
      case OBJ_GANNFAN           : break;
      //--- Elliott
      case OBJ_ELLIOTWAVE5       : break;
      case OBJ_ELLIOTWAVE3       : break;
      //--- Shapes
      case OBJ_RECTANGLE         : break;
      case OBJ_TRIANGLE          : break;
      case OBJ_ELLIPSE           : break;
      //--- Arrows
      case OBJ_ARROW_THUMB_UP    : break;
      case OBJ_ARROW_THUMB_DOWN  : break;
      case OBJ_ARROW_UP          : break;
      case OBJ_ARROW_DOWN        : break;
      case OBJ_ARROW_STOP        : break;
      case OBJ_ARROW_CHECK       : break;
      case OBJ_ARROW_LEFT_PRICE  : break;
      case OBJ_ARROW_RIGHT_PRICE : break;
      case OBJ_ARROW_BUY         : break;
      case OBJ_ARROW_SELL        : break;
      case OBJ_ARROW             : break;
      //--- Graphical objects with time/price coordinates
      case OBJ_TEXT              : break;
      case OBJ_BITMAP            : break;
      //---
      default: break;
     }
   return false;
  }
//+------------------------------------------------------------------+

For now, only the screen coordinates of the graphical objects having two pivot points and a single central one are set in the structure.

The method receives the pointer to the graphical object whose pivot point coordinates should be set in the structure array which is also passed to the method by link. In case of a successful coordinate conversion, the method returns true together with the fully filled structure array with screen coordinates for each pivot point of a graphical object. If failed, the method returns false.

In the class event handler, we need to handle the object management form shift so that the object is moved in its entirety if this is a central point. To achieve this, we need to calculate the shifts of its edge forms relative to the central one (used to drag the object) and relocate both pivot points by a shift value calculated and set in the structure. Thus, all its pivot points will shift by the same value as the central one dragged by the mouse.

Let's add such handling of a central control point (form) moving event:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CGraphElementsCollection::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   CGStdGraphObj *obj_std=NULL;  // Pointer to the standard graphical object
   CGCnvElement  *obj_cnv=NULL;  // Pointer to the graphical element object on canvas
   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_std=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_std==NULL)
        {
         //--- Let's search the list for the object that is not on the chart
         obj_std=this.FindMissingObj(chart_id);
         //--- If failed to find the object here as well, exit
         if(obj_std==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_std.SetNamePrev(obj_std.Name()) && obj_std.SetName(name_new))
            ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_RENAME,obj_std.ChartID(),obj_std.TimeCreate(),obj_std.Name());
        }
      //--- Update the properties of the obtained object
      //--- and check their change
      obj_std.PropertiesRefresh();
      obj_std.PropertiesCheckChanged();
     }

//--- Handle standard graphical object events in the collection list
   for(int i=0;i<this.m_list_all_graph_obj.Total();i++)
     {
      //--- Get the next graphical object and
      obj_std=this.m_list_all_graph_obj.At(i);
      if(obj_std==NULL)
         continue;
      //--- call its event handler
      obj_std.OnChartEvent((id<CHARTEVENT_CUSTOM ? id : idx),lparam,dparam,sparam);
     }

//--- 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_std=list.At(i);
            if(obj_std==NULL)
               continue;
            obj_std.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
           }
        }
     }
//--- Handling mouse events of graphical objects on canvas
//--- If the event is not a chart change
   else
     {
      //--- Check whether the mouse button is pressed
      bool pressed=(this.m_mouse.ButtonKeyState(id,lparam,dparam,sparam)==MOUSE_BUTT_KEY_STATE_LEFT ? true : false);
      ENUM_MOUSE_FORM_STATE mouse_state=MOUSE_FORM_STATE_NONE;
      //--- Declare static variables for the active form and status flags
      static CForm *form=NULL;
      static bool pressed_chart=false;
      static bool pressed_form=false;
      static bool move=false;
      //--- Declare static variables for the index of the form for managing an extended standard graphical object and its ID
      static int  form_index=WRONG_VALUE;
      static long graph_obj_id=WRONG_VALUE;
      
      //--- If the button is not pressed on the chart and the movement flag is not set, get the form, above which the cursor is located
      if(!pressed_chart && !move)
         form=this.GetFormUnderCursor(id,lparam,dparam,sparam,mouse_state,graph_obj_id,form_index);
      
      //--- If the button is not pressed, reset all flags and enable the chart tools 
      if(!pressed)
        {
         pressed_chart=false;
         pressed_form=false;
         move=false;
         this.SetChartTools(::ChartID(),true);
        }
      
      //--- If this is a mouse movement event and the movement flag is active, move the form, above which the cursor is located (if the pointer to it is valid)
      if(id==CHARTEVENT_MOUSE_MOVE && move)
        {
         if(form!=NULL)
           {
            //--- calculate the cursor movement relative to the form coordinate origin
            int x=this.m_mouse.CoordX()-form.OffsetX();
            int y=this.m_mouse.CoordY()-form.OffsetY();
            //--- get the width and height of the chart the form is located at
            int chart_width=(int)::ChartGetInteger(form.ChartID(),CHART_WIDTH_IN_PIXELS,form.SubWindow());
            int chart_height=(int)::ChartGetInteger(form.ChartID(),CHART_HEIGHT_IN_PIXELS,form.SubWindow());
            //--- If the form is not within an extended standard graphical object
            if(form_index==WRONG_VALUE)
              {
               //--- Adjust the calculated form coordinates if the form is out of the chart range
               if(x<0) x=0;
               if(x>chart_width-form.Width()) x=chart_width-form.Width();
               if(y<0) y=0;
               if(y>chart_height-form.Height()) y=chart_height-form.Height();
               //--- If the one-click trading panel is not present on the chart,
               if(!::ChartGetInteger(form.ChartID(),CHART_SHOW_ONE_CLICK))
                 {
                  //--- calculate the form coordinates so that the form does not overlap with the one-click trading panel button
                  if(y<17 && x<41)
                     y=17;
                 }
               //--- If the one-click trading panel is on the chart,
               else
                 {
                  //--- calculate the form coordinates so that the form does not overlap with the one-click trading panel
                  if(y<80 && x<192)
                     y=80;
                 }
              }
            //--- If the form is included into the extended standard graphical object
            else
              {
               if(graph_obj_id>WRONG_VALUE)
                 {
                  //--- Get the list of objects by object ID (there should be one object)
                  CArrayObj *list_ext=CSelect::ByGraphicStdObjectProperty(GetListStdGraphObjectExt(),GRAPH_OBJ_PROP_ID,0,graph_obj_id,EQUAL);
                  //--- If managed to obtain the list and it is not empty,
                  if(list_ext!=NULL && list_ext.Total()>0)
                    {
                     //--- get the graphical object from the list
                     CGStdGraphObj *ext=list_ext.At(0);
                     //--- If the pointer to the object has been received,
                     if(ext!=NULL)
                       {
                        //--- get the object type
                        ENUM_OBJECT type=ext.GraphObjectType();
                        //--- If the object is built using screen coordinates, set the coordinates to the object
                        if(type==OBJ_LABEL || type==OBJ_BUTTON || type==OBJ_BITMAP_LABEL || type==OBJ_EDIT || type==OBJ_RECTANGLE_LABEL)
                          {
                           ext.SetXDistance(x);
                           ext.SetYDistance(y);
                          }
                        //--- otherwise, if the object is built based on time/price coordinates,
                        else
                          {
                           //--- calculate the coordinate shift
                           int shift=(int)::ceil(form.Width()/2)+1;
                           //--- If the form is located on one of the graphical object pivot points,
                           if(form_index<ext.Pivots())
                             {
                              //--- limit the form coordinates so that they do not move beyond the chart borders
                              if(x+shift<0)
                                 x=-shift;
                              if(x+shift>chart_width)
                                 x=chart_width-shift;
                              if(y+shift<0)
                                 y=-shift;
                              if(y+shift>chart_height)
                                 y=chart_height-shift;
                              //--- set the calculated coordinates to the object
                              ext.ChangeCoordsExtendedObj(x+shift,y+shift,form_index);
                             }
                           //--- If the form is central for managing all pivot points of a graphical object
                           else
                             {
                              //--- Get screen coordinates of all object pivot points and write them to the m_data_pivot_point structure
                              if(this.GetPivotPointCoordsAll(ext,m_data_pivot_point))
                                {
                                 //--- In the loop by the number of object pivot points,
                                 for(int i=0;i<(int)this.m_data_pivot_point.Size();i++)
                                   {
                                    //--- limit the screen coordinates of the current pivot point so that they do not move beyond the chart borders
                                    if(x+shift-this.m_data_pivot_point[i].ShiftX<0)
                                       x=-shift+m_data_pivot_point[i].ShiftX;
                                    if(x+shift+this.m_data_pivot_point[i].ShiftX>chart_width)
                                       x=chart_width-shift-this.m_data_pivot_point[i].ShiftX;
                                    if(y+shift+this.m_data_pivot_point[i].ShiftY<0)
                                       y=-shift-this.m_data_pivot_point[i].ShiftY;
                                    if(y+shift-this.m_data_pivot_point[i].ShiftY>chart_height)
                                       y=chart_height-shift+this.m_data_pivot_point[i].ShiftY;
                                    //--- set the calculated coordinates to the current object pivot point
                                    ext.ChangeCoordsExtendedObj(x+shift-this.m_data_pivot_point[i].ShiftX,y+shift-this.m_data_pivot_point[i].ShiftY,i);
                                   }
                                }
                             }
                          }
                       }
                    }
                 }
              }
            //--- Move the form by the obtained coordinates
            form.Move(x,y,true);
           }
        }
   
      //--- Display debugging comments on the chart
      Comment
        (
         (form!=NULL ? form.Name()+":" : ""),"\n",
         EnumToString((ENUM_CHART_EVENT)id),"\n",
         EnumToString(this.m_mouse.ButtonKeyState(id,lparam,dparam,sparam)),
         "\n",EnumToString(mouse_state),
         "\npressed=",pressed,", move=",move,(form!=NULL ? ", Interaction="+(string)form.Interaction() : ""),
         "\npressed_chart=",pressed_chart,", pressed_form=",pressed_form,
         "\nform_index=",form_index,", graph_obj_id=",graph_obj_id
        );
      
      //--- If the cursor is not above the form
      if(form==NULL)
        {
         //--- If the mouse button is pressed
         if(pressed)
           {
            //--- If the button is still pressed and held on the form, exit
            if(pressed_form)
              {
               return;
              }
            //--- If the button hold flag is not enabled yet, set the flags and enable chart tools
            if(!pressed_chart)
              {
               pressed_chart=true;  // Button is held on the chart
               pressed_form=false;  // Cursor is not above the form
               move=false;          // movement disabled
               this.SetChartTools(::ChartID(),true);
              }
           }
         //--- If the mouse button is not pressed
         else
           {
            //--- Get the list of extended standard graphical objects
            CArrayObj *list_ext=GetListStdGraphObjectExt();
            //--- In the loop by all extended graphical objects,
            int total=list_ext.Total();
            for(int i=0;i<total;i++)
              {
               //--- get the next graphical object
               CGStdGraphObj *obj=list_ext.At(i);
               if(obj==NULL)
                  continue;
               //--- and redraw it without a point and a circle
               obj.RedrawControlPointForms(0,CTRL_POINT_COLOR);
              }
           }
        }
      //--- If the cursor is above the form
      else
        {
         //--- If the button is still pressed and held on the chart, exit
         if(pressed_chart)
           {
            return;
           }
         
         //--- If the flag of holding the button on the form is not set yet
         if(!pressed_form)
           {
            pressed_chart=false;    // The button is not pressed on the chart
            this.SetChartTools(::ChartID(),false);
            
            //--- 'The cursor is inside the form, no mouse buttons are clicked' event handler
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED)
              {
               //--- If the cursor is above the form for managing the pivot point of an extended graphical object,
               if(graph_obj_id>WRONG_VALUE)
                 {
                  //--- get the object by its ID and by the chart ID
                  CGStdGraphObj *graph_obj=this.GetStdGraphObjectExt(graph_obj_id,form.ChartID());
                  if(graph_obj!=NULL)
                    {
                     //--- Get the toolkit of an extended standard graphical object
                     CGStdGraphObjExtToolkit *toolkit=graph_obj.GetExtToolkit();
                     if(toolkit!=NULL)
                       {
                        //--- Draw a point with a circle on the form and delete it on all other forms
                        toolkit.DrawOneControlPoint(form);
                       }
                    }
                 }
              }
            //--- 'The cursor is inside the form, a mouse button is clicked (any)' event handler
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_PRESSED)
              {
               this.SetChartTools(::ChartID(),false);
               //--- If the flag of holding the form is not set yet
               if(!pressed_form)
                 {
                  pressed_form=true;      // set the flag of pressing on the form
                  pressed_chart=false;    // disable the flag of pressing on the form
                 }
              }
            //--- 'The cursor is inside the form, the mouse wheel is being scrolled' event handler workpiece
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_WHEEL)
              {
               
              }
            
            
            //--- 'The cursor is inside the active area, the mouse buttons are not clicked' event handler
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED)
              {
               //--- Set the cursor shift relative to the form initial coordinates
               form.SetOffsetX(this.m_mouse.CoordX()-form.CoordX());
               form.SetOffsetY(this.m_mouse.CoordY()-form.CoordY());
               //--- If the cursor is above the active area of the form for managing the pivot point of an extended graphical object,
               if(graph_obj_id>WRONG_VALUE)
                 {
                  //--- get the object by its ID and by the chart ID
                  CGStdGraphObj *graph_obj=this.GetStdGraphObjectExt(graph_obj_id,form.ChartID());
                  if(graph_obj!=NULL)
                    {
                     //--- Get the toolkit of an extended standard graphical object
                     CGStdGraphObjExtToolkit *toolkit=graph_obj.GetExtToolkit();
                     if(toolkit!=NULL)
                       {
                        //--- Draw a point with a circle on the form and delete it on all other forms
                        toolkit.DrawOneControlPoint(form);
                       }
                    }
                 }
              }
            //--- 'The cursor is inside the active area,  any mouse button is clicked' event handler
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED && !move)
              {
               pressed_form=true;   // the flag of holding the mouse button on the form
               //--- If the left mouse button is pressed
               if(this.m_mouse.IsPressedButtonLeft())
                 {
                  //--- Set flags and form parameters
                  move=true;                                            // movement flag
                  form.SetInteraction(true);                            // flag of the form interaction with the environment
                  form.BringToTop();                                    // form on the background - above all others
                  this.ResetAllInteractionExeptOne(form);               // Reset interaction flags for all forms except the current one
                  form.SetOffsetX(this.m_mouse.CoordX()-form.CoordX()); // Cursor shift relative to the X coordinate
                  form.SetOffsetY(this.m_mouse.CoordY()-form.CoordY()); // Cursor shift relative to the Y coordinate
                 }
              }
            //--- 'The cursor is inside the active area, the mouse wheel is being scrolled' event handler workpiece
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL)
              {
               
              }
            
            
            //--- 'The cursor is inside the window scrolling area, no mouse buttons are clicked' event handler workpiece
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED)
              {
               
              }
            //--- 'The cursor is inside the window scrolling area, a mouse button is clicked (any)' event handler workpiece
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED)
              {
               
              }
            //--- 'The cursor is inside the window scrolling area, the mouse wheel is being scrolled' event handler workpiece
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL)
              {
               
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Apart from the new handler of moving the central management form, I have also added calling the method drawing a point on the object form located under the cursor and deleting these points on other forms of the graphical object. This allows avoiding a simultaneous drawing of points on several form objects if they are located close to each other and overlap each other as shown above.

Currently, all is ready for testing the new functionality.


Test

To perform the test, let's use the EA from the previous article and save it in \MQL5\Experts\TestDoEasy\Part99\ as TestDoEasyPart99.mq5.

I do not have to insert any changes to the EA — for now, all changes are made only in the library classes.

Compile the EA and launch it on the chart:


As we can see, if we move the composite graphical object in the form, in which it was created, then all restrictions related to pivot points exceeding the chart limits work correctly. But if we "reverse" the location of pivot points relative to their initial location, the object "configuration" is distorted when the pivot point goes beyond the chart limits. This implies the incorrect calculation of the restrictions and dependence on which pivot point exceeds the right, left, top or bottom chart edge.
This is not surprising since the pivot point shifts are calculated relative to the central one which means that one point will have a positive shift, while the second one will have a negative shift. We face the limitation calculation error when changing the location of the pivot points relative to the central one. I will fix this in the next article.


What's next?

In the next article, I will continue my work on composite graphical objects and their functionality.

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
Graphics in DoEasy library (Part 95): Composite graphical object controls
Graphics in DoEasy library (Part 96): Graphics in form objects and handling mouse events
Graphics in DoEasy library (Part 97): Independent handling of form object movement
Graphics in DoEasy library (Part 98): Moving pivot points of extended standard graphical objects

Translated from Russian by MetaQuotes Software Corp.
Original article: https://www.mql5.com/ru/articles/10584

Attached files |
MQL5.zip (4368.36 KB)
Multiple indicators on one chart (Part 05): Turning MetaTrader 5 into a RAD system (I) Multiple indicators on one chart (Part 05): Turning MetaTrader 5 into a RAD system (I)
There are a lot of people who do not know how to program but they are quite creative and have great ideas. However, the lack of programming knowledge prevents them from implementing these ideas. Let's see together how to create a Chart Trade using the MetaTrader 5 platform itself, as if it were an IDE.
Multiple indicators on one chart (Part 04): Advancing to an Expert Advisor Multiple indicators on one chart (Part 04): Advancing to an Expert Advisor
In my previous articles, I have explained how to create an indicator with multiple subwindows, which becomes interesting when using custom indicators. This time we will see how to add multiple windows to an Expert Advisor.
Learn how to design a trading system by Parabolic SAR Learn how to design a trading system by Parabolic SAR
In this article, we will continue our series about how to design a trading system using the most popular indicators. In this article, we will learn about the Parabolic SAR indicator in detail and how we can design a trading system to be used in MetaTrader 5 using some simple strategies.
Graphics in DoEasy library (Part 98): Moving pivot points of extended standard graphical objects Graphics in DoEasy library (Part 98): Moving pivot points of extended standard graphical objects
In the article, I continue the development of extended standard graphical objects and create the functionality for moving pivot points of composite graphical objects using the control points for managing the coordinates of the graphical object pivot points.