Graphics in DoEasy library (Part 86): Graphical object collection - managing property modification
Contents
- Concept
- Tracking modification of graphical object properties
- Tracking graphical object deletion
- Test
- What's next?
Concept
In the previous article, I have implemented the ability to store newly created standard graphical objects in the collection class of graphical objects — we detect new chart objects, create objects of classes corresponding to the object type on the chart and add them to the collection list. However, this is insufficient for full-fledged management of graphical objects. We need to control all changes in the properties of graphical objects on a chart, as well as their deletion or renaming.
Since the functions from the ObjectGetXXX series are used to read graphical object properties, we cannot constantly check the values of each graphical object in the timer since these functions are synchronous. This means they wait for the command to be executed. This can be very resource-intensive in case of a large number of graphical objects.
Here we are faced with a choice. We can either use a timer to survey each property of each graphical object with all ensuing consequences of the synchronicity of the property request functions, or apply the event model by responding in the OnChartEvent() handler, which, unfortunately, does not work in the strategy tester (as you might remember, the timer operation in the tester is handled in the library using the OnTick() and OnCalculate() handlers).
After weighing all the pros and cons, I decided to track changes in the graphical object properties in the chart event handler. In other words, I will use the event model, which simplifies the code but imposes restrictions for working in the tester. The tester does not allow us to handle graphical objects (at least for now). We cannot add them to the tester window and change their properties afterwards. This means we should handle graphical objects only on "live" charts the event handler works on.
In this article, I will implement a test version of handling graphical object events for the current chart only (the one the program is launched on). As soon as I ensure that everything works correctly, I will develop full-fledged event handlers for each open chart. The handlers will send events to the main chart of the program where the library will collect and handle them in its graphical object collection.
Tracking modification of graphical object properties
In the OnChartEvent() handler, we are interested in the following events:
- CHARTEVENT_OBJECT_CREATE — create a graphical object (if CHART_EVENT_OBJECT_CREATE=true for a chart);
- CHARTEVENT_OBJECT_CHANGE — change object properties via the properties dialog;
- CHARTEVENT_OBJECT_DELETE — delete a graphical object (if CHART_EVENT_OBJECT_DELETE=true for a chart);
- CHARTEVENT_OBJECT_DRAG — drag a graphical object with a mouse.
I prepared the graphical object creation event in the previous article without calling the OnChartEvent() handler.
We need the event of changing the graphical object property via its terminal property dialog to manually control the changes of the object properties.
We already have the graphical object deletion event — the library tracks the number of graphical objects on all terminal charts and has event flags for each open chart — if the number of chart objects decreases, we are able to find out the number of objects removed from the chart and handle the situation.
We need the graphical object moving event to control changes in the graphical object location as a whole and its individual anchor points in particular.
The moving event is also activated when an object is created manually. The moment we click the chart to set an object and the mouse button is not released yet, the object is already created and the library is able to see it creating the appropriate class object and adding it to the collection. Not all object property values are set correctly. The mouse button has not been released yet and we can move the object or set its remaining anchor points if the object uses several points. But when I release the mouse button, the graphical object movement event is created provided that all object anchor points are already set. By tracking the event and changing the property values of the already created class object according to the fully set parameters of a created graphical object, we set the correct values of all properties of the newly created object.
Changing the object name entails three events at once — removing the object, creating the object and changing the object properties. These three events can be tracked to detect the change in the name of one of the existing objects. But I will use a more simple approach. When we change an object name, the CHARTEVENT_OBJECT_CHANGE event always comes last to be handled. Since all terminal objects are selected by their name and chart ID, we can check which object has become absent from the collection list out of the ones present on a chart. Then we find the object name on the chart, for which the class object is absent from the collection (1), find the object having no appropriately named chart object (2) and add that name to the (1) class object detected in the collection list. This may seem pretty complicated. However, all is actually simple.
In order to understand which object property has been changed (since we get the event having the name of the changed object but no indication of the changed property), we need to compare the current values of all object properties with the ones that were present before receiving the event of changing the object properties. Therefore, we need to create three more arrays to store "previous" object properties.
Open the file \MQL5\Include\DoEasy\Objects\Graph\Standard\GStdGraphObj.mqh of the abstract standard graphical object class and add new arrays for storing "previous" object properties in the private section:
//+------------------------------------------------------------------+ //| The class of the abstract standard graphical object | //+------------------------------------------------------------------+ class CGStdGraphObj : public CGBaseObj { private: long m_long_prop[GRAPH_OBJ_PROP_INTEGER_TOTAL]; // Integer properties double m_double_prop[GRAPH_OBJ_PROP_DOUBLE_TOTAL]; // Real properties string m_string_prop[GRAPH_OBJ_PROP_STRING_TOTAL]; // String properties long m_long_prop_prev[GRAPH_OBJ_PROP_INTEGER_TOTAL]; // Integer properties before change double m_double_prop_prev[GRAPH_OBJ_PROP_DOUBLE_TOTAL]; // Real properties before change string m_string_prop_prev[GRAPH_OBJ_PROP_STRING_TOTAL]; // String properties before change //--- Return the index of the array the (1) double and (2) string properties are actually located at int IndexProp(ENUM_GRAPH_OBJ_PROP_DOUBLE property) const { return(int)property-GRAPH_OBJ_PROP_INTEGER_TOTAL; } int IndexProp(ENUM_GRAPH_OBJ_PROP_STRING property) const { return(int)property-GRAPH_OBJ_PROP_INTEGER_TOTAL-GRAPH_OBJ_PROP_DOUBLE_TOTAL; } public:
In the private section of the class, set the methods for setting and returning "previous" object properties:
public: //--- Set object's (1) integer, (2) real and (3) string properties void SetProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property,long value) { this.m_long_prop[property]=value; } void SetProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property,double value) { this.m_double_prop[this.IndexProp(property)]=value; } void SetProperty(ENUM_GRAPH_OBJ_PROP_STRING property,string value) { this.m_string_prop[this.IndexProp(property)]=value; } //--- Return object’s (1) integer, (2) real and (3) string property from the properties array long GetProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property) const { return this.m_long_prop[property]; } double GetProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property) const { return this.m_double_prop[this.IndexProp(property)]; } string GetProperty(ENUM_GRAPH_OBJ_PROP_STRING property) const { return this.m_string_prop[this.IndexProp(property)]; } //--- Set object's previous (1) integer, (2) real and (3) string properties void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER property,long value) { this.m_long_prop_prev[property]=value; } void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE property,double value){ this.m_double_prop_prev[this.IndexProp(property)]=value;} void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING property,string value){ this.m_string_prop_prev[this.IndexProp(property)]=value;} //--- Return object’s (1) integer, (2) real and (3) string property from the previous properties array long GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER property) const { return this.m_long_prop_prev[property]; } double GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE property) const { return this.m_double_prop_prev[this.IndexProp(property)]; } string GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING property) const { return this.m_string_prop_prev[this.IndexProp(property)]; } //--- Return itself CGStdGraphObj *GetObject(void) { return &this;}
We have the "object ID" property for the class object describing a graphical object. The property allows us to set a unique object label allowing us to identify it.
This property should not be involved in defining the object events. Therefore, in the method of setting the object ID, we will set the value passed to the method into both properties at once — the current (already developed) and the previous one (to be added now):
public: //+--------------------------------------------------------------------+ //|Methods of simplified access and setting graphical object properties| //+--------------------------------------------------------------------+ //--- Object index in the list int Number(void) const { return (int)this.GetProperty(GRAPH_OBJ_PROP_NUM); } void SetNumber(const int number) { this.SetProperty(GRAPH_OBJ_PROP_NUM,number); } //--- Object ID long ObjectID(void) const { return this.GetProperty(GRAPH_OBJ_PROP_ID); } void SetObjectID(const long obj_id) { CGBaseObj::SetObjectID(obj_id); this.SetProperty(GRAPH_OBJ_PROP_ID,obj_id); this.SetPropertyPrev(GRAPH_OBJ_PROP_ID,obj_id); } //--- Graphical object type
The event response algorithm is as follows: after receiving an event, we need to update all data of the class object describing the graphical object so that all its properties have relevant values. Since we do not know which property has changed, we update all object properties and compare the current properties with the ones the object had before receiving the object change event. I will compare all three object property arrays with the appropriate arrays of previous properties in three loops. When comparing, the message about changing the property is temporarily (since this is a test version so far) sent to the journal when the current property value is not equal to the previous one. The same goes for each detected difference of compared values in each object property array. Later, when implementing control over property changes for each of the opened charts, I will introduce another method. All changed properties will be sent to the event object. Each such object will be received in the collection class to signal about changes in the properties of each of the objects on each open chart.
In the public section of the class, declare the method for overwriting all object properties. It will be needed to detect the property change event to go through all the properties of a graphical object at once and enter them into the class object properties.
The method checking the changes in the object properties will compare all current object properties with their previous state.
In the private section of the class, declare three methods for receiving all properties from the graphical object and setting them to the class object properties.
The method copying the current properties to the previous ones allows comparing the properties with the changed ones during the next check when an event is detected:
//--- Return the description of the object visibility on timeframes string VisibleOnTimeframeDescription(void); //--- Re-write all graphical object properties void PropertiesRefresh(void); //--- Check object property changes void PropertiesCheckChanged(void); private: //--- Get and save (1) integer, (2) real and (3) string properties void GetAndSaveINT(void); void GetAndSaveDBL(void); void GetAndSaveSTR(void); //--- Copy the current data to the previous one void PropertiesCopyToPrevData(void); }; //+------------------------------------------------------------------+
Let's simplify the protected parametric constructor. Previously, it received all properties inherent in all graphical objects from the object and set them in the class object:
//+------------------------------------------------------------------+ //| Protected parametric constructor | //+------------------------------------------------------------------+ CGStdGraphObj::CGStdGraphObj(const ENUM_OBJECT_DE_TYPE obj_type, const ENUM_GRAPH_OBJ_BELONG belong, const ENUM_GRAPH_OBJ_GROUP group, const long chart_id,const string name) { //--- Set the object (1) type, type of graphical (2) object, (3) element, (4) subwindow affiliation and (5) index, as well as (6) chart symbol Digits this.m_type=obj_type; CGBaseObj::SetChartID(chart_id); CGBaseObj::SetTypeGraphObject(CGBaseObj::GraphObjectType(obj_type)); CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_STANDARD); CGBaseObj::SetBelong(belong); CGBaseObj::SetGroup(group); CGBaseObj::SetSubwindow(chart_id,name); CGBaseObj::SetDigits((int)::SymbolInfoInteger(::ChartSymbol(chart_id),SYMBOL_DIGITS)); //--- Save integer properties //--- properties inherent in all graphical objects but not present in a graphical object this.m_long_prop[GRAPH_OBJ_PROP_CHART_ID] = CGBaseObj::ChartID(); // Chart ID this.m_long_prop[GRAPH_OBJ_PROP_WND_NUM] = CGBaseObj::SubWindow(); // Chart subwindow index this.m_long_prop[GRAPH_OBJ_PROP_TYPE] = CGBaseObj::TypeGraphObject(); // Graphical object type (ENUM_OBJECT) this.m_long_prop[GRAPH_OBJ_PROP_ELEMENT_TYPE]= CGBaseObj::TypeGraphElement(); // Graphical element type (ENUM_GRAPH_ELEMENT_TYPE) this.m_long_prop[GRAPH_OBJ_PROP_BELONG] = CGBaseObj::Belong(); // Graphical object affiliation this.m_long_prop[GRAPH_OBJ_PROP_GROUP] = CGBaseObj::Group(); // Graphical object group this.m_long_prop[GRAPH_OBJ_PROP_ID] = 0; // Object ID this.m_long_prop[GRAPH_OBJ_PROP_NUM] = 0; // Object index in the list //--- Properties inherent in all graphical objects and present in a graphical object this.m_long_prop[GRAPH_OBJ_PROP_CREATETIME] = ::ObjectGetInteger(chart_id,name,OBJPROP_CREATETIME); // Object creation time this.m_long_prop[GRAPH_OBJ_PROP_TIMEFRAMES] = ::ObjectGetInteger(chart_id,name,OBJPROP_TIMEFRAMES); // Object visibility on timeframes this.m_long_prop[GRAPH_OBJ_PROP_BACK] = ::ObjectGetInteger(chart_id,name,OBJPROP_BACK); // Background object this.m_long_prop[GRAPH_OBJ_PROP_ZORDER] = ::ObjectGetInteger(chart_id,name,OBJPROP_ZORDER); // Priority of a graphical object for receiving the event of clicking on a chart this.m_long_prop[GRAPH_OBJ_PROP_HIDDEN] = ::ObjectGetInteger(chart_id,name,OBJPROP_HIDDEN); // Disable displaying the name of a graphical object in the terminal object list this.m_long_prop[GRAPH_OBJ_PROP_SELECTED] = ::ObjectGetInteger(chart_id,name,OBJPROP_SELECTED); // Object selection this.m_long_prop[GRAPH_OBJ_PROP_SELECTABLE] = ::ObjectGetInteger(chart_id,name,OBJPROP_SELECTABLE); // Object availability this.m_long_prop[GRAPH_OBJ_PROP_TIME] = ::ObjectGetInteger(chart_id,name,OBJPROP_TIME); // First point time coordinate this.m_long_prop[GRAPH_OBJ_PROP_COLOR] = ::ObjectGetInteger(chart_id,name,OBJPROP_COLOR); // Color this.m_long_prop[GRAPH_OBJ_PROP_STYLE] = ::ObjectGetInteger(chart_id,name,OBJPROP_STYLE); // Style this.m_long_prop[GRAPH_OBJ_PROP_WIDTH] = ::ObjectGetInteger(chart_id,name,OBJPROP_WIDTH); // Line width //--- Properties belonging to different graphical objects this.m_long_prop[GRAPH_OBJ_PROP_FILL] = 0; // Object color filling this.m_long_prop[GRAPH_OBJ_PROP_READONLY] = 0; // Ability to edit text in the Edit object this.m_long_prop[GRAPH_OBJ_PROP_LEVELS] = 0; // Number of levels this.m_long_prop[GRAPH_OBJ_PROP_LEVELCOLOR] = 0; // Level line color this.m_long_prop[GRAPH_OBJ_PROP_LEVELSTYLE] = 0; // Level line style this.m_long_prop[GRAPH_OBJ_PROP_LEVELWIDTH] = 0; // Level line width this.m_long_prop[GRAPH_OBJ_PROP_ALIGN] = 0; // Horizontal text alignment in the Edit object (OBJ_EDIT) this.m_long_prop[GRAPH_OBJ_PROP_FONTSIZE] = 0; // Font size this.m_long_prop[GRAPH_OBJ_PROP_RAY_LEFT] = 0; // Ray goes to the left this.m_long_prop[GRAPH_OBJ_PROP_RAY_RIGHT] = 0; // Ray goes to the right this.m_long_prop[GRAPH_OBJ_PROP_RAY] = 0; // Vertical line goes through all windows of a chart this.m_long_prop[GRAPH_OBJ_PROP_ELLIPSE] = 0; // Display the full ellipse of the Fibonacci Arc object this.m_long_prop[GRAPH_OBJ_PROP_ARROWCODE] = 0; // Arrow code for the "Arrow" object this.m_long_prop[GRAPH_OBJ_PROP_ANCHOR] = 0; // Position of the binding point of the graphical object this.m_long_prop[GRAPH_OBJ_PROP_XDISTANCE] = 0; // Distance from the base corner along the X axis in pixels this.m_long_prop[GRAPH_OBJ_PROP_YDISTANCE] = 0; // Distance from the base corner along the Y axis in pixels this.m_long_prop[GRAPH_OBJ_PROP_DIRECTION] = 0; // Gann object trend this.m_long_prop[GRAPH_OBJ_PROP_DEGREE] = 0; // Elliott wave marking level this.m_long_prop[GRAPH_OBJ_PROP_DRAWLINES] = 0; // Display lines for Elliott wave marking this.m_long_prop[GRAPH_OBJ_PROP_STATE] = 0; // Button state (pressed/released) this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_CHART_ID] = 0; // Chart object ID (OBJ_CHART). this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_PERIOD] = 0; // Chart object period< this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_DATE_SCALE] = 0; // Time scale display flag for the Chart object this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_PRICE_SCALE] = 0; // Price scale display flag for the Chart object this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_CHART_SCALE] = 0; // Chart object scale this.m_long_prop[GRAPH_OBJ_PROP_XSIZE] = 0; // Object width along the X axis in pixels. this.m_long_prop[GRAPH_OBJ_PROP_YSIZE] = 0; // Object height along the Y axis in pixels. this.m_long_prop[GRAPH_OBJ_PROP_XOFFSET] = 0; // X coordinate of the upper-left corner of the visibility area. this.m_long_prop[GRAPH_OBJ_PROP_YOFFSET] = 0; // Y coordinate of the upper-left corner of the visibility area. this.m_long_prop[GRAPH_OBJ_PROP_BGCOLOR] = 0; // Background color for OBJ_EDIT, OBJ_BUTTON, OBJ_RECTANGLE_LABEL this.m_long_prop[GRAPH_OBJ_PROP_CORNER] = 0; // Chart corner for binding a graphical object this.m_long_prop[GRAPH_OBJ_PROP_BORDER_TYPE] = 0; // Border type for "Rectangle border" this.m_long_prop[GRAPH_OBJ_PROP_BORDER_COLOR] = 0; // Border color for OBJ_EDIT and OBJ_BUTTON //--- Save real properties this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_PRICE)] = ::ObjectGetDouble(chart_id,name,OBJPROP_PRICE); // Price coordinate this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_LEVELVALUE)] = 0; // Level value this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_SCALE)] = 0; // Scale (property of Gann objects and Fibonacci Arcs objects) this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_ANGLE)] = 0; // Angle this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_DEVIATION)] = 0; // Deviation of the standard deviation channel //--- Save string properties this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_NAME)] = name; // Object name this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_TEXT)] = ::ObjectGetString(chart_id,name,OBJPROP_TEXT); // Object description (the text contained in the object) this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_TOOLTIP)] = ::ObjectGetString(chart_id,name,OBJPROP_TOOLTIP);// Tooltip text this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_LEVELTEXT)] = ""; // Level description this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_FONT)] = ""; // Font this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_BMPFILE)] = ""; // BMP file name for the "Bitmap Level" object this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL)]= ""; // Chart object symbol //--- Save basic properties in the parent object this.m_create_time=(datetime)this.GetProperty(GRAPH_OBJ_PROP_CREATETIME); this.m_back=(bool)this.GetProperty(GRAPH_OBJ_PROP_BACK); this.m_selected=(bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTED); this.m_selectable=(bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTABLE); this.m_hidden=(bool)this.GetProperty(GRAPH_OBJ_PROP_HIDDEN); this.m_name=this.GetProperty(GRAPH_OBJ_PROP_NAME); } //+-------------------------------------------------------------------+
Now all these strings are moved to separate methods that are to be called from the PropertiesRefresh() method in one go.
Therefore, let's remove these strings. So now the constructor looks as follows:
//+------------------------------------------------------------------+ //| Protected parametric constructor | //+------------------------------------------------------------------+ CGStdGraphObj::CGStdGraphObj(const ENUM_OBJECT_DE_TYPE obj_type, const ENUM_GRAPH_OBJ_BELONG belong, const ENUM_GRAPH_OBJ_GROUP group, const long chart_id,const string name) { //--- Set the object (1) type, type of graphical (2) object, (3) element, (4) subwindow affiliation and (5) index, as well as (6) chart symbol Digits this.m_type=obj_type; this.SetName(name); CGBaseObj::SetChartID(chart_id); CGBaseObj::SetTypeGraphObject(CGBaseObj::GraphObjectType(obj_type)); CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_STANDARD); CGBaseObj::SetBelong(belong); CGBaseObj::SetGroup(group); CGBaseObj::SetSubwindow(chart_id,name); CGBaseObj::SetDigits((int)::SymbolInfoInteger(::ChartSymbol(chart_id),SYMBOL_DIGITS)); //--- Save integer properties //--- properties inherent in all graphical objects but not present in a graphical object this.m_long_prop[GRAPH_OBJ_PROP_CHART_ID] = CGBaseObj::ChartID(); // Chart ID this.m_long_prop[GRAPH_OBJ_PROP_WND_NUM] = CGBaseObj::SubWindow(); // Chart subwindow index this.m_long_prop[GRAPH_OBJ_PROP_TYPE] = CGBaseObj::TypeGraphObject(); // Graphical object type (ENUM_OBJECT) this.m_long_prop[GRAPH_OBJ_PROP_ELEMENT_TYPE]= CGBaseObj::TypeGraphElement(); // Graphical element type (ENUM_GRAPH_ELEMENT_TYPE) this.m_long_prop[GRAPH_OBJ_PROP_BELONG] = CGBaseObj::Belong(); // Graphical object affiliation this.m_long_prop[GRAPH_OBJ_PROP_GROUP] = CGBaseObj::Group(); // Graphical object group this.m_long_prop[GRAPH_OBJ_PROP_ID] = 0; // Object ID this.m_long_prop[GRAPH_OBJ_PROP_NUM] = 0; // Object index in the list //--- Save the properties inherent in all graphical objects and present in a graphical object this.PropertiesRefresh(); //--- Save basic properties in the parent object this.m_create_time=(datetime)this.GetProperty(GRAPH_OBJ_PROP_CREATETIME); this.m_back=(bool)this.GetProperty(GRAPH_OBJ_PROP_BACK); this.m_selected=(bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTED); this.m_selectable=(bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTABLE); this.m_hidden=(bool)this.GetProperty(GRAPH_OBJ_PROP_HIDDEN); //--- Save the current properties to the previous ones this.PropertiesCopyToPrevData(); } //+-------------------------------------------------------------------+
In order for the PropertiesRefresh() method to work correctly, it should know the name of the graphical object it should get the data from. Previously, the name was written almost at the very end — in the block for reading string parameters. Now, the graphical object name is read and set to the class object properties immediately upon entering the constructor. After adding all the properties to the class object, call the PropertiesCopyToPrevData() method which already sets all saved object properties to the arrays of "previous" properties to control their changes.
The methods receiving integer, real and string properties from the graphical object and saving them to the class object:
//+------------------------------------------------------------------+ //| Get and save the integer properties | //+------------------------------------------------------------------+ void CGStdGraphObj::GetAndSaveINT(void) { //--- Properties inherent in all graphical objects and present in a graphical object this.m_long_prop[GRAPH_OBJ_PROP_CREATETIME] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_CREATETIME); // Object creation time this.m_long_prop[GRAPH_OBJ_PROP_TIMEFRAMES] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_TIMEFRAMES); // Object visibility on timeframes this.m_long_prop[GRAPH_OBJ_PROP_BACK] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_BACK); // Background object this.m_long_prop[GRAPH_OBJ_PROP_ZORDER] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_ZORDER); // Priority of a graphical object for receiving the event of clicking on a chart this.m_long_prop[GRAPH_OBJ_PROP_HIDDEN] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_HIDDEN); // Disable displaying the name of a graphical object in the terminal object list this.m_long_prop[GRAPH_OBJ_PROP_SELECTED] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_SELECTED); // Object selection this.m_long_prop[GRAPH_OBJ_PROP_SELECTABLE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_SELECTABLE); // Object availability this.m_long_prop[GRAPH_OBJ_PROP_TIME] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_TIME); // First point time coordinate this.m_long_prop[GRAPH_OBJ_PROP_COLOR] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_COLOR); // Color this.m_long_prop[GRAPH_OBJ_PROP_STYLE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_STYLE); // Style this.m_long_prop[GRAPH_OBJ_PROP_WIDTH] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_WIDTH); // Line width //--- Properties belonging to different graphical objects this.m_long_prop[GRAPH_OBJ_PROP_FILL] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_FILL); // Fill an object with color this.m_long_prop[GRAPH_OBJ_PROP_READONLY] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_READONLY); // Ability to edit text in the Edit object this.m_long_prop[GRAPH_OBJ_PROP_LEVELS] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_LEVELS); // Number of levels this.m_long_prop[GRAPH_OBJ_PROP_LEVELCOLOR] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_LEVELCOLOR); // Level line color this.m_long_prop[GRAPH_OBJ_PROP_LEVELSTYLE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_LEVELSTYLE); // Level line style this.m_long_prop[GRAPH_OBJ_PROP_LEVELWIDTH] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_LEVELWIDTH); // Level line width this.m_long_prop[GRAPH_OBJ_PROP_ALIGN] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_ALIGN); // Horizontal text alignment in the Edit object (OBJ_EDIT) this.m_long_prop[GRAPH_OBJ_PROP_FONTSIZE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_FONTSIZE); // Font size this.m_long_prop[GRAPH_OBJ_PROP_RAY_LEFT] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_RAY_LEFT); // Ray goes to the left this.m_long_prop[GRAPH_OBJ_PROP_RAY_RIGHT] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_RAY_RIGHT); // Ray goes to the right this.m_long_prop[GRAPH_OBJ_PROP_RAY] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_RAY); // Vertical line goes through all windows of a chart this.m_long_prop[GRAPH_OBJ_PROP_ELLIPSE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_ELLIPSE); // Display the full ellipse of the Fibonacci Arc object this.m_long_prop[GRAPH_OBJ_PROP_ARROWCODE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_ARROWCODE); // Arrow code for the "Arrow" object this.m_long_prop[GRAPH_OBJ_PROP_ANCHOR] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_ANCHOR); // Position of the binding point of the graphical object this.m_long_prop[GRAPH_OBJ_PROP_XDISTANCE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_XDISTANCE); // Distance from the base corner along the X axis in pixels this.m_long_prop[GRAPH_OBJ_PROP_YDISTANCE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_YDISTANCE); // Distance from the base corner along the Y axis in pixels this.m_long_prop[GRAPH_OBJ_PROP_DIRECTION] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_DIRECTION); // Gann object trend this.m_long_prop[GRAPH_OBJ_PROP_DEGREE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_DEGREE); // Elliott wave marking level this.m_long_prop[GRAPH_OBJ_PROP_DRAWLINES] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_DRAWLINES); // Display lines for Elliott wave marking this.m_long_prop[GRAPH_OBJ_PROP_STATE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_STATE); // Button state (pressed/released) this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_CHART_ID] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_CHART_ID); // Chart object ID (OBJ_CHART). this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_PERIOD] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_PERIOD); // Chart object period this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_DATE_SCALE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_DATE_SCALE); // Time scale display flag for the Chart object this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_PRICE_SCALE]= ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_PRICE_SCALE);// Price scale display flag for the Chart object this.m_long_prop[GRAPH_OBJ_PROP_CHART_OBJ_CHART_SCALE]= ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_CHART_SCALE);// Chart object scale this.m_long_prop[GRAPH_OBJ_PROP_XSIZE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_XSIZE); // Object width along the X axis in pixels. this.m_long_prop[GRAPH_OBJ_PROP_YSIZE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_YSIZE); // Object height along the Y axis in pixels. this.m_long_prop[GRAPH_OBJ_PROP_XOFFSET] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_XOFFSET); // X coordinate of the upper-left corner of the visibility area. this.m_long_prop[GRAPH_OBJ_PROP_YOFFSET] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_YOFFSET); // Y coordinate of the upper-left corner of the visibility area. this.m_long_prop[GRAPH_OBJ_PROP_BGCOLOR] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_BGCOLOR); // Background color for OBJ_EDIT, OBJ_BUTTON, OBJ_RECTANGLE_LABEL this.m_long_prop[GRAPH_OBJ_PROP_CORNER] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_CORNER); // Chart corner for binding a graphical object this.m_long_prop[GRAPH_OBJ_PROP_BORDER_TYPE] = ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_BORDER_TYPE);// Border type for "Rectangle border" this.m_long_prop[GRAPH_OBJ_PROP_BORDER_COLOR]= ::ObjectGetInteger(this.ChartID(),this.Name(),OBJPROP_BORDER_COLOR);// Border color for OBJ_EDIT and OBJ_BUTTON } //+------------------------------------------------------------------+ //| Get and save the real properties | //+------------------------------------------------------------------+ void CGStdGraphObj::GetAndSaveDBL(void) { this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_PRICE)] = ::ObjectGetDouble(this.ChartID(),this.Name(),OBJPROP_PRICE); // Price coordinate this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_LEVELVALUE)] = ::ObjectGetDouble(this.ChartID(),this.Name(),OBJPROP_LEVELVALUE); // Level value this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_SCALE)] = ::ObjectGetDouble(this.ChartID(),this.Name(),OBJPROP_SCALE); // Scale (property of Gann objects and Fibonacci Arcs objects) this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_ANGLE)] = ::ObjectGetDouble(this.ChartID(),this.Name(),OBJPROP_ANGLE); // Corner this.m_double_prop[this.IndexProp(GRAPH_OBJ_PROP_DEVIATION)] = ::ObjectGetDouble(this.ChartID(),this.Name(),OBJPROP_DEVIATION); // Deviation of the standard deviation channel } //+------------------------------------------------------------------+ //| Get and save the string properties | //+------------------------------------------------------------------+ void CGStdGraphObj::GetAndSaveSTR(void) { this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_TEXT)] = ::ObjectGetString(this.ChartID(),this.Name(),OBJPROP_TEXT); // Object description (the text contained in the object) this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_TOOLTIP)] = ::ObjectGetString(this.ChartID(),this.Name(),OBJPROP_TOOLTIP); // Tooltip text this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_LEVELTEXT)] = ::ObjectGetString(this.ChartID(),this.Name(),OBJPROP_LEVELTEXT);// Level description this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_FONT)] = ::ObjectGetString(this.ChartID(),this.Name(),OBJPROP_FONT); // Font this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_BMPFILE)] = ::ObjectGetString(this.ChartID(),this.Name(),OBJPROP_BMPFILE); // BMP file name for the "Bitmap Level" object this.m_string_prop[this.IndexProp(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL)] = ::ObjectGetString(this.ChartID(),this.Name(),OBJPROP_SYMBOL); // Chart object symbol } //+------------------------------------------------------------------+
The class constructor strings replaced with the method overwriting all graphical object properties have been moved to these three methods:
//+------------------------------------------------------------------+ //| Overwrite all graphical object properties | //+------------------------------------------------------------------+ void CGStdGraphObj::PropertiesRefresh(void) { this.GetAndSaveINT(); this.GetAndSaveDBL(); this.GetAndSaveSTR(); } //+------------------------------------------------------------------+
All three methods considered above are called here one by one.
The method copying the current class object properties to the previous one:
//+------------------------------------------------------------------+ //| Copy the current data to the previous one | //+------------------------------------------------------------------+ void CGStdGraphObj::PropertiesCopyToPrevData(void) { ::ArrayCopy(this.m_long_prop_prev,this.m_long_prop); ::ArrayCopy(this.m_double_prop_prev,this.m_double_prop); ::ArrayCopy(this.m_string_prop_prev,this.m_string_prop); } //+------------------------------------------------------------------+
Here copy the arrays of integer, real and string properties to the appropriate arrays of previous properties one by one using the array copying function.
The method checking the object property changes:
//+------------------------------------------------------------------+ //| Check object property changes | //+------------------------------------------------------------------+ void CGStdGraphObj::PropertiesCheckChanged(void) { bool changed=false; int beg=0, end=GRAPH_OBJ_PROP_INTEGER_TOTAL; for(int i=beg; i<end; i++) { ENUM_GRAPH_OBJ_PROP_INTEGER prop=(ENUM_GRAPH_OBJ_PROP_INTEGER)i; if(!this.SupportProperty(prop)) continue; if(this.GetProperty(prop)!=this.GetPropertyPrev(prop)) { changed=true; ::Print(DFUN,this.Name(),": ",TextByLanguage(" Изменённое свойство: "," Modified property: "),GetPropertyDescription(prop)); } } beg=end; end+=GRAPH_OBJ_PROP_DOUBLE_TOTAL; for(int i=beg; i<end; i++) { ENUM_GRAPH_OBJ_PROP_DOUBLE prop=(ENUM_GRAPH_OBJ_PROP_DOUBLE)i; if(!this.SupportProperty(prop)) continue; if(this.GetProperty(prop)!=this.GetPropertyPrev(prop)) { changed=true; ::Print(DFUN,this.Name(),": ",TextByLanguage(" Изменённое свойство: "," Modified property: "),GetPropertyDescription(prop)); } } beg=end; end+=GRAPH_OBJ_PROP_STRING_TOTAL; for(int i=beg; i<end; i++) { ENUM_GRAPH_OBJ_PROP_STRING prop=(ENUM_GRAPH_OBJ_PROP_STRING)i; if(!this.SupportProperty(prop)) continue; if(this.GetProperty(prop)!=this.GetPropertyPrev(prop)) { changed=true; ::Print(DFUN,this.Name(),": ",TextByLanguage(" Изменённое свойство: "," Modified property: "),GetPropertyDescription(prop)); } } if(changed) PropertiesCopyToPrevData(); } //+------------------------------------------------------------------+
Here in three loops (for integer, real and string properties separately), get the next property from the appropriate array and compare it with the same property in the array of previous properties. If the compared values of the current and previous properties are not equal, the property has changed. Set the flag of changes in the object properties and display the message in the journal.
This is a test method meant only for checking the concept of searching for object property changes. In the coming articles, I will improve the concept to make it fully functional. I will track changes in all graphical objects on all open charts and send object property change events to the control program chart so that they are further handled by the library.
Tracking graphical object deletion
Changing graphical object properties is tracked in the object class since the graphical object properties belong to the object. They are set in the object class and can be checked there. However, all methods of object property changes and change verifications created above will be called from the graphical object collection class. In contrast, adding and removing graphical objects from a chart can be tracked only in the graphical object collection class. This class manages the full list of all objects on all open charts and keeps track of them in its own collection list.
Open the file of the graphical object collection class \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh and make all the necessary improvements in it.
In the chart object management class located in the same file, declare the private method setting the permission to track mouse and graphical object events. This allows setting permissions to track such events for each of the open charts.
In the class constructors, call these methods for setting permissions for charts the control objects are to be created for. At the end of the class listing, declare the event handler (I will implement it in the next article):
//+------------------------------------------------------------------+ //| Chart object management class | //+------------------------------------------------------------------+ class CChartObjectsControl : public CObject { private: CArrayObj m_list_new_graph_obj; // List of added graphical objects ENUM_TIMEFRAMES m_chart_timeframe; // Chart timeframe long m_chart_id; // Chart ID string m_chart_symbol; // Chart symbol bool m_is_graph_obj_event; // Event flag in the list of graphical objects int m_total_objects; // Number of graphical objects int m_last_objects; // Number of graphical objects during the previous check int m_delta_graph_obj; // Difference in the number of graphical objects compared to the previous check //--- Return the name of the last graphical object added to the chart string LastAddedGraphObjName(void); //--- Set the permission to track mouse events and graphical objects void SetMouseEvent(void); public: //--- Return the variable values ENUM_TIMEFRAMES Timeframe(void) const { return this.m_chart_timeframe; } long ChartID(void) const { return this.m_chart_id; } string Symbol(void) const { return this.m_chart_symbol; } bool IsEvent(void) const { return this.m_is_graph_obj_event; } int TotalObjects(void) const { return this.m_total_objects; } int Delta(void) const { return this.m_delta_graph_obj; } //--- Create a new standard graphical object CGStdGraphObj *CreateNewGraphObj(const ENUM_OBJECT obj_type,const long chart_id, const string name); //--- Return the list of newly added objects CArrayObj *GetListNewAddedObj(void) { return &this.m_list_new_graph_obj;} //--- Check the chart objects void Refresh(void); //--- Constructors CChartObjectsControl(void) { this.m_list_new_graph_obj.Clear(); this.m_list_new_graph_obj.Sort(); this.m_chart_id=::ChartID(); this.m_chart_timeframe=(ENUM_TIMEFRAMES)::ChartPeriod(this.m_chart_id); this.m_chart_symbol=::ChartSymbol(this.m_chart_id); this.m_is_graph_obj_event=false; this.m_total_objects=0; this.m_last_objects=0; this.m_delta_graph_obj=0; this.SetMouseEvent(); } CChartObjectsControl(const long chart_id) { this.m_list_new_graph_obj.Clear(); this.m_list_new_graph_obj.Sort(); this.m_chart_id=chart_id; this.m_chart_timeframe=(ENUM_TIMEFRAMES)::ChartPeriod(this.m_chart_id); this.m_chart_symbol=::ChartSymbol(this.m_chart_id); this.m_is_graph_obj_event=false; this.m_total_objects=0; this.m_last_objects=0; this.m_delta_graph_obj=0; this.SetMouseEvent(); } //--- Compare CChartObjectsControl objects by a chart ID (for sorting the list by an object property) virtual int Compare(const CObject *node,const int mode=0) const { const CChartObjectsControl *obj_compared=node; return(this.ChartID()>obj_compared.ChartID() ? 1 : this.ChartID()<obj_compared.ChartID() ? -1 : 0); } //--- Event handler void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam); }; //+------------------------------------------------------------------+
Beyond the class body, implement the method for setting permissions to track mouse and graphical object events:
//+------------------------------------------------------------------+ //| Set the permission | //| to track mouse and graphical object events for the chart | //+------------------------------------------------------------------+ void CChartObjectsControl::SetMouseEvent(void) { ::ChartSetInteger(this.ChartID(),CHART_EVENT_MOUSE_MOVE,true); ::ChartSetInteger(this.ChartID(),CHART_EVENT_MOUSE_WHEEL,true); ::ChartSetInteger(this.ChartID(),CHART_EVENT_OBJECT_CREATE,true); ::ChartSetInteger(this.ChartID(),CHART_EVENT_OBJECT_DELETE,true); } //+------------------------------------------------------------------+
Now let's focus on the graphical object collection class.
In the private section of the class, declare new methods with their functions clearly stated in their descriptions:
//+------------------------------------------------------------------+ //| Collection of graphical objects | //+------------------------------------------------------------------+ 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 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 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 graphical element object presence in the collection list of graphical objects 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); //--- Return the first free ID of the graphical (1) object and (2) element on canvas long GetFreeGraphObjID(void); 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); //--- Find the graphical object present on a chart but not in the collection string FindExtraObj(const long chart_id); //--- Remove the graphical object from the graphical object collection list bool DeleteGraphObjFromList(CGStdGraphObj *obj); public:
In the public section of the class, declare the method returning a graphical object by chart name and ID and the chart event handler:
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 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,long value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),property,value,mode); } CArrayObj *GetList(ENUM_GRAPH_OBJ_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),property,value,mode); } CArrayObj *GetList(ENUM_GRAPH_OBJ_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),property,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 a graphical object by chart name and ID CGStdGraphObj *GetStdGraphObject(const string name,const long chart_id); //--- 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); //--- 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); }; //+------------------------------------------------------------------+
In the method updating the list of all graphical objects, add the block handling the removal of a graphical object from a chart:
//+------------------------------------------------------------------+ //| Update the list of all graphical objects | //+------------------------------------------------------------------+ void CGraphElementsCollection::Refresh(void) { //--- 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(!AddGraphObjToCollection(DFUN_ERR_LINE,obj_ctrl)) continue; } //--- If the graphical object has been removed else if(obj_ctrl.Delta()<0) { // Find an extra object in the list CGStdGraphObj *obj=this.FindMissingObj(chart_id); if(obj!=NULL) { //--- Display a short description of a detected object deleted from a chart in the journal obj.PrintShort(); //--- Remove the class object of a removed graphical object from the collection list if(!this.DeleteGraphObjFromList(obj)) CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_DETACH_OBJ_FROM_LIST); } } //--- otherwise else { } } //--- Increase the loop index i++; } } //+------------------------------------------------------------------+
In this method, we check the presence of an event in each control object in a loop by the total number of chart control objects. If the event is present, check the value, by which the number of objects on the chart (managed by the chart control object) has changed. I have implemented handling adding an object in the previous article. Here I have introduced handling the negative value of a change in the chart object number.
Here all is simple: first, we search for a collection list object having no graphical object on a chart and remove it from the collection list.
The method searching for an object present in the collection but not on a chart:
//+------------------------------------------------------------------+ //|Find an object present in the collection but not on a chart | //+------------------------------------------------------------------+ CGStdGraphObj *CGraphElementsCollection::FindMissingObj(const long chart_id) { CArrayObj *list=CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),GRAPH_OBJ_PROP_CHART_ID,chart_id,EQUAL); if(list==NULL) return NULL; for(int i=0;i<list.Total();i++) { CGStdGraphObj *obj=list.At(i); if(obj==NULL) continue; if(!this.IsPresentGraphObjOnChart(obj.ChartID(),obj.Name())) return obj; } return NULL; } //+------------------------------------------------------------------+
Here we get the list of all objects with their chart IDs equal to the one specified in the method parameters.
In the loop by the obtained list, get the next object of the standard graphical object class. If the chart has no object with such a name, return the pointer to the object.
Upon completing the loop, return NULL.
The method searching for an object present on a chart but not in the collection:
//+------------------------------------------------------------------+ //|Find an object present on a chart but not in the collection | //+------------------------------------------------------------------+ string CGraphElementsCollection::FindExtraObj(const long chart_id) { int total=::ObjectsTotal(chart_id); for(int i=0;i<total;i++) { string name=::ObjectName(chart_id,i); if(!this.IsPresentGraphObjInList(chart_id,name)) return name; } return NULL; } //+------------------------------------------------------------------+
Here, in the loop by all objects in the terminal list, get the name of the next object. If there is no object with such a name and chart ID in the collection list, return the graphical object name. Upon the loop completion, return NULL.
The method returning the flag indicating the presence of the graphical object class in the graphical object collection list:
//+------------------------------------------------------------------------------+ //| Return the flag indicating the presence of the graphical object class object | //| in the graphical object collection list | //+------------------------------------------------------------------------------+ bool CGraphElementsCollection::IsPresentGraphObjInList(const long chart_id,const string name) { CArrayObj *list=CSelect::ByGraphicStdObjectProperty(this.GetListGraphObj(),GRAPH_OBJ_PROP_CHART_ID,chart_id,EQUAL); list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_NAME,name,EQUAL); return(list==NULL || list.Total()==0 ? false : true); } //+------------------------------------------------------------------+
Get the list of objects featuring the specified chart ID. Get the pointer to the object, whose name matches the necessary one, from the obtained list. If failed to obtain the list or it is empty, return false — object is not found, otherwise — return true.
The method returning the flag indicating the presence of a graphical object on a chart by name:
//+----------------------------------------------------------------------------------+ //| Return the flag indicating the presence of a graphical object on a chart by name | //+----------------------------------------------------------------------------------+ bool CGraphElementsCollection::IsPresentGraphObjOnChart(const long chart_id,const string name) { int total=::ObjectsTotal(chart_id); for(int i=0;i<total;i++) if(::ObjectName(chart_id,i)==name) return true; return false; } //+-------------------------------------------------------------------+
In the loop by the total number of all graphical objects, get the name of the next object on a chart specified by ID. If the name matches the necessary one, return true. Upon the loop completion, return false.
The method removing the graphical object from the graphical object collection list:
//+---------------------------------------------------------------------+ //|Remove the graphical object from the graphical object collection list| //+---------------------------------------------------------------------+ bool CGraphElementsCollection::DeleteGraphObjFromList(CGStdGraphObj *obj) { this.m_list_all_graph_obj.Sort(); int index=this.m_list_all_graph_obj.Search(obj); return(index==WRONG_VALUE ? false : this.m_list_all_graph_obj.Delete(index)); } //+------------------------------------------------------------------+
The method receives the pointer to the object to be removed from the list.
Set the sorted list flag to the list (the search is performed in sorted lists only), get the object index using the Search() method of the Standard Library.
If the object index is not found, return false, otherwise return the result of deleting the object from the list using the Delete() method of the Standard Library.
The method returning the pointer to the graphical object by chart name and ID:
//+------------------------------------------------------------------+ //| Return a graphical object by chart name and ID | //+------------------------------------------------------------------+ CGStdGraphObj *CGraphElementsCollection::GetStdGraphObject(const string name,const long chart_id) { CArrayObj *list=this.GetList(GRAPH_OBJ_PROP_CHART_ID,chart_id); list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_NAME,name,EQUAL); return(list!=NULL && list.Total()>0 ? list.At(0) : NULL); } //+------------------------------------------------------------------+
Get the list of objects whose chart IDs are equal to the one passed to the method. Get the list containing the object, whose name matches the necessary one, from the obtained list (there should be only one such object). If failed to obtain the list and it is not empty, return the pointer to the first (and only) object in the list. Otherwise, return NULL.
Event handler.
In the current article, I will implement the test version of the event handler which will handle graphical object events on the current chart only. The handler will handle the events of changing or relocating graphical objects. This method is sufficient to define the events named above. Besides, we additionally fix the issue of incomplete filling in object properties that are not built with one mouse click. I have already mentioned earlier that the first click on a chart creates an object creation event and the library creates the appropriate event right away.
At the same time, not all properties are set correctly in it, since the object is built in multiple mouse clicks. Upon completing the object construction, the movement event is created. The response to it will re-write object properties (since we want to track the event in real time, but when creating an object with several anchor points, we also get a movement event leading to the updating of object properties, so that they are re-written with the correct data).
The entire logic is described in the method code comments. Let's consider the method:
//+------------------------------------------------------------------+ //| Event handler | //+------------------------------------------------------------------+ void CGraphElementsCollection::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { CGStdGraphObj *obj=NULL; if(id==CHARTEVENT_OBJECT_CHANGE || id==CHARTEVENT_OBJECT_DRAG) { //--- If the object, whose properties were changed or which was relocated, //--- is successfully received from the collection list by its name set in sparam obj=this.GetStdGraphObject(sparam,::ChartID()); if(obj!=NULL) { //--- Update the properties of the obtained object //--- and check their change obj.PropertiesRefresh(); obj.PropertiesCheckChanged(); } //--- If failed to get the object by its name, it is not on the list, //--- which means its name has been changed else { //--- Let's search the list for the object that is not on the chart obj=this.FindMissingObj(::ChartID()); 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(::ChartID()); //--- Set a new name for the collection list object, which does not correspond to any graphical object on the chart, //--- update the chart properties and check their change obj.SetName(name_new); obj.PropertiesRefresh(); obj.PropertiesCheckChanged(); } } } //+------------------------------------------------------------------+
Besides, we need to get access to the graphical object collection from library-based programs.
To do this, in the main library object within \MQL5\Include\DoEasy\Engine.mqh, create the method returning the pointer to the collection class of library graphical objects:
//--- 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 graphical object collection CGraphElementsCollection *GetGraphicObjCollection(void) { return &this.m_graph_objects; } //--- Constructor/destructor CEngine(); ~CEngine(); private:
Test
To perform the test, let's use the EA from the previous article and save it to \MQL5\Experts\TestDoEasy\Part86\ as TestDoEasyPart86.mq5.
From the OnInit() handler, remove the strings setting permissions to track mouse events:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Set the permissions to send cursor movement and mouse scroll events ChartSetInteger(ChartID(),CHART_EVENT_MOUSE_MOVE,true); ChartSetInteger(ChartID(),CHART_EVENT_MOUSE_WHEEL,true); //--- Set EA global variables ArrayResize(array_clr,2); // Array of gradient filling colors array_clr[0]=C'246,244,244'; // Original ≈pale gray array_clr[1]=C'249,251,250'; // Final ≈pale gray-green //--- Create the array with the current symbol and set it to be used in the library string array[1]={Symbol()}; engine.SetUsedSymbols(array); //--- Create the timeseries object for the current symbol and period, and show its description in the journal engine.SeriesCreate(Symbol(),Period()); engine.GetTimeSeriesCollection().PrintShort(false); // Short descriptions //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Now permissions to track mouse and graphical object events are set in the chart management class. The permissions are set for all open charts.
At the very end of the OnChartEvent() handler, add the call of the handler of the graphical object collection class events:
//+------------------------------------------------------------------+ //| 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(); } //--- Redraw the chart ChartRedraw(); } } engine.GetGraphicObjCollection().OnChartEvent(id,lparam,dparam,sparam); } //+------------------------------------------------------------------+
These are all the improvements. Compile the EA and launch it on the chart. When creating/deleting an object or changing its properties, the appropriate event entries are displayed in the client terminal journal:
So far, these are simply journal entries but that will change later.
What's next?
In the next article, I will create the object event handlers for each open chart and implement sending these events to the control program chart so that the program is able to fully handle them.
Leave your questions, comments and suggestions in the comments.
*Previous articles within the series:
Graphics in DoEasy library (Part 83): Class of the abstract standard graphical object
Graphics in DoEasy library (Part 84): Descendant classes of the abstract standard graphical object
Graphics in DoEasy library (Part 85): Graphical object collection - adding newly created objects
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/10018
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use