Русский 中文 Español Deutsch 日本語 Português
Graphics in DoEasy library (Part 91): Standard graphical object events. Object name change history

Graphics in DoEasy library (Part 91): Standard graphical object events. Object name change history

MetaTrader 5Examples | 15 February 2022, 15:05
6 054 0
Artyom Trishkin
Artyom Trishkin

Contents


Concept

The library is already able to define events occurring to standard graphical objects. In this article, I want to implement the functionality that allows users working in a library-based program to accurately define which property has been changed and for how much when receiving an event. The idea is simple and based on passing an event ID and a name of an object the event has occurred in, an ID of a chart the changed graphical object is located in and a name of a property whose value has been changed. When removing graphical objects from the chart, objects of the classes describing these removed graphical objects are placed to the list of removed objects. This allows us to programatically define, which object has been removed, and receive all its properties or restore an accidentally deleted one.

If we expand the concept of storing removed graphical objects up to storing changed object properties, we will be able to create an attractive functionality enabling us to repeat the entire object change history. In other words, if we have the list of all successively changed object properties, we will always be able to reproduce any state the object had before the current one. Thus, it is possible to "set" different object positions relative to a chart and reproduce them sequentially (or not) afterwards. This will make it possible to make technical analysis tools featuring their own "memory", including composite graphical objects I am going to consider in the next articles.

I am not going to introduce this concept in full here but I will test its functionality using the "Object name" property change as an example. In subsequent articles, I will gradually create the ability to record and reproduce all object properties.

This concept was born from the need to let the program know, which graphical object name has been changed, to display its previous and current names in the journal. I decided that it would be good to expand the functionality of the library and library-based graphical objects. So I have come up with the current concept.


Improving library classes

As usual, I will start with implementing new library messages in \MQL5\Include\DoEasy\Data.mqh.

Add new message indices:

//--- CGraphElementsCollection
   MSG_GRAPH_OBJ_FAILED_GET_ADDED_OBJ_LIST,           // Failed to get the list of newly added objects
   MSG_GRAPH_OBJ_FAILED_DETACH_OBJ_FROM_LIST,         // Failed to remove a graphical object from the list
   MSG_GRAPH_OBJ_FAILED_ADD_OBJ_TO_DEL_LIST,          // Failed to set a graphical object to the list of removed objects
   MSG_GRAPH_OBJ_FAILED_ADD_OBJ_TO_RNM_LIST,          // Failed to set a graphical object to the list of renamed objects
   
   MSG_GRAPH_OBJ_CREATE_EVN_CTRL_INDICATOR,           // Indicator for controlling and sending events created
   MSG_GRAPH_OBJ_FAILED_CREATE_EVN_CTRL_INDICATOR,    // Failed to create the indicator for controlling and sending events
   MSG_GRAPH_OBJ_ADDED_EVN_CTRL_INDICATOR,            // Indicator for controlling and sending events successfully added to the chart
   MSG_GRAPH_OBJ_FAILED_ADD_EVN_CTRL_INDICATOR,       // Failed to add the indicator for controlling and sending events
   MSG_GRAPH_OBJ_ALREADY_EXIST_EVN_CTRL_INDICATOR,    // Indicator for controlling and sending events is already present on the chart
   MSG_GRAPH_OBJ_CLOSED_CHARTS,                       // Chart windows closed:
   MSG_GRAPH_OBJ_OBJECTS_ON_CLOSED_CHARTS,            // Objects removed together with charts:
   MSG_GRAPH_OBJ_FAILED_CREATE_EVN_OBJ,               // Failed to create the event object for a graphical object
   MSG_GRAPH_OBJ_FAILED_ADD_EVN_OBJ,                  // Failed to add the event object to the list
   MSG_GRAPH_OBJ_GRAPH_OBJ_PROP_CHANGE_HISTORY,       // Property change history: 
   
   MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CREATE,                // New graphical object created
   MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CHANGE,                // Changed the graphical object property
   MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_RENAME,                // Graphical object renamed
   MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DELETE,                // Graphical object removed
   MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DEL_CHART,             // Graphical object removed together with the chart
   
  };
//+------------------------------------------------------------------+

and text messages corresponding to newly added indices:

//--- CGraphElementsCollection
   {"Не удалось получить список вновь добавленных объектов","Failed to get the list of newly added objects"},
   {"Не удалось изъять графический объект из списка","Failed to detach graphic object from the list"},
   {"Не удалось поместить графический объект в список удалённых объектов","Failed to place graphic object in the list of deleted objects"},
   {"Не удалось поместить графический объект в список переименованных объектов","Failed to place graphic object in the list of renamed objects"},
   
   {"Создан индикатор контроля и отправки событий","An indicator for monitoring and sending events has been created"},
   {"Не удалось создать индикатор контроля и отправки событий","Failed to create indicator for monitoring and sending events"},
   {"Индикатор контроля и отправки событий успешно добавлен на график ","The indicator for monitoring and sending events has been successfully added to the chart "},
   {"Не удалось добавить индикатор контроля и отправки событий на график ","Failed to add the indicator of monitoring and sending events to the chart "},
   {"Индикатор контроля и отправки событий уже есть на графике","The indicator for monitoring and sending events is already on the chart"},
   {"Закрыто окон графиков: ","Closed chart windows: "},
   {"С ними удалено объектов: ","Objects removed with them: "},
   {"Не удалось создать объект-событие для графического объекта","Failed to create event object for graphic object"},
   {"Не удалось добавить объект-событие в список","Failed to add event object to list"},
   {"История изменения свойства: ","Property change history: "},
   
   {"Создан новый графический объект","New graphic object created"},
   {"Изменено свойство графического объекта","Changed graphic object property"},
   {"Графический объект переименован","Graphic object renamed"},
   {"Удалён графический объект","Graphic object deleted"},
   {"Графический объект удалён вместе с графиком","The graphic object has been removed along with the chart"},
   
  };
//+---------------------------------------------------------------------+


In the public section of the abstract standard graphical object class in \MQL5\Include\DoEasy\Objects\Graph\Standard\GStdGraphObj.mqh, add the new method for returning the pointer to the property object and declare the method for displaying the history of renaming the graphical object in the journal:

//--- Return (1) itself and (2) the properties
   CGStdGraphObj    *GetObject(void)                                       { return &this;      }
   CProperties      *Properties(void)                                      { return this.Prop;  }
//--- 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;       }

//--- Get description of (1) integer, (2) real and (3) string properties
   string            GetPropertyDescription(ENUM_GRAPH_OBJ_PROP_INTEGER property);
   string            GetPropertyDescription(ENUM_GRAPH_OBJ_PROP_DOUBLE property);
   string            GetPropertyDescription(ENUM_GRAPH_OBJ_PROP_STRING property);

//--- Return the description of the graphical object anchor point position
   virtual string    AnchorDescription(void)                                  const { return (string)this.GetProperty(GRAPH_OBJ_PROP_ANCHOR,0); }

//--- Display the description of the object properties in the journal (full_prop=true - all properties, false - supported ones only - implemented in descendant classes)
   virtual void      Print(const bool full_prop=false,const bool dash=false);
//--- Display a short description of the object in the journal
   virtual void      PrintShort(const bool dash=false,const bool symbol=false);
//--- Return the object short name
   virtual string    Header(const bool symbol=false);
//--- Display the history of renaming the object to the journal
   void              PrintRenameHistory(void);

//--- Return the description of the (1) (ENUM_OBJECT) graphical object type
   virtual string    TypeDescription(void)                                    const { return CMessage::Text(MSG_GRAPH_STD_OBJ_ANY);          }


When renaming an object, we cannot know its previous name if it is not set anywhere in advance. We already have the mechanism of defining an object renaming. The graphical object collection stores the object of the standard graphical object class accompanied with the description of its parameters for each physical object on a chart. As soon as the object renaming event is defined, we change its name in the appropriate class object. Before changing the name, we are able to know the object previous name and save it for history. The same can be done with the rest of the properties.

I have decided to store a previous object name directly in the object properties, since objects of the graphical object class are now based on multidimensional dynamic arrays, which allows us to set the necessary array size in any of its dimensions at any time. Let's use this. I am going to increase the object name property array at each subsequent renaming of the graphical object and enter its previous name to the array end. Thus, cell 0 of the object name property array will store its actual name, while other array cells are to store its previous names. In this case, we are always able to know the entire history of object renaming. If we do the same for its other properties, we will be able to receive an interesting graphical object able to write and store its states. By specifying the necessary cell the parameters set to the object are to be read from, we will be able to quickly reproduce all its states, which can be useful for creating analytical tools with memory.

Other properties (not an object name) will be improved later. Here I will only check the concept of storing the history of replacing object properties. Then I will slightly change the structure of the multidimensional array storing the object properties so that they can be used to store the object multi-property (for example, its levels) and the history of the level changes. Currently, the levels are stored in the same array cells as the history of changes of the Name object property values. To avoid property collisions, I will have to slightly change the structure of the multidimensional property array. I will do this in the next article.

In the public section of the class, set the method for setting the object previous name:

//--- Compare CGStdGraphObj objects by a specified property (to sort the list by an object property)
   virtual int       Compare(const CObject *node,const int mode=0) const;
//--- Compare CGStdGraphObj objects with each other by all properties (to search for equal objects)
   bool              IsEqual(CGStdGraphObj* compared_req) const;
   
//--- Set the object previous name
   bool              SetNamePrev(const string name)
                       {
                        if(!this.Prop.SetSizeRange(GRAPH_OBJ_PROP_NAME,this.Prop.CurrSize(GRAPH_OBJ_PROP_NAME)+1))
                           return false;
                        this.SetProperty(GRAPH_OBJ_PROP_NAME,this.Prop.CurrSize(GRAPH_OBJ_PROP_NAME)-1,name);
                        return true;
                       }
  
//--- Default constructor
                     CGStdGraphObj(){ this.m_type=OBJECT_DE_TYPE_GSTD_OBJ; m_group=WRONG_VALUE; }
//--- Destructor
                    ~CGStdGraphObj()
                       {
                        if(this.Prop!=NULL)
                           delete this.Prop;
                       }
protected:

The method receives the object previous name, the size of the property array containing the name is increased by 1, while the new array cell receives the previous name of the graphical object passed to the method and true is returned. If changing the array size or setting the name fails, the appropriate journal message is displayed and false is returned.

Beyond the class body, implement the method displaying the object renaming history in the journal:

//+------------------------------------------------------------------+
//| Display the history of renaming the object to the journal        |
//+------------------------------------------------------------------+
void CGStdGraphObj::PrintRenameHistory(void)
  {
   ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_GRAPH_OBJ_PROP_CHANGE_HISTORY),this.GetPropertyDescription(GRAPH_OBJ_PROP_NAME),":");
   int size=this.Properties().CurrSize(GRAPH_OBJ_PROP_NAME);
   ::Print(DFUN,"0: ",this.GetProperty(GRAPH_OBJ_PROP_NAME,0));
   for(int i=size-1;i>0;i--)
      ::Print(DFUN,size-i,": ",this.GetProperty(GRAPH_OBJ_PROP_NAME,i));
  }
//+------------------------------------------------------------------+

Here the header with the new object name is displayed first followed by its current name with index 0.
Next, in the reverse loop up to the array cell with index 1 inclusive, display the description of the Name object property added to the object name array cells storing its renaming history by the loop index. Since the current object name is always stored in the zero cell, while the previous one is stored in the last one, we will calculate this value rather than using the loop index for the correct display of object renaming sequence index.

For example, the following entry appears for an object renamed three times:

CGStdGraphObj::PrintRenameHistory: Property change history: Name: "M30 Vertical Line 24264_3":
CGStdGraphObj::PrintRenameHistory: 0: M30 Vertical Line 24264_3
CGStdGraphObj::PrintRenameHistory: 1: M30 Vertical Line 24264_2
CGStdGraphObj::PrintRenameHistory: 2: M30 Vertical Line 24264_1
CGStdGraphObj::PrintRenameHistory: 3: M30 Vertical Line 24264

Here we can clearly see the sequence of renaming a graphical object. (I will probably change the sequence numbering so that 0 always corresponds to the object initial name, while the last index corresponds to the current name).


The main work on finalizing the tracking of graphical object events is performed in the graphical object collection class.

Open \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh and make the necessary changes.

In the current implementation of handling the event of adding objects, we get the last added object from the terminal list. This works only if a single new object has been added to the chart. When several objects are added simultaneously, we still get a single event in the library — adding a single object on a chart out of several ones. To avoid this, we need to check if one object has been added to the chart and, if so, get the last added object. If several objects have been added at the same time, then we need to select each one in the loop through all chart objects and send an event about its addition.

I will slightly change this structure, however. First, I will create the list of all added objects. Events will be sent to the control program chart from that list afterwards. Newly added objects are checked in the Refresh() methods of the CChartObjectsControl chart management class objects, which we have for each of the open charts. In the methods, we create class objects for each added object, next we add them to the list and send messages about creating objects to the control program in the list by this loop. The library event handler is called from the program. It receives the necessary chart management object the messages have arrived from, retrieves the class objects of standard graphical objects and places them to the collection list.

After the improvements, the Refresh() method of the chart management object class is to look as follows:

//+------------------------------------------------------------------+
//| CChartObjectsControl: Check objects on a chart                   |
//+------------------------------------------------------------------+
void CChartObjectsControl::Refresh(void)
  {
//--- Clear the list of newly added objects
   this.m_list_new_graph_obj.Clear();
//--- Calculate the number of new objects on the chart
   this.m_total_objects=::ObjectsTotal(this.ChartID());
   this.m_delta_graph_obj=this.m_total_objects-this.m_last_objects;
   //--- If an object is added to the chart
   if(this.m_delta_graph_obj>0)
     {
      //--- Create the list of added graphical objects
      for(int i=0;i<this.m_delta_graph_obj;i++)
        {
         //--- Get the name of the last added object (if a single new object is added),
         //--- or a name from the terminal object list by index (if several objects have been added)
         string name=(this.m_delta_graph_obj==1 ? this.LastAddedGraphObjName() : ::ObjectName(this.m_chart_id,i));
         //--- Handle only non-programmatically created objects
         if(name==NULL || ::StringFind(name,this.m_name_program)>WRONG_VALUE)
            continue;
         //--- Create the object of the graphical object class corresponding to the added graphical object type
         ENUM_OBJECT type=(ENUM_OBJECT)::ObjectGetInteger(this.ChartID(),name,OBJPROP_TYPE);
         ENUM_OBJECT_DE_TYPE obj_type=ENUM_OBJECT_DE_TYPE(type+OBJECT_DE_TYPE_GSTD_OBJ+1);
         CGStdGraphObj *obj=this.CreateNewGraphObj(type,name);
         //--- If failed to create an object, inform of that and move on to the new iteration
         if(obj==NULL)
           {
            CMessage::ToLog(DFUN,MSG_GRAPH_STD_OBJ_ERR_FAILED_CREATE_CLASS_OBJ);
            continue;
           }
         //--- Set the object affiliation and add the created object to the list of new objects
         obj.SetBelong(GRAPH_OBJ_BELONG_NO_PROGRAM); 
         //--- If failed to add the object to the list, inform of that, remove the object and move on to the next iteration
         if(!this.m_list_new_graph_obj.Add(obj))
           {
            CMessage::ToLog(DFUN_ERR_LINE,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
            delete obj;
            continue;
           }
        }
      //--- Send events to the control program chart from the created list
      for(int i=0;i<this.m_list_new_graph_obj.Total();i++)
        {
         CGStdGraphObj *obj=this.m_list_new_graph_obj.At(i);
         if(obj==NULL)
            continue;
         //--- Send an event to the control program chart
         ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_CREATE,this.ChartID(),obj.TimeCreate(),obj.Name());
        }
     }
//--- save the index of the last added graphical object and the difference with the last check
   this.m_last_objects=this.m_total_objects;
   this.m_is_graph_obj_event=(bool)this.m_delta_graph_obj;
  }
//+------------------------------------------------------------------+

The method logic is fully described in the code comments. The dparam parameter of the EventChartCustom() function passes the graphical object creation time. This is necessary for identifying objects located on a restored chart, which was previously deleted. In other words, if we set graphical objects on an open chart, then remove that chart and restore it, the graphical objects set on it at the time of the deletion are restored as well. Such objects will have zero creation time. We can later use this time for identifying graphical objects restored together with the symbol chart.

In the method adding the event control indicator of the chart management class on the chart, make sure such an indicator is not present on the chart yet before adding it:

//+------------------------------------------------------------------+
//|CChartObjectsControl: Add the event control indicator to the chart|
//+------------------------------------------------------------------+
bool CChartObjectsControl::AddEventControlInd(void)
  {
   if(this.m_handle_ind==INVALID_HANDLE)
      return false;
   ::ResetLastError();
   string shortname="EventSend_From#"+(string)this.ChartID()+"_To#"+(string)this.m_chart_id_main;
   int total=::ChartIndicatorsTotal(this.ChartID(),0);
   for(int i=0;i<total;i++)
      if(::ChartIndicatorName(this.ChartID(),0,i)==shortname)
        {
         CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_ALREADY_EXIST_EVN_CTRL_INDICATOR);
         return true;
        }
   return ::ChartIndicatorAdd(this.ChartID(),0,this.m_handle_ind);
  }
//+------------------------------------------------------------------+

Here we first create the short indicator name used to search for indicators on the chart. Next, in the loop, get the next chart ID. If the short name of the obtained indicator matches the necessary one, inform that such an indicator is already present on the chart and return true. Upon the loop completion, return the result of adding the indicator to the chart.

In the private section of the graphical object collection class, declare the list of removed graphical objects, declare the overloaded method finding the object (present in the collection but absent on the chart), which, apart from the pointer to the detected object, also returns the object index in the list. Declare the methods relocating removed objects to the list of removed graphical objects.

//+------------------------------------------------------------------+
//| 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:
   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
   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
   
//--- Return the flag indicating the graphical element class object presence in the collection list of graphical elements
   bool              IsPresentGraphElmInList(const int id,const ENUM_GRAPH_ELEMENT_TYPE type_obj);
//--- Return the flag indicating the presence of the graphical object class in the graphical object collection list
   bool              IsPresentGraphObjInList(const long chart_id,const string name);
//--- Return the flag indicating the presence of a graphical object on a chart by name
   bool              IsPresentGraphObjOnChart(const long chart_id,const string name);
//--- Return the pointer to the object of managing objects of the specified chart
   CChartObjectsControl *GetChartObjectCtrlObj(const long chart_id);
//--- Create a new object of managing graphical objects of a specified chart and add it to the list
   CChartObjectsControl *CreateChartObjectCtrlObj(const long chart_id);
//--- Update the list of graphical objects by chart ID
   CChartObjectsControl *RefreshByChartID(const long chart_id);
//--- Check if the chart window is present
   bool              IsPresentChartWindow(const long chart_id);
//--- Handle removing the chart window
   void              RefreshForExtraObjects(void);
//--- Return the first free ID of the graphical (1) object and (2) element on canvas
   long              GetFreeGraphObjID(bool program_object);
   long              GetFreeCanvElmID(void);
//--- Add a graphical object to the collection
   bool              AddGraphObjToCollection(const string source,CChartObjectsControl *obj_control);
//--- 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);
//--- Create a new standard graphical object, return an object name
   bool              CreateNewStdGraphObject(const long chart_id,
                                             const string name,
                                             const ENUM_OBJECT type,
                                             const int subwindow,
                                             const datetime time1,
                                             const double price1,
                                             const datetime time2=0,
                                             const double price2=0,
                                             const datetime time3=0,
                                             const double price3=0,
                                             const datetime time4=0,
                                             const double price4=0,
                                             const datetime time5=0,
                                             const double price5=0);
public:


In the public section of the class, write the methods returning the lists of removed graphical objects by selected properties, declare the method returning the pointer to the removed graphical object and write the methods returning the list of removed graphical objects, the pointer to the last removed graphical object and the one returning the size of the graphical object properties array.
Finally, declare the method displaying the history of object renaming in the journal:

public:
//--- Return itself
   CGraphElementsCollection *GetObject(void)                                                             { return &this;                        }
//--- Return the full collection list of standard graphical objects "as is"
   CArrayObj        *GetListGraphObj(void)                                                               { return &this.m_list_all_graph_obj;   }
//--- Return the full collection list of graphical elements on canvas "as is"
   CArrayObj        *GetListCanvElm(void)                                                                { return &this.m_list_all_canv_elm_obj;}
//--- Return the list of graphical elements by a selected (1) integer, (2) real and (3) string properties meeting the compared criterion
   CArrayObj        *GetList(ENUM_CANV_ELEMENT_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode=EQUAL)             { return CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),property,value,mode);           }
   CArrayObj        *GetList(ENUM_CANV_ELEMENT_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode=EQUAL)            { return CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),property,value,mode);           }
   CArrayObj        *GetList(ENUM_CANV_ELEMENT_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode=EQUAL)            { return CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),property,value,mode);           }
//--- Return the list of existing graphical objects by a selected (1) integer, (2) real and (3) string properties meeting the compared criterion
   CArrayObj        *GetList(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index,long value,ENUM_COMPARER_TYPE mode=EQUAL)      { return CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),property,index,value,mode);    }
   CArrayObj        *GetList(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index,double value,ENUM_COMPARER_TYPE mode=EQUAL)     { return CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),property,index,value,mode);    }
   CArrayObj        *GetList(ENUM_GRAPH_OBJ_PROP_STRING property,int index,string value,ENUM_COMPARER_TYPE mode=EQUAL)     { return CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),property,index,value,mode);    }
//--- Return the list of removed graphical objects by a selected (1) integer, (2) real and (3) string properties meeting the compared criterion
   CArrayObj        *GetListDel(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index,long value,ENUM_COMPARER_TYPE mode=EQUAL)   { return CSelect::ByGraphicStdObjectProperty(this.GetListDeletedObj(),property,index,value,mode);  }
   CArrayObj        *GetListDel(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index,double value,ENUM_COMPARER_TYPE mode=EQUAL)  { return CSelect::ByGraphicStdObjectProperty(this.GetListDeletedObj(),property,index,value,mode);  }
   CArrayObj        *GetListDel(ENUM_GRAPH_OBJ_PROP_STRING property,int index,string value,ENUM_COMPARER_TYPE mode=EQUAL)  { return CSelect::ByGraphicStdObjectProperty(this.GetListDeletedObj(),property,index,value,mode);  }
//--- Return the number of new graphical objects, (3) the flag of the occurred change in the list of graphical objects
   int               NewObjects(void)   const                                                            { return this.m_delta_graph_obj;       }
   bool              IsEvent(void) const                                                                 { return this.m_is_graph_obj_event;    }
//--- Return an (1) existing and (2) removed graphical object by chart name and ID
   CGStdGraphObj    *GetStdGraphObject(const string name,const long chart_id);
   CGStdGraphObj    *GetStdDelGraphObject(const string name,const long chart_id);
//--- Return the list of (1) chart management objects and (2) removed graphical objects
   CArrayObj        *GetListChartsControl(void)                                                          { return &this.m_list_charts_control;  }
   CArrayObj        *GetListDeletedObj(void)                                                             { return &this.m_list_deleted_obj;     }
//--- Return (1) the last removed graphical object and (2) the array size of graphical object properties
   CGStdGraphObj    *GetLastDeletedGraphObj(void)                 const { return this.m_list_deleted_obj.At(this.m_list_deleted_obj.Total()-1); }
   int               GetSizeProperty(const string name,const long chart_id,const int prop)
                       {
                        CGStdGraphObj *obj=this.GetStdGraphObject(name,chart_id);
                        return(obj!=NULL ? obj.Properties().CurrSize(prop) : 0);
                       }
   
//--- Constructor
                     CGraphElementsCollection();
//--- Display the description of the object properties in the journal (full_prop=true - all properties, false - supported ones only - implemented in descendant classes)
   virtual void      Print(const bool full_prop=false,const bool dash=false);
//--- Display a short description of the object in the journal
   virtual void      PrintShort(const bool dash=false,const bool symbol=false);
//--- Display the history of renaming the object to the journal
   void              PrintRenameHistory(const string name,const long chart_id);

//--- Create the list of chart management objects and return the number of charts
   int               CreateChartControlList(void);
//--- Update the list of (1) all graphical objects, (2) on the specified chart, fill in the data on the number of new ones and set the event flag
   void              Refresh(void);
   void              Refresh(const long chart_id);
//--- Event handler
   void              OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam);

private:


In the class constructor, clear the list of removed graphical objects and set the sorted list flag:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CGraphElementsCollection::CGraphElementsCollection()
  {
   this.m_type=COLLECTION_GRAPH_OBJ_ID;
   ::ChartSetInteger(::ChartID(),CHART_EVENT_MOUSE_MOVE,true);
   ::ChartSetInteger(::ChartID(),CHART_EVENT_MOUSE_WHEEL,true);
   this.m_list_all_graph_obj.Type(COLLECTION_GRAPH_OBJ_ID);
   this.m_list_all_graph_obj.Sort(SORT_BY_CANV_ELEMENT_ID);
   this.m_list_all_graph_obj.Clear();
   this.m_list_charts_control.Sort();
   this.m_list_charts_control.Clear();
   this.m_total_objects=0;
   this.m_is_graph_obj_event=false;
   this.m_list_deleted_obj.Clear();
   this.m_list_deleted_obj.Sort();
  }
//+------------------------------------------------------------------+


In the Refresh() method of the graphical object collection class, we found only one removed object when handling the event of removing graphical objects from the chart. Now we need to find each removed object and send the appropriate event to the control program chart in the loop by the number of removed objects. This is done in the improved block of handling the removal of graphical objects:

//+------------------------------------------------------------------+
//| Update the list of all graphical objects                         |
//+------------------------------------------------------------------+
void CGraphElementsCollection::Refresh(void)
  {
   this.RefreshForExtraObjects();
//--- Declare variables to search for charts
   long chart_id=0;
   int i=0;
//--- In the loop by all open charts in the terminal (no more than 100)
   while(i<CHARTS_MAX)
     {
      //--- Get the chart ID
      chart_id=::ChartNext(chart_id);
      if(chart_id<0)
         break;
      //--- Get the pointer to the object for managing graphical objects
      //--- and update the list of graphical objects by chart ID
      CChartObjectsControl *obj_ctrl=this.RefreshByChartID(chart_id);
      //--- If failed to get the pointer, move on to the next chart
      if(obj_ctrl==NULL)
         continue;
      //--- If the number of objects on the chart changes
      if(obj_ctrl.IsEvent())
        {
         //--- If a graphical object is added to the chart
         if(obj_ctrl.Delta()>0)
           {
            //--- Get the list of added graphical objects and move them to the collection list
            //--- (if failed to move the object to the collection, move on to the next object)
            if(!this.AddGraphObjToCollection(DFUN_ERR_LINE,obj_ctrl))
               continue;
           }
         //--- If the graphical object has been removed
         else if(obj_ctrl.Delta()<0)
           {
            int index=WRONG_VALUE;
            //--- In the loop by the number of removed graphical objects
            for(int j=0;j<-obj_ctrl.Delta();j++)
              {
               // Find an extra object in the list
               CGStdGraphObj *obj=this.FindMissingObj(chart_id,index);
               if(obj!=NULL)
                 {
                  //--- Get the removed object parameters
                  long   lparam=obj.ChartID();
                  string sparam=obj.Name();
                  double dparam=(double)obj.TimeCreate();
                  //--- Move the graphical object class object to the list of removed objects
                  //--- and send the event to the control program chart
                  if(this.MoveGraphObjToDeletedObjList(index))
                     ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_DELETE,lparam,dparam,sparam);
                 }
              }
           }
        }
      //--- Increase the loop index
      i++;
     }
  }
//+------------------------------------------------------------------+


Previously, I removed the objects of classes describing graphical objects removed together with the chart window in the method handling a chart window removal. Now I am going to move them to the list of removed graphical objects. Later, the list will enable us to retrieve all properties of each object removed with the chart:

//+------------------------------------------------------------------+
//| Handle removing the chart window                                 |
//+------------------------------------------------------------------+
void CGraphElementsCollection::RefreshForExtraObjects(void)
  {
   for(int i=this.m_list_charts_control.Total()-1;i>WRONG_VALUE;i--)
     {
      CChartObjectsControl *obj_ctrl=this.m_list_charts_control.At(i);
      if(obj_ctrl==NULL)
         continue;
      if(!this.IsPresentChartWindow(obj_ctrl.ChartID()))
        {
         long chart_id=obj_ctrl.ChartID();
         string chart_symb=obj_ctrl.Symbol();
         int total_ctrl=this.m_list_charts_control.Total();
         this.DeleteGraphObjCtrlObjFromList(obj_ctrl);
         int total_obj=this.m_list_all_graph_obj.Total();
         this.MoveGraphObjectsToDeletedObjList(chart_id);
         int del_ctrl=total_ctrl-this.m_list_charts_control.Total();
         int del_obj=total_obj-this.m_list_all_graph_obj.Total();
         ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_DEL_CHART,chart_id,del_obj,chart_symb);
        }
     }
  }
//+------------------------------------------------------------------+


The method located in the collection but absent on the chart and returning the pointer to the object and its index in the list:

//+------------------------------------------------------------------+
//|Find an object present in the collection but not on a chart       |
//| Return the pointer to the object and its index in the list.      |
//+------------------------------------------------------------------+
CGStdGraphObj *CGraphElementsCollection::FindMissingObj(const long chart_id,int &index)
  {
   CArrayObj *list=CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),GRAPH_OBJ_PROP_CHART_ID,0,chart_id,EQUAL);
   if(list==NULL)
      return NULL;
   index=WRONG_VALUE;
   for(int i=0;i<list.Total();i++)
     {
      CGStdGraphObj *obj=list.At(i);
      if(obj==NULL)
         continue;
      if(!this.IsPresentGraphObjOnChart(obj.ChartID(),obj.Name()))
        {
         index=i;
         return obj;
        }
     }
   return NULL;
  }
//+------------------------------------------------------------------+

This method is an overload of the same-name method implemented earlier. Unlike its "partner", in addition to the pointer to a detected object, the method also returns its index, which may be necessary when accessing the detected object by its index in the list.


The method moving all objects by chart ID to the list of removed graphical objects:

//+------------------------------------------------------------------+
//| Move all objects (by chart ID)                                   |
//| to the list of removed graphical objects                         |
//+------------------------------------------------------------------+
void CGraphElementsCollection::MoveGraphObjectsToDeletedObjList(const long chart_id)
  {
   CArrayObj *list=GetList(GRAPH_OBJ_PROP_CHART_ID,0,chart_id,EQUAL);
   if(list==NULL)
      return;
   for(int i=list.Total()-1;i>WRONG_VALUE;i--)
     {
      CGStdGraphObj *obj=list.At(i);
      if(obj==NULL)
         continue;
      this.MoveGraphObjToDeletedObjList(obj);
     }
  }
//+------------------------------------------------------------------+

The method moves all objects with the specified chart ID to the list of removed objects.
Here we get the list of objects with the specified chart ID. Also, in the loop by the specified list, get the next object and move it to the list of removed graphical objects using the method considered below.


The method moving the graphical object class object by index to the list of removed objects:

//+------------------------------------------------------------------+
//| Move the class object of the graphical object by index           |
//| to the list of removed graphical objects                         |
//+------------------------------------------------------------------+
bool CGraphElementsCollection::MoveGraphObjToDeletedObjList(const int index)
  {
   CGStdGraphObj *obj=this.m_list_all_graph_obj.Detach(index);
   if(obj==NULL)
     {
      CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_DETACH_OBJ_FROM_LIST);
      return false;
     }
   if(!this.m_list_deleted_obj.Add(obj))
     {
      CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_ADD_OBJ_TO_DEL_LIST);
      delete obj;
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

The method moves the object, whose index has been passed to the method, from the collection list to the list of removed objects.
Here we retrieve the object from the list by the specified index. If failed to retrieve the object, inform of that and return false.
If failed to place the object to the list of removed objects, inform of that, remove the object and return false.
As a result, return true.

The method moving the specified graphical object class object to the list of removed graphical objects:

//+------------------------------------------------------------------+
//| Move the specified graphical object class object                 |
//| to the list of removed graphical objects                         |
//+------------------------------------------------------------------+
bool CGraphElementsCollection::MoveGraphObjToDeletedObjList(CGStdGraphObj *obj)
  {
   this.m_list_all_graph_obj.Sort();
   int index=this.m_list_all_graph_obj.Search(obj);
   return this.MoveGraphObjToDeletedObjList(index);
  }
//+------------------------------------------------------------------+

The method moves the object specified by the pointer to the list of removed graphical objects.
Here we set the sorted list flag to the collection list and get the object index in the list using the Search() method.
When using the method moving the object by its index in the list considered above, move the object to the list of removed graphical objects.

The method returning a removed graphical object by chart name and ID:

//+------------------------------------------------------------------+
//| Return a removed graphical object                                |
//| by chart name and ID                                             |
//+------------------------------------------------------------------+
CGStdGraphObj *CGraphElementsCollection::GetStdDelGraphObject(const string name,const long chart_id)
  {
   CArrayObj *list=this.GetListDel(GRAPH_OBJ_PROP_CHART_ID,0,chart_id);
   list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_NAME,0,name,EQUAL);
   return(list!=NULL && list.Total()>0 ? list.At(0) : NULL);
  }
//+------------------------------------------------------------------+

The object name and chart ID are passed to the method. Next, receive the list of objects by chart ID. From the obtained list, receive the list of objects by name (there should be only one such object). If the list has been received and its size exceeds zero, return the pointer to the single object in the list located at index 0. Otherwise, return NULL — failed to get the object.

From the event handler, remove the code block responsible for handling graphical object events. Move it to the EA event handler:

//+------------------------------------------------------------------+
//| 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);
         //--- Send an event with the old name of an object to the control program chart and
         //--- set a new name for the collection list object, which does not correspond to any graphical object on the chart
         ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_RENAME,obj.ChartID(),0,obj.Name());
         obj.SetName(name_new);
        }
      //--- Update the properties of the obtained object
      //--- and check their change
      obj.PropertiesRefresh();
      obj.PropertiesCheckChanged();
     }
//--- Handle standard graphical object events
   if(idx>GRAPH_OBJ_EVENT_NO_EVENT && idx<GRAPH_OBJ_EVENTS_NEXT_CODE)
     {
      //--- Depending on the event type, display an appropriate message in the journal
      switch(idx)
        {
         case GRAPH_OBJ_EVENT_CREATE   :
           ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CREATE));
           obj=this.GetStdGraphObject(sparam,lparam);
           if(obj!=NULL)
              obj.PrintShort();
           break;
         case GRAPH_OBJ_EVENT_CHANGE   :
           ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CHANGE));
           obj=this.GetStdGraphObject(sparam,lparam);
           if(obj!=NULL)
              obj.PrintShort();
           break;
         case GRAPH_OBJ_EVENT_RENAME   :
           ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_RENAME));
           obj=this.GetStdGraphObject(sparam,lparam);
           if(obj!=NULL)
              obj.PrintShort();
           break;
         case GRAPH_OBJ_EVENT_DELETE   :
           ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DELETE));
           break;
         case GRAPH_OBJ_EVENT_DEL_CHART:
           ::Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DEL_CHART),": ChartID: ",lparam,", ChartSymbol: ",sparam);
           break;
         default:
           break;
        }
     }
  }
//+------------------------------------------------------------------+

In the handler itself, fix the logic of sending the graphical object renaming message. If a new name has been set for the object, send the event to the control program chart:

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


The method displaying the object renaming history in the journal:

//+------------------------------------------------------------------+
//| Display the history of renaming the object to the journal        |
//+------------------------------------------------------------------+
void CGraphElementsCollection::PrintRenameHistory(const string name,const long chart_id)
  {
   CGStdGraphObj *obj=this.GetStdGraphObject(name,chart_id);
   if(obj==NULL)
      return;
   obj.PrintRenameHistory();
  }
//+------------------------------------------------------------------+

The method receives the graphical object name and the ID of the chart it is located on.
Get the pointer to the object by name and chart ID and call its method displaying the object renaming history in the journal using the previously considered PrintRenameHistory() method of the abstract standard graphical object class.

Let's add some methods to the CEngine library main class for more convenient work with the program.

Open \MQL5\Include\DoEasy\Engine.mqh and add the method returning the list of removed graphical objects, the method returning the number of removed graphical objects and the method returning the size of the specified property array:

//--- Launch the new pause countdown
   void                 Pause(const ulong pause_msc,const datetime time_start=0)
                          {
                           this.PauseSetWaitingMSC(pause_msc);
                           this.PauseSetTimeBegin(time_start*1000);
                           while(!this.PauseIsCompleted() && !::IsStopped()){}
                          }

//--- Return the (1) collection of graphical objects and (2) the list of removed objects
   CGraphElementsCollection *GetGraphicObjCollection(void)              { return &this.m_graph_objects;                       }
   CArrayObj           *GetListDeletedObj(void)                         { return this.m_graph_objects.GetListDeletedObj();    }
//--- Return (1) the number of removed graphical objects and (2) the size of the property array
   int                  TotalDeletedGraphObjects(void)                  { return this.GetListDeletedObj().Total();            }
   int                  GraphGetSizeProperty(const string name,const long chart_id,const int prop)
                          {
                           return this.m_graph_objects.GetSizeProperty(name,chart_id,prop);
                          }
   
//--- Fill in the array with IDs of the charts opened in the terminal

These methods return the result provided by the same-name methods of the graphical object collection class. I believe, no more details are needed here.


Test

To perform the test, let's use the EA from the previous article and save it to \MQL5\Experts\TestDoEasy\Part91\ as TestDoEasyPart91.mq5.

All we need to do is add the code block for handling graphical object events removed from the handler of graphical object collection class events to the EA's OnChartEvent() handler:

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- If working in the tester, exit
   if(MQLInfoInteger(MQL_TESTER))
      return;
//--- If the mouse is moved
   /*
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      CForm *form=NULL;
      datetime time=0;
      double price=0;
      int wnd=0;
      
      //--- If Ctrl is not pressed,
      if(!IsCtrlKeyPressed())
        {
         //--- clear the list of created form objects, allow scrolling a chart with the mouse and show the context menu
         list_forms.Clear();
         ChartSetInteger(ChartID(),CHART_MOUSE_SCROLL,true);
         ChartSetInteger(ChartID(),CHART_CONTEXT_MENU,true);
         return;
        }
      
      //--- If X and Y chart coordinates are successfully converted into time and price,
      if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,wnd,time,price))
        {
         //--- get the bar index the cursor is hovered over
         int index=iBarShift(Symbol(),PERIOD_CURRENT,time);
         if(index==WRONG_VALUE)
            return;
         
         //--- Get the bar index by index
         CBar *bar=engine.SeriesGetBar(Symbol(),Period(),index);
         if(bar==NULL)
            return;
         
         //--- Convert the coordinates of a chart from the time/price representation of the bar object to the X and Y coordinates
         int x=(int)lparam,y=(int)dparam;
         if(!ChartTimePriceToXY(ChartID(),0,bar.Time(),(bar.Open()+bar.Close())/2.0,x,y))
            return;
         
         //--- Disable moving a chart with the mouse and showing the context menu
         ChartSetInteger(ChartID(),CHART_MOUSE_SCROLL,false);
         ChartSetInteger(ChartID(),CHART_CONTEXT_MENU,false);
         
         //--- Create the form object name and hide all objects except one having such a name
         string name="FormBar_"+(string)index;
         HideFormAllExceptOne(name);
         
         //--- If the form object with such a name does not exist yet,
         if(!IsPresentForm(name))
           {
            //--- create a new form object
            form=bar.CreateForm(index,name,x,y,114,16);   
            if(form==NULL)
               return;
            
            //--- Set activity and unmoveability flags for the form
            form.SetActive(true);
            form.SetMovable(false);
            //--- Set the opacity of 200
            form.SetOpacity(200);
            //--- The form background color is set as the first color from the color array
            form.SetColorBackground(array_clr[0]);
            //--- Form outlining frame color
            form.SetColorFrame(C'47,70,59');
            //--- Draw the shadow drawing flag
            form.SetShadow(true);
            //--- Calculate the shadow color as the chart background color converted to the monochrome one
            color clrS=form.ChangeColorSaturation(form.ColorBackground(),-100);
            //--- If the settings specify the usage of the chart background color, replace the monochrome color with 20 units
            //--- Otherwise, use the color specified in the settings for drawing the shadow
            color clr=(InpUseColorBG ? form.ChangeColorLightness(clrS,-20) : InpColorForm3);
            //--- Draw the form shadow with the right-downwards offset from the form by three pixels along all axes
            //--- Set the shadow opacity to 200, while the blur radius is equal to 4
            form.DrawShadow(2,2,clr,200,3);
            //--- Fill the form background with a vertical gradient
            form.Erase(array_clr,form.Opacity());
            //--- Draw an outlining rectangle at the edges of the form
            form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,form.ColorFrame(),form.Opacity());
            //--- If failed to add the form object to the list, remove the form and exit the handler
            if(!list_forms.Add(form))
              {
               delete form;
               return;
              }
            //--- Capture the form appearance
            form.Done();
           }
         //--- If the form object exists,
         if(form!=NULL)
           {
            //--- draw a text with the bar type description on it and show the form. The description corresponds to the mouse cursor position
            form.TextOnBG(0,bar.BodyTypeDescription(),form.Width()/2,form.Height()/2-1,FRAME_ANCHOR_CENTER,C'7,28,21');
            form.Show();
           }
         //--- Re-draw the chart
         ChartRedraw();
        }
     }
   */
   if(id==CHARTEVENT_CLICK)
     {
      if(!IsCtrlKeyPressed())
         return;
      datetime time=0;
      double price=0;
      int sw=0;
      if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,sw,time,price))
        {
         long array[];
         engine.GraphGetArrayChartsID(array);
         for(int i=0;i<ArraySize(array);i++)
            engine.CreateLineVertical(array[i],"LineVertical",0,time);
        }
     }
   engine.GetGraphicObjCollection().OnChartEvent(id,lparam,dparam,sparam);

//--- Handle standard graphical object events
   ushort idx=ushort(id-CHARTEVENT_CUSTOM);
   CGStdGraphObj *obj=NULL;
   if(idx>GRAPH_OBJ_EVENT_NO_EVENT && idx<GRAPH_OBJ_EVENTS_NEXT_CODE)
     {
      CChartObjectsControl *chart_ctrl=NULL;
      int end=0;
      string evn="";
      //--- Depending on the event type, display an appropriate message in the journal
      switch(idx)
        {
         //--- Graphical object creation event
         case GRAPH_OBJ_EVENT_CREATE   :
           //--- Display the message about creating a new graphical object
           Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CREATE),":");
           //--- Get the pointer to the object by chart name and ID passed to sparam and lparam, respectively
           //--- and display the short description of a newly created object to the journal
           obj=engine.GetGraphicObjCollection().GetStdGraphObject(sparam,lparam);
           if(obj!=NULL)
             {
              obj.PrintShort();
             }
           break;
         //--- Event of changing the graphical object property
         case GRAPH_OBJ_EVENT_CHANGE   :
           //--- Display the message about changing the graphical object property
           Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_CHANGE),":");
           //--- Get the pointer to the object by chart name and ID passed to sparam and lparam, respectively
           obj=engine.GetGraphicObjCollection().GetStdGraphObject(sparam,lparam);
           if(obj!=NULL)
             {
              //--- Display a short description of the changed object in the journal
              obj.PrintShort();
              //--- calculate the code of the changed property passed to dparam and get the property description
              if(dparam<GRAPH_OBJ_PROP_INTEGER_TOTAL)
                 evn=obj.GetPropertyDescription((ENUM_GRAPH_OBJ_PROP_INTEGER)dparam);
              else if(dparam<GRAPH_OBJ_PROP_DOUBLE_TOTAL)
                 evn=obj.GetPropertyDescription((ENUM_GRAPH_OBJ_PROP_DOUBLE)dparam);
              else
                 evn=obj.GetPropertyDescription((ENUM_GRAPH_OBJ_PROP_STRING)dparam);
              //--- Display the description of the graphical object's changed property in the journal
              Print(DFUN,evn);
             }
           break;
         //--- Graphical object renaming event
         case GRAPH_OBJ_EVENT_RENAME   :
           //--- Display the message about renaming the graphical object
           Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_RENAME));
           //--- Get the pointer to the object by chart name and ID passed to sparam and lparam, respectively
           obj=engine.GetGraphicObjCollection().GetStdGraphObject(sparam,lparam);
           if(obj!=NULL)
             {
              //--- Display the previous and new object name, as well as its entire renaming history, in the journal
              Print(DFUN,obj.GetProperty(GRAPH_OBJ_PROP_NAME,obj.Properties().CurrSize(GRAPH_OBJ_PROP_NAME)-1)," >>> ",obj.GetProperty(GRAPH_OBJ_PROP_NAME,0));
              obj.PrintRenameHistory();
             }
           break;
         //--- Graphical object deletion event
         case GRAPH_OBJ_EVENT_DELETE   :
           //--- Display the message about removing the graphical object
           Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DELETE),":");
           //--- Get the pointer to the removed object by chart name and ID passed to sparam and lparam, respectively
           //--- and display a short description of the removed object in the journal
           obj=engine.GetGraphicObjCollection().GetStdDelGraphObject(sparam,lparam);
           if(obj!=NULL)
             {
              obj.PrintShort();
             }
           break;
         //--- Event of removing the graphical object together with the chart window
         case GRAPH_OBJ_EVENT_DEL_CHART:
           //--- Display the message about removing graphical objects together with the chart window, whose ID and symbol are passed to lparam and sparam
           Print(DFUN,CMessage::Text(MSG_GRAPH_OBJ_EVN_GRAPH_OBJ_DEL_CHART),": #",lparam,", ",sparam,":");
           //--- Calculate the end value for the loop by the list of removed graphical objects
           end=engine.TotalDeletedGraphObjects()-(int)dparam;
           if(end<0)
              end=0;
           //--- In the loop from the end of the removed graphical objects list up to the value calculated in the 'end' variable,
           for(int i=engine.TotalDeletedGraphObjects()-1;i>=end;i--)
             {
              //--- get the next removed graphical object from the list
              obj=engine.GetListDeletedObj().At(i);
              if(obj==NULL)
                 continue;
              //--- and display its brief description in the journal
              obj.PrintShort();
             }
           break;
         //---
         default:
           break;
        }
     }
  }
//+------------------------------------------------------------------+

The logic in the added code block is described in great detail. I believe, there is no need to dwell on it here.
If you have any questions, feel free to ask them in the comments below.

Compile the EA and launch it on the chart:


As we can see, the object renaming is saved in its "memory".

The package deletion of graphical objects is also handled correctly:


Keep in mind that not all graphical objects removed together with the chart are restored correctly in the graphical object collection list after the previously removed chart window is restored. I have not found the reason for skipping objects yet. But I will definitely fix this.

What's next?

In the next article, I will continue my work on graphical object events and start implementing the functionality for storing the object property change history in the object properties.

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 86): Graphical object collection - managing property modification
Graphics in DoEasy library (Part 87): Graphical object collection - managing object property modification on all open charts
Graphics in DoEasy library (Part 88): Graphical object collection — two-dimensional dynamic array for storing dynamically changing object properties
Graphics in DoEasy library (Part 89): Programming standard graphical objects. Basic functionality
Graphics in DoEasy library (Part 90): Standard graphical object events. Basic functionality

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

Attached files |
MQL5.zip (4181.64 KB)
Learn how to design a trading system by Bollinger Bands Learn how to design a trading system by Bollinger Bands
In this article, we will learn about Bollinger Bands which is one of the most popular indicators in the trading world. We will consider technical analysis and see how to design an algorithmic trading system based on the Bollinger Bands indicator.
Improved candlestick pattern recognition illustrated by the example of Doji Improved candlestick pattern recognition illustrated by the example of Doji
How to find more candlestick patterns than usual? Behind the simplicity of candlestick patterns, there is also a serious drawback, which can be eliminated by using the significantly increased capabilities of modern trading automation tools.
An Analysis of Why Expert Advisors Fail An Analysis of Why Expert Advisors Fail
This article presents an analysis of currency data to better understand why expert advisors can have good performance in some regions of time and poor performance in other regions of time.
Matrices and vectors in MQL5 Matrices and vectors in MQL5
By using special data types 'matrix' and 'vector', it is possible to create code which is very close to mathematical notation. With these methods, you can avoid the need to create nested loops or to mind correct indexing of arrays in calculations. Therefore, the use of matrix and vector methods increases the reliability and speed in developing complex programs.