Graphics in DoEasy library (Part 95): Composite graphical object controls
Contents
- Concept
- Improving library classes
- Extended standard graphical object toolkit class
- Test
- What's next?
Concept
In this article, I will continue the development of composite graphical objects. These are standard graphical objects consisting of several ones and combined into a single graphical object. In the library, graphical objects included into a composite one are defined as extended standard graphical objects. Such objects have some additional properties and functionality allowing them to be incorporated and incorporate other graphical objects.
The concept of a composite graphical object requires the functionality for keeping the object at the point it is attached to another object and adjusting its location when changing or relocating a parent object.
In the previous article, I started creating the handlers of composite graphical object events, implemented the handling of a composite graphical object removal and started developing its relocation handler.
Today, I will slightly digress from relocating a composite graphical object and implement the handler of change events on a chart featuring a composite graphical object. Besides, I will focus on the controls for managing a composite graphical object.
Why? I am going to implement real-time creation of composite graphical objects — attaching a subordinate object to a base one by dragging the subordinate object to the base one. The base graphical object tracks if another object is being dragged to it with a mouse. The object attachment mechanism is enabled at a certain distance from one of its chart anchor points . The lines connecting the anchor point of the attached object with the anchor point of the base one are displayed visually indicating that the dragged object is ready to be attached to the base one. To achieve this, each anchor point of the graphical object should feature a form object of a certain size. Entering the form object area is to activate the attachment mechanism, while the lines, indicating that the objects are ready for interaction, are displayed on the form itself. Such forms are invisibly present on each pivot point of the graphical object. The area size can be seen only for debugging purposes by enabling the drawing of a rectangle along the form edges:
Besides, the form displays the graphical object anchor points that will appear only when the mouse cursor hovers over the form active area. Thus, we will be able to move and modify the extended graphical object by hovering the mouse cursor over the form area, rather than by highlighting it by a mouse click. As soon as we hover the cursor over the active area of the form (marked by rectangles on the image above), the labels appear in the graphical object anchor point (the blue dot at the center of the circle). If we start dragging the form by mouse, the appropriate pivot point of the graphical object will follow the cursor modifying the object itself together with the composite graphical object.
If the mouse cursor enters the form active area with the pressed mouse button, this means (if verified) that we apply another graphical object to the form activating the mechanism of bounding one object to another. Thus, the forms allow us to accomplish several objectives at once.
I am not going to implement attaching one object to another here since the preparations are not finished yet. Instead, I will create the forms, attach them to the graphical object anchor points and implement the mechanism of moving them along the object pivot point coordinates when changing the chart — relocating it or changing its display scale. This should be done since the form object has the coordinates in screen pixels, while most graphical objects are shown in time/price values.
Improving library classes
In \MQL5\Include\DoEasy\Data.mqh, add new message indices:
//--- CLinkedPivotPoint MSG_GRAPH_OBJ_EXT_NOT_ANY_PIVOTS_X, // Not a single pivot point is set for the object along the X axis MSG_GRAPH_OBJ_EXT_NOT_ANY_PIVOTS_Y, // Not a single pivot point is set for the object along the Y axis MSG_GRAPH_OBJ_EXT_NOT_ATACHED_TO_BASE, // The object is not attached to the basic graphical object MSG_GRAPH_OBJ_EXT_FAILED_CREATE_PP_DATA_OBJ, // Failed to create a data object for the X and Y pivot points MSG_GRAPH_OBJ_EXT_NUM_BASE_PP_TO_SET_X, // Number of base object pivot points for calculating the X coordinate: MSG_GRAPH_OBJ_EXT_NUM_BASE_PP_TO_SET_Y, // Number of base object pivot points for calculating the Y coordinate: //--- CGStdGraphObjExtToolkit MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA, // Failed to change the size of the pivot point time data array MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA, // Failed to change the size of the pivot point price data array MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM, // Failed to create a form object to manage a pivot point }; //+------------------------------------------------------------------+
and the message texts corresponding to the newly added indices:
//--- CLinkedPivotPoint {"Для объекта не установлено ни одной опорной точки по оси X","The object does not have any pivot points set along the x-axis"}, {"Для объекта не установлено ни одной опорной точки по оси Y","The object does not have any pivot points set along the y-axis"}, {"Объект не привязан к базовому графическому объекту","The object is not attached to the base graphical object"}, {"Не удалось создать объект данных опорной точки X и Y.","Failed to create X and Y reference point data object"}, {"Количество опорных точек базового объекта для расчёта координаты X: ","Number of reference points of the base object to set the X coordinate: "}, {"Количество опорных точек базового объекта для расчёта координаты Y: ","Number of reference points of the base object to set the Y coordinate: "}, //--- CGStdGraphObjExtToolkit {"Не удалось изменить размер массива данных времени опорной точки","Failed to resize pivot point time data array"}, {"Не удалось изменить размер массива данных цены опорной точки","Failed to resize pivot point price data array"}, {"Не удалось создать объект-форму для контроля опорной точки","Failed to create form object to control pivot point"}, }; //+---------------------------------------------------------------------+
Replace the macro substitution in \MQL5\Include\DoEasy\Defines.mqh
#define CLR_DEFAULT (0xFF000000) // Default symbol background color in the navigator
with the more comprehensible
#define CLR_MW_DEFAULT (0xFF000000) // Default symbol background color in the Market Watch
and the macro substitution
#define NULL_COLOR (0x00FFFFFF) // Zero for the canvas with the alpha channel
with the more comprehensible
#define CLR_CANV_NULL (0x00FFFFFF) // Zero for the canvas with the alpha channel
and add the new macro substitutions for setting the default values for the form objects to be created here:
//--- Graphical object parameters #define PROGRAM_OBJ_MAX_ID (10000) // Maximum value of an ID of a graphical object belonging to a program #define CTRL_POINT_SIZE (5) // Radius of the control point on the form for managing graphical object pivot points #define CTRL_FORM_SIZE (40) // Size of the control point form for managing graphical object pivot points //+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+
Replace the old macro substitution names in all files.
Simply press Ctrl+Shift+H, enter the following values and check the boxes shown below:
Click "Replace in Files". The editor makes replacements in all library files.
Replace NULL_COLOR with CLR_CANV_NULL the same way.
The new macro substitution names are more illustrative and indicative of their function reducing the amount of potential errors (for instance, I entered CLR_DEFAULT for setting a transparent canvas background and spent a lot of time trying to understand why it did not work).
In addition, I also performed some small changes in all class files of the base graphical object descendants (by simply adding a comma to the text of the method displaying a short object description in the journal):
//+------------------------------------------------------------------+ //| Display a short description of the object in the journal | //+------------------------------------------------------------------+ void CGStdArrowBuyObj::PrintShort(const bool dash=false,const bool symbol=false) { ::Print ( (dash ? " - " : "")+this.Header(symbol)," \"",CGBaseObj::Name(),"\": ID ",(string)this.GetProperty(GRAPH_OBJ_PROP_ID,0), ", ",::TimeToString(CGBaseObj::TimeCreate(),TIME_DATE|TIME_MINUTES|TIME_SECONDS) ); } //+------------------------------------------------------------------+
This is a purely "cosmetic" improvement.
It has been implemented in all files in \MQL5\Include\DoEasy\Objects\Graph\Standard\.
Extended standard graphical object toolkit class
Let's start creating a toolkit for handling extended graphical objects. This will be a class featuring all the necessary methods for creating form objects and working with them. Each extended graphical object is to have a pointer to the object of the appropriate class. If necessary (if this is a base object within a composite graphical object), the class object is created dynamically when creating an extended object and deleted when the latter is deleted.
The object receives the necessary parameters of the base graphical object (its coordinates, type, name, etc.). The base object coordinates are tracked and the form object coordinates are adjusted inside the object. Eventually, the form objects will enable managing the base object.
But first things first...
In \MQL5\Include\DoEasy\Objects\Graph\, create the new Extend\ folder containing the CGStdGraphObjExtToolkit.mqh file of the CGStdGraphObjExtToolkit class inherited from the base CObject class for constructing the MQL5 standard library:
//+------------------------------------------------------------------+ //| CGStdGraphObjExtToolkit.mqh | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" #property strict // Necessary for mql4 //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "..\..\Graph\Form.mqh" //+------------------------------------------------------------------+ //| Extended standard graphical | //| object toolkit class | //+------------------------------------------------------------------+ class CGStdGraphObjExtToolkit : public CObject { }
The form object class file should be included in the class file.
In the private section of the class, declare the variables storing all the necessary base object properties, the properties for constructing forms, the list for storing them, and the methods for creating a form object and returning its screen coordinates:
//+------------------------------------------------------------------+ //| Extended standard graphical | //| object toolkit class | //+------------------------------------------------------------------+ class CGStdGraphObjExtToolkit : public CObject { private: long m_base_chart_id; // Base graphical object chart ID int m_base_subwindow; // Base graphical object chart subwindow ENUM_OBJECT m_base_type; // Base object type string m_base_name; // Base object name int m_base_pivots; // Number of base object reference points datetime m_base_time[]; // Time array of base object reference points double m_base_price[]; // Price array of base object reference points int m_base_x; // Base object X coordinate int m_base_y; // Base object Y coordinate int m_ctrl_form_size; // Size of forms for managing reference points int m_shift; // Shift coordinates for adjusting the form location CArrayObj m_list_forms; // List of form objects for managing reference points //--- Create a form object on a base object reference point CForm *CreateNewControlPointForm(const int index); //--- Return X and Y screen coordinates of the specified reference point of the graphical object bool GetControlPointCoordXY(const int index,int &x,int &y); public:
Let's use the arrays to store price and time coordinates since a single graphical object may have multiple pivot points. So, the coordinates of each point will be stored in the appropriate array cells. The first point coordinate — by the array 0 index, the second point coordinate — by the array 1 index, the third one — by index 2, etc.
Shifting the form coordinates allows us to locate the form precisely at the center of the object pivot point. This shift covers the half of the point size. If the form size is multiple of two, for example 10, it is adjusted by adding 1, i.e. 11. This enables placing the form precisely at the center of the graphical object pivot point so that no side exceeds another one even by a single pixel.
The list of forms is to store all created forms. The access to them is granted by the pointer. The method for calculating the screen coordinates allows us to know the coordinates of the screen where the form is to be placed. This enables its accurate placement on the graphical object pivot point.
In the public section of the class, declare all the necessary methods for handling the class:
public: //--- Set the parameters of the base object of a composite graphical object void SetBaseObj(const ENUM_OBJECT base_type,const string base_name, const long base_chart_id,const int base_subwindow, const int base_pivots,const int ctrl_form_size, const int base_x,const int base_y, const datetime &base_time[],const double &base_price[]); //--- Set the base object (1) time, (2) price, (3) time and price coordinates void SetBaseObjTime(const datetime time,const int index); void SetBaseObjPrice(const double price,const int index); void SetBaseObjTimePrice(const datetime time,const double price,const int index); //--- Set the base object (1) X, (2) Y, (3) X and Y screen coordinates void SetBaseObjCoordX(const int value) { this.m_base_x=value; } void SetBaseObjCoordY(const int value) { this.m_base_y=value; } void SetBaseObjCoordXY(const int value_x,const int value_y) { this.m_base_x=value_x; this.m_base_y=value_y; } //--- (1) Set and (2) return the size of the form of pivot point management control points void SetControlFormSize(const int size); int GetControlFormSize(void) const { return this.m_ctrl_form_size; } //--- Return the pointer to the pivot point form by (1) index and (2) name CForm *GetControlPointForm(const int index) { return this.m_list_forms.At(index); } CForm *GetControlPointForm(const string name,int &index); //--- Return the number of the base object pivot points int GetNumPivotsBaseObj(void) const { return this.m_base_pivots; } //--- Create form objects on the base object pivot points bool CreateAllControlPointForm(void); //--- Remove all form objects from the list void DeleteAllControlPointForm(void); //--- Event handler void OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam); //--- Constructor/destructor CGStdGraphObjExtToolkit(const ENUM_OBJECT base_type,const string base_name, const long base_chart_id,const int base_subwindow, const int base_pivots,const int ctrl_form_size, const int base_x,const int base_y, const datetime &base_time[],const double &base_price[]) { this.m_list_forms.Clear(); this.SetBaseObj(base_type,base_name,base_chart_id,base_subwindow,base_pivots,ctrl_form_size,base_x,base_y,base_time,base_price); this.CreateAllControlPointForm(); } CGStdGraphObjExtToolkit(){;} ~CGStdGraphObjExtToolkit(){;} }; //+------------------------------------------------------------------+
In the class constructor, clear the list of form objects, set all the necessary base object values (passed in the constructor parameters) to the class variables and create the form objects for managing the base object at each pivot point of the base graphical object.
The method setting the parameters of the base object of a composite graphical object:
//+------------------------------------------------------------------+ //| Set the base object parameters of the | //| composite graphical object | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::SetBaseObj(const ENUM_OBJECT base_type,const string base_name, const long base_chart_id,const int base_subwindow, const int base_pivots,const int ctrl_form_size, const int base_x,const int base_y, const datetime &base_time[],const double &base_price[]) { this.m_base_chart_id=base_chart_id; // Base graphical object chart ID this.m_base_subwindow=base_subwindow; // Base graphical object chart subwindow this.m_base_type=base_type; // Base object type this.m_base_name=base_name; // Base object name this.m_base_pivots=base_pivots; // Number of base object reference points this.m_base_x=base_x; // Base object X coordinate this.m_base_y=base_y; // Base object Y coordinate this.SetControlFormSize(ctrl_form_size); // Size of forms for managing reference points if(this.m_base_type==OBJ_LABEL || this.m_base_type==OBJ_BUTTON || this.m_base_type==OBJ_BITMAP_LABEL || this.m_base_type==OBJ_EDIT || this.m_base_type==OBJ_RECTANGLE_LABEL || this.m_base_type==OBJ_CHART) return; if(::ArraySize(base_time)==0) { CMessage::ToLog(DFUN+"base_time: ",MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY); return; } if(::ArraySize(base_price)==0) { CMessage::ToLog(DFUN+"base_price: ",MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY); return; } if(::ArrayResize(this.m_base_time,this.m_base_pivots)!=this.m_base_pivots) { CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA); return; } if(::ArrayResize(this.m_base_price,this.m_base_pivots)!=this.m_base_pivots) { CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA); return; } for(int i=0;i<this.m_base_pivots;i++) { this.m_base_time[i]=base_time[i]; // Time (i) of the base object pivot point this.m_base_price[i]=base_price[i]; // Price (i) of the base object pivot point } } //+------------------------------------------------------------------+
The method receives all the necessary values of the base graphical object properties. Next, check the base object type. If this is an object not based on the price/time coordinates, exit the method. We do not handle such objects yet. Next, check the size of the arrays of the base object coordinates passed to the method. If they are of zero size (the method has received an empty array), inform of that and exit the method. Next, change the size of the coordinate internal arrays according to the passed ones. If failed to change the array, inform of that and leave. In the end, simply copy the input arrays to the internal ones element by element.
The method setting the size of reference points for managing pivot points:
//+------------------------------------------------------------------+ //|Set the size of reference points for managing pivot points | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::SetControlFormSize(const int size) { this.m_ctrl_form_size=(size>254 ? 255 : size<5 ? 5 : size%2==0 ? size+1 : size); this.m_shift=(int)ceil(m_ctrl_form_size/2)+1; } //+------------------------------------------------------------------+
The method receives the required form size. If the size exceeds 254, set it to 255 (odd value), if the passed size is less than 5, set it to 5 (this will be the minimal form size value). Otherwise, if the passed size is equal to two, add one to it and use it. If none of the checked values is true, use the size passed to the method.
Next, calculate the coordinate shift since the form should be set so that the pivot point of the graphical object is located precisely at its center. To achieve this, we should substract the shift value from the coordinate value. Divide the calculate form size into two, take the closest higher integer value and add one.
The method setting the time coordinate of the base object:
//+------------------------------------------------------------------+ //| Set the time coordinate of the base object | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::SetBaseObjTime(const datetime time,const int index) { if(index>this.m_base_pivots-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY); return; } this.m_base_time[index]=time; } //+------------------------------------------------------------------+
The method receives the pivot point time and the object pivot point index. If the index exceeds the number of pivot points in the object, inform of the request out of the array range and leave. As a result, the time value, passed to the method, is set in the cell corresponding to the index in the time array.
The method is necessary for specifying the base object time in the class object when the object is changed.
The method setting the price coordinate of the base object:
//+------------------------------------------------------------------+ //| Set the coordinate of the base object price | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::SetBaseObjPrice(const double price,const int index) { if(index>this.m_base_pivots-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY); return; } this.m_base_price[index]=price; } //+------------------------------------------------------------------+
The method is identical to the one considered above except that here we add the price of the base object pivot point, specified by the index, to the class price array.
The method setting the time and price coordinate of the base object:
//+------------------------------------------------------------------+ //| Set the time and price coordinates of the base object | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::SetBaseObjTimePrice(const datetime time,const double price,const int index) { if(index>this.m_base_pivots-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY); return; } this.m_base_time[index]=time; this.m_base_price[index]=price; } //+------------------------------------------------------------------+
The method is identical to the two methods considered above except that both the price and time are set in the class arrays.
The method returning the X and Y coordinates of the specified pivot point of the graphical object in screen coordinates:
//+------------------------------------------------------------------+ //| Return the X and Y coordinates of the specified pivot point | //| of the graphical object in screen coordinates | //+------------------------------------------------------------------+ bool CGStdGraphObjExtToolkit::GetControlPointCoordXY(const int index,int &x,int &y) { switch(this.m_base_type) { case OBJ_LABEL : case OBJ_BUTTON : case OBJ_BITMAP_LABEL : case OBJ_EDIT : case OBJ_RECTANGLE_LABEL : case OBJ_CHART : x=this.m_base_x; y=this.m_base_y; break; default: if(!::ChartTimePriceToXY(this.m_base_chart_id,this.m_base_subwindow,this.m_base_time[index],this.m_base_price[index],x,y)) { x=0; y=0; return false; } } return true; } //+------------------------------------------------------------------+
The method receives the index of the required pivot point of the base graphical object. This is the pivot point the screen coordinates (in pixels from the upper left screen corner) and two variables (via the link), which are to receive the form screen coordinates, should be received for. If the object is already located within the screen coordinates, these coordinates are returned.
If the object is located within the price/time coordinates, calculate them using the ChartTimePriceToXY() function. If failed to convert the coordinates to the screen ones, set the coordinates to zero and return false.
As a result, return true.
The method returning the pointer to the pivot point form by name:
//+------------------------------------------------------------------+ //| Return the pointer to the pivot point form by name | //+------------------------------------------------------------------+ CForm *CGStdGraphObjExtToolkit::GetControlPointForm(const string name,int &index) { index=WRONG_VALUE; for(int i=0;i<this.m_list_forms.Total();i++) { CForm *form=this.m_list_forms.At(i); if(form==NULL) continue; if(form.Name()==name) { index=i; return form; } } return NULL; } //+------------------------------------------------------------------+
The method receives the name of the desired form and the variable via the link featuring the index of the detected form in the list of form objects.
In the loop by the list of form objects, get the next object. If its name matches the desired one, write the loop index to the variable and return the pointer to the detected object. Upon the loop completion, return NULL — the form is not found, the index is equal to -1. This value has been set before the loop start.
The method creating a form object on a base object reference point:
//+------------------------------------------------------------------+ //| Create a form object on a base object reference point | //+------------------------------------------------------------------+ CForm *CGStdGraphObjExtToolkit::CreateNewControlPointForm(const int index) { string name=this.m_base_name+"_TKPP_"+(index<this.m_base_pivots ? (string)index : "X"); CForm *form=this.GetControlPointForm(index); if(form!=NULL) return NULL; int x=0, y=0; if(!this.GetControlPointCoordXY(index,x,y)) return NULL; return new CForm(this.m_base_chart_id,this.m_base_subwindow,name,x-this.m_shift,y-this.m_shift,this.GetControlFormSize(),this.GetControlFormSize()); } //+------------------------------------------------------------------+
The method receives the index of the required pivot point the form object should be created at.
Create the form object name consisting of the base object name + "ToolKit Pivot Point" (_TKPP) abbreviation + pivot point index. When creating the index description, check its value. If it is less than the number of base object pivot points (calculation starts from zero), use the string representation of the index passed to the method. Otherwise, use the X icon. Why do we need this? Later, it will be possible to attach subordinate objects to the base one not only at its pivot points but also between them. Besides, we need to create a control form at the base object line center, beyond which the entire object is relocated, to relocate it. Therefore, the form name should feature the ability to create the form not only for pivot points but for other points as well.
Next, check the presence of the form in the list by index passed to the method. If the form object is already present by the index in the list (the pointer to it is not equal to NULL), return NULL.
Next, transform the pivot point coordinates by its index to the screen coordinates and return the result of creating a form object on received coordinates. The shift value is subtracted from both coordinates for precise positioning of the form center on the pivot point.
I could simply set the value for the form anchor point but our library convention states that the anchor point of all forms remains unchanged — in its upper left corner. Therefore, the shift for the form object positioning is used wherever necessary.
The method creating form objects on the base object pivot points:
//+------------------------------------------------------------------+ //| Create form objects on the base object pivot points | //+------------------------------------------------------------------+ bool CGStdGraphObjExtToolkit::CreateAllControlPointForm(void) { bool res=true; //--- In the loop by the number of base object pivot points for(int i=0;i<this.m_base_pivots;i++) { //--- Create a new form object on the current pivot point corresponding to the loop index CForm *form=this.CreateNewControlPointForm(i); //--- If failed to create the form, inform of that and add 'false' to the final result if(form==NULL) { CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM); res &=false; } //--- If failed to add the form to the list, inform of that, remove the created form and add 'false' to the final result if(!this.m_list_forms.Add(form)) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST); delete form; res &=false; } //--- Set all the necessary properties for the created form object form.SetBelong(GRAPH_OBJ_BELONG_PROGRAM); // Object is created programmatically form.SetActive(true); // Form object is active form.SetMovable(true); // Movable object form.SetActiveAreaShift(0,0,0,0); // Object active area - the entire form form.SetFlagSelected(false,false); // Object is not selected form.SetFlagSelectable(false,false); // Object cannot be selected by mouse form.Erase(CLR_CANV_NULL,0); // Fill in the form with transparent color and set the full transparency //form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,clrSilver); // Draw an outlining rectangle for visual display of the form location form.DrawCircle((int)floor(form.Width()/2),(int)floor(form.Height()/2),CTRL_POINT_SIZE,clrDodgerBlue); // Draw a circle in the form center form.DrawCircleFill((int)floor(form.Width()/2),(int)floor(form.Height()/2),2,clrDodgerBlue); // Draw a point in the form center form.Done(); // Save the initial form object state (its appearance) } //--- Redraw the chart for displaying changes (if successful) and return the final result if(res) ::ChartRedraw(this.m_base_chart_id); return res; } //+------------------------------------------------------------------+
The entire logic here is described in the code comments. In brief, in the loop by the number of base object pivot points, create a new form object for each pivot point, add it to the list of form objects and set the coordinates of the appropriate base object pivot point to each form. Each form features the circle in the center and the point specifying that this is the object of managing the base object pivot point.
Such objects are to be initially invisible and to appear only when the mouse cursor hovers over the form area. For now, I will make them visible to test their behavior during chart changes. In subsequent articles, I will stick to my plan — the objects will be initially invisible and appear only when the mouse hovers over their active area (i.e. the entire form object size).
The method removing all form objects from the list:
//+------------------------------------------------------------------+ //| Remove all form objects from the list | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::DeleteAllControlPointForm(void) { this.m_list_forms.Clear(); } //+------------------------------------------------------------------+
Simply use the Clear() method fully clearing the entire list.
In the event handler, process form object events according to an occurred event:
//+------------------------------------------------------------------+ //| Event handler | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam) { if(id==CHARTEVENT_CHART_CHANGE) { for(int i=0;i<this.m_list_forms.Total();i++) { CForm *form=this.m_list_forms.At(i); if(form==NULL) continue; int x=0, y=0; if(!this.GetControlPointCoordXY(i,x,y)) continue; form.SetCoordX(x-this.m_shift); form.SetCoordY(y-this.m_shift); form.Update(); } ::ChartRedraw(this.m_base_chart_id); } } //+------------------------------------------------------------------+
Currently, we handle only the chart change event. In the loop by all form objects, get the next form from the list. If failed to receive its screen coordinates according to the pivot point it is drawn on, move on to the next form. Set the new screen coordinates to the form and update the form. Upon the loop completion, redraw the chart for displaying changes.
Since the toolkit object of the expanded standard graphical object is stored in the object of the standard graphical object class, we need to improve the class in \MQL5\Include\DoEasy\Objects\Graph\Standard\GStdGraphObj.mqh.
First, include the files of the form object and newly created toolkit object of the extended standard graphical object to the file:
//+------------------------------------------------------------------+ //| GStdGraphObj.mqh | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" #property strict // Necessary for mql4 //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "..\GBaseObj.mqh" #include "..\..\..\Services\Properties.mqh" #include "..\..\Graph\Form.mqh" #include "..\..\Graph\Extend\CGStdGraphObjExtToolkit.mqh" //+------------------------------------------------------------------+ //| Class of the dependent object pivot point data | //+------------------------------------------------------------------+
In the private section of the class of the abstract standard graphical object, declare the pointer to the toolkit object of the extended standard graphical object:
//+------------------------------------------------------------------+ //| The class of the abstract standard graphical object | //+------------------------------------------------------------------+ class CGStdGraphObj : public CGBaseObj { private: CArrayObj m_list; // List of subordinate graphical objects CProperties *Prop; // Pointer to the property object CLinkedPivotPoint m_linked_pivots; // Linked pivot points CGStdGraphObjExtToolkit *ExtToolkit; // Pointer to the extended graphical object toolkit int m_pivots; // Number of object reference points //--- Read and set (1) the time and (2) the price of the specified object pivot point void SetTimePivot(const int index); void SetPricePivot(const int index); //--- Read and set (1) color, (2) style, (3) width, (4) value, (5) text of the specified object level void SetLevelColor(const int index); void SetLevelStyle(const int index); void SetLevelWidth(const int index); void SetLevelValue(const int index); void SetLevelText(const int index); //--- Read and set the BMP file name for the "Bitmap Level" object. Index: 0 - ON, 1 - OFF void SetBMPFile(const int index); public:
In the public section, write the method returning the pointer to the toolkit object:
public: //--- Set object's (1) integer, (2) real and (3) string properties void SetProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index,long value) { this.Prop.Curr.SetLong(property,index,value); } void SetProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index,double value) { this.Prop.Curr.SetDouble(property,index,value); } void SetProperty(ENUM_GRAPH_OBJ_PROP_STRING property,int index,string value) { this.Prop.Curr.SetString(property,index,value); } //--- Return object’s (1) integer, (2) real and (3) string property from the properties array long GetProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index) const { return this.Prop.Curr.GetLong(property,index); } double GetProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index) const { return this.Prop.Curr.GetDouble(property,index); } string GetProperty(ENUM_GRAPH_OBJ_PROP_STRING property,int index) const { return this.Prop.Curr.GetString(property,index); } //--- Set object's previous (1) integer, (2) real and (3) string properties void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index,long value) { this.Prop.Prev.SetLong(property,index,value); } void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index,double value){ this.Prop.Prev.SetDouble(property,index,value); } void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING property,int index,string value){ this.Prop.Prev.SetString(property,index,value); } //--- Return object’s (1) integer, (2) real and (3) string property from the previous properties array long GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index) const { return this.Prop.Prev.GetLong(property,index); } double GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index) const { return this.Prop.Prev.GetDouble(property,index); } string GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING property,int index) const { return this.Prop.Prev.GetString(property,index); } //--- Return (1) itself, (2) properties and (3) the change history CGStdGraphObj *GetObject(void) { return &this; } CProperties *Properties(void) { return this.Prop; } CChangeHistory *History(void) { return this.Prop.History;} CGStdGraphObjExtToolkit *GetExtToolkit(void) { return this.ExtToolkit; } //--- Return the flag of the object supporting this property virtual bool SupportProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property) { return true; } virtual bool SupportProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property) { return true; } virtual bool SupportProperty(ENUM_GRAPH_OBJ_PROP_STRING property) { return true; }
In the public section of the class, declare the handler of graphical object events. In the constructor, set the default value of NULL for the pointer to the toolkit object, while in the class destructor, check the pointer validity and first, remove all forms from the toolkit object and then delete the object itself:
private: //--- Set the X coordinate (1) from the specified property of the base object to the specified subordinate object, (2) from the base object void SetCoordXToDependentObj(CGStdGraphObj *obj,const int prop_from,const int modifier_from,const int modifier_to); void SetCoordXFromBaseObj(const int prop_from,const int modifier_from,const int modifier_to); //--- Set the Y coordinate (1) from the specified property of the base object to the specified subordinate object, (2) from the base object void SetCoordYToDependentObj(CGStdGraphObj *obj,const int prop_from,const int modifier_from,const int modifier_to); void SetCoordYFromBaseObj(const int prop_from,const int modifier_from,const int modifier_to); //--- Set the (1) integer, (2) real and (3) string property to the specified subordinate property void SetDependentINT(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_INTEGER prop,const long value,const int modifier); void SetDependentDBL(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_DOUBLE prop,const double value,const int modifier); void SetDependentSTR(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_STRING prop,const string value,const int modifier); public: //--- Event handler void OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam); //--- Default constructor CGStdGraphObj(){ this.m_type=OBJECT_DE_TYPE_GSTD_OBJ; this.m_species=WRONG_VALUE; this.ExtToolkit=NULL; } //--- Destructor ~CGStdGraphObj() { if(this.Prop!=NULL) delete this.Prop; if(this.ExtToolkit!=NULL) { this.ExtToolkit.DeleteAllControlPointForm(); delete this.ExtToolkit; } } protected: //--- Protected parametric constructor CGStdGraphObj(const ENUM_OBJECT_DE_TYPE obj_type, const ENUM_GRAPH_ELEMENT_TYPE elm_type, const ENUM_GRAPH_OBJ_BELONG belong, const ENUM_GRAPH_OBJ_SPECIES species, const long chart_id, const int pivots, const string name); public: //+--------------------------------------------------------------------+ //|Methods of simplified access and setting graphical object properties| //+--------------------------------------------------------------------+
In the block of methods for a simplified access and setting graphical object properties, write the method returning the number of graphical object pivot points:
public: //+--------------------------------------------------------------------+ //|Methods of simplified access and setting graphical object properties| //+--------------------------------------------------------------------+ //--- Number of object reference points int Pivots(void) const { return this.m_pivots; } //--- Object index in the list int Number(void) const { return (int)this.GetProperty(GRAPH_OBJ_PROP_NUM,0); } void SetNumber(const int number) { this.SetProperty(GRAPH_OBJ_PROP_NUM,0,number); }
In the protected parametric constructor, check the graphical element type. If this is an extended graphical object, create a new toolkit object and save the pointer to it in the ExtToolkit variable. At the end of the constructor listing, initialize the toolkit object:
//+------------------------------------------------------------------+ //| Protected parametric constructor | //+------------------------------------------------------------------+ CGStdGraphObj::CGStdGraphObj(const ENUM_OBJECT_DE_TYPE obj_type, const ENUM_GRAPH_ELEMENT_TYPE elm_type, const ENUM_GRAPH_OBJ_BELONG belong, const ENUM_GRAPH_OBJ_SPECIES species, const long chart_id,const int pivots, const string name) { //--- Create the property object with the default values this.Prop=new CProperties(GRAPH_OBJ_PROP_INTEGER_TOTAL,GRAPH_OBJ_PROP_DOUBLE_TOTAL,GRAPH_OBJ_PROP_STRING_TOTAL); this.ExtToolkit=(elm_type==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED ? new CGStdGraphObjExtToolkit() : NULL); //--- Set the number of pivot points and object levels this.m_pivots=pivots; int levels=(int)::ObjectGetInteger(chart_id,name,OBJPROP_LEVELS); //--- Set the property array dimensionalities according to the number of pivot points and levels this.Prop.SetSizeRange(GRAPH_OBJ_PROP_TIME,this.m_pivots); this.Prop.SetSizeRange(GRAPH_OBJ_PROP_PRICE,this.m_pivots); this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELCOLOR,levels); this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELSTYLE,levels); this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELWIDTH,levels); this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELVALUE,levels); this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELTEXT,levels); this.Prop.SetSizeRange(GRAPH_OBJ_PROP_BMPFILE,2); //--- Set the object (1) type, type of graphical (2) object, (3) element, (4) subwindow affiliation and (5) index, as well as (6) chart symbol Digits this.m_type=obj_type; this.SetName(name); CGBaseObj::SetChartID(chart_id); CGBaseObj::SetTypeGraphObject(CGBaseObj::GraphObjectType(obj_type)); CGBaseObj::SetTypeElement(elm_type); CGBaseObj::SetBelong(belong); CGBaseObj::SetSpecies(species); CGBaseObj::SetSubwindow(chart_id,name); CGBaseObj::SetDigits((int)::SymbolInfoInteger(::ChartSymbol(chart_id),SYMBOL_DIGITS)); //--- Save the integer properties inherent in all graphical objects but not present in the current one this.SetProperty(GRAPH_OBJ_PROP_CHART_ID,0,CGBaseObj::ChartID()); // Chart ID this.SetProperty(GRAPH_OBJ_PROP_WND_NUM,0,CGBaseObj::SubWindow()); // Chart subwindow index this.SetProperty(GRAPH_OBJ_PROP_TYPE,0,CGBaseObj::TypeGraphObject()); // Graphical object type (ENUM_OBJECT) this.SetProperty(GRAPH_OBJ_PROP_ELEMENT_TYPE,0,CGBaseObj::TypeGraphElement()); // Graphical element type (ENUM_GRAPH_ELEMENT_TYPE) this.SetProperty(GRAPH_OBJ_PROP_BELONG,0,CGBaseObj::Belong()); // Graphical object affiliation this.SetProperty(GRAPH_OBJ_PROP_SPECIES,0,CGBaseObj::Species()); // Graphical object species this.SetProperty(GRAPH_OBJ_PROP_GROUP,0,0); // Graphical object group this.SetProperty(GRAPH_OBJ_PROP_ID,0,0); // Object ID this.SetProperty(GRAPH_OBJ_PROP_BASE_ID,0,0); // Base object ID this.SetProperty(GRAPH_OBJ_PROP_NUM,0,0); // Object index in the list this.SetProperty(GRAPH_OBJ_PROP_CHANGE_HISTORY,0,false); // Flag of storing the change history this.SetProperty(GRAPH_OBJ_PROP_BASE_NAME,0,this.Name()); // Base object name //--- Save the properties inherent in all graphical objects and present in a graphical object this.PropertiesRefresh(); //--- Save basic properties in the parent object this.m_create_time=(datetime)this.GetProperty(GRAPH_OBJ_PROP_CREATETIME,0); this.m_back=(bool)this.GetProperty(GRAPH_OBJ_PROP_BACK,0); this.m_selected=(bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTED,0); this.m_selectable=(bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTABLE,0); this.m_hidden=(bool)this.GetProperty(GRAPH_OBJ_PROP_HIDDEN,0); //--- Initialize the extended graphical object toolkit if(this.GraphElementType()==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED) { datetime times[]; double prices[]; if(::ArrayResize(times,this.Pivots())!=this.Pivots()) CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA); if(::ArrayResize(prices,this.Pivots())!=this.Pivots()) CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA); for(int i=0;i<this.Pivots();i++) { times[i]=this.Time(i); prices[i]=this.Price(i); } this.ExtToolkit.SetBaseObj(this.TypeGraphObject(),this.Name(),this.ChartID(),this.SubWindow(),this.Pivots(),CTRL_FORM_SIZE,this.XDistance(),this.YDistance(),times,prices); this.ExtToolkit.CreateAllControlPointForm(); this.SetFlagSelected(false,false); this.SetFlagSelectable(false,false); } //--- Save the current properties to the previous ones this.PropertiesCopyToPrevData(); } //+-------------------------------------------------------------------+
When initializing the toolkit object, first declare the arrays of time and price properties, adjust their size depending on the number of graphical object pivot points and set price and time values from the object pivot points corresponding to the loop index.
Next, call the toolkit object initialization method and pass the necessary graphical object parameters to it together with newly filled price and time property arrays. After the initialization, call the method for creating form objects on the graphical object pivot points. Finally, set the status of the non-selected object for the graphical object and disable the ability to select it with a mouse.
In the method checking the object property changes, namely in the code block handling the extended standard graphical object, add the code block for moving control points (form objects) to the new screen coordinates when changing the location of pivot points of the extended standard graphical object:
//+------------------------------------------------------------------+ //| Check object property changes | //+------------------------------------------------------------------+ void CGStdGraphObj::PropertiesCheckChanged(void) { CGBaseObj::ClearEventsList(); bool changed=false; int begin=0, end=GRAPH_OBJ_PROP_INTEGER_TOTAL; for(int i=begin; i<end; i++) { ENUM_GRAPH_OBJ_PROP_INTEGER prop=(ENUM_GRAPH_OBJ_PROP_INTEGER)i; if(!this.SupportProperty(prop)) continue; for(int j=0;j<Prop.CurrSize(prop);j++) { if(this.GetProperty(prop,j)!=this.GetPropertyPrev(prop,j)) { changed=true; this.CreateAndAddNewEvent(GRAPH_OBJ_EVENT_CHANGE,this.ChartID(),prop,this.Name()); } } } begin=end; end+=GRAPH_OBJ_PROP_DOUBLE_TOTAL; for(int i=begin; i<end; i++) { ENUM_GRAPH_OBJ_PROP_DOUBLE prop=(ENUM_GRAPH_OBJ_PROP_DOUBLE)i; if(!this.SupportProperty(prop)) continue; for(int j=0;j<Prop.CurrSize(prop);j++) { if(this.GetProperty(prop,j)!=this.GetPropertyPrev(prop,j)) { changed=true; this.CreateAndAddNewEvent(GRAPH_OBJ_EVENT_CHANGE,this.ChartID(),prop,this.Name()); } } } begin=end; end+=GRAPH_OBJ_PROP_STRING_TOTAL; for(int i=begin; i<end; i++) { ENUM_GRAPH_OBJ_PROP_STRING prop=(ENUM_GRAPH_OBJ_PROP_STRING)i; if(!this.SupportProperty(prop)) continue; for(int j=0;j<Prop.CurrSize(prop);j++) { if(this.GetProperty(prop,j)!=this.GetPropertyPrev(prop,j) && prop!=GRAPH_OBJ_PROP_NAME) { changed=true; this.CreateAndAddNewEvent(GRAPH_OBJ_EVENT_CHANGE,this.ChartID(),prop,this.Name()); } } } if(changed) { for(int i=0;i<this.m_list_events.Total();i++) { CGBaseEvent *event=this.m_list_events.At(i); if(event==NULL) continue; ::EventChartCustom(::ChartID(),event.ID(),event.Lparam(),event.Dparam(),event.Sparam()); } if(this.AllowChangeHistory()) { int total=HistoryChangesTotal(); if(this.CreateNewChangeHistoryObj(total<1)) ::Print ( DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_SUCCESS_CREATE_SNAPSHOT)," #",(total==0 ? "0-1" : (string)total), ": ",this.HistoryChangedObjTimeChangedToString(total-1) ); } //--- If subordinate objects are attached to the base one (in a composite graphical object) if(this.m_list.Total()>0) { //--- In the loop by the number of added graphical objects, for(int i=0;i<this.m_list.Total();i++) { //--- get the next graphical object, CGStdGraphObj *dep=m_list.At(i); if(dep==NULL) continue; //--- get the data object of its pivot points, CLinkedPivotPoint *pp=dep.GetLinkedPivotPoint(); if(pp==NULL) continue; //--- get the number of coordinate points the object is attached to int num=pp.GetNumLinkedCoords(); //--- In the loop by the object coordinate points, for(int j=0;j<num;j++) { //--- get the number of coordinate points of the base object for setting the X coordinate int numx=pp.GetBasePivotsNumX(j); //--- In the loop by each coordinate point for setting the X coordinate, for(int nx=0;nx<numx;nx++) { //--- get the property for setting the X coordinate, its modifier //--- and set it in the object selected as the current one in the main loop int prop_from=pp.GetPropertyX(j,nx); int modifier_from=pp.GetPropertyModifierX(j,nx); this.SetCoordXToDependentObj(dep,prop_from,modifier_from,nx); } //--- Get the number of coordinate points of the base object for setting the Y coordinate int numy=pp.GetBasePivotsNumY(j); //--- In the loop by each coordinate point for setting the Y coordinate, for(int ny=0;ny<numy;ny++) { //--- get the property for setting the Y coordinate, its modifier //--- and set it in the object selected as the current one in the main loop int prop_from=pp.GetPropertyY(j,ny); int modifier_from=pp.GetPropertyModifierY(j,ny); this.SetCoordYToDependentObj(dep,prop_from,modifier_from,ny); } } dep.PropertiesCopyToPrevData(); } //--- Move reference control points to new coordinates if(ExtToolkit!=NULL) { for(int i=0;i<this.Pivots();i++) { ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i); } ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance()); long lparam=0; double dparam=0; string sparam=""; ExtToolkit.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam); } //--- Upon completion of the loop of handling all bound objects, redraw the chart to display all the changes ::ChartRedraw(m_chart_id); } //--- Save the current properties as the previous ones this.PropertiesCopyToPrevData(); } } //+------------------------------------------------------------------+
If we move one of the graphical object pivot points or the entire object, the screen coordinates of its pivot points change. This means we need to move the toolkit class form objects to the new screen coordinates as well. Therefore, first I pass the new graphical object coordinates to the toolkit object (in the loop by the number of pivot points for the price/time coordinates and separate coordinates in pixels). Then I call the handler of the toolkit object events by passing the ID of the chart change event to it. This will make the handler of the toolkit object events recalculate the screen coordinates of all forms and relocate them to a new place according to the new price and time coordinates of the graphical object.
In the method adding a subordinate standard graphical object to the list, fix a small error — a subordinate graphical object alters its own properties, therefore the new properties should be saved as the previous ones right away to avoid generating new events of changing these graphical objects when clicking on it:
//+------------------------------------------------------------------+ //| Add a subordinate standard graphical object to the list | //+------------------------------------------------------------------+ bool CGStdGraphObj::AddDependentObj(CGStdGraphObj *obj) { //--- If the current object is not an extended one, inform of that and return 'false' if(this.TypeGraphElement()!=GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED) { CMessage::ToLog(MSG_GRAPH_OBJ_NOT_EXT_OBJ); return false; } //--- If failed to add the pointer to the passed object into the list, inform of that and return 'false' if(!this.m_list.Add(obj)) { CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_ADD_DEP_EXT_OBJ_TO_LIST); return false; } //--- Object added to the list - set its number in the list, //--- name and ID of the current object as the base one, //--- set the flags of object availability and selection //--- and the graphical element type - standard extended graphical object obj.SetNumber(this.m_list.Total()-1); obj.SetBaseName(this.Name()); obj.SetBaseObjectID(this.ObjectID()); obj.SetFlagSelected(false,false); obj.SetFlagSelectable(false,false); obj.SetTypeElement(GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED); obj.PropertiesCopyToPrevData(); return true; } //+------------------------------------------------------------------+
The handler of the abstract standard graphical object events:
//+------------------------------------------------------------------+ //| Event handler | //+------------------------------------------------------------------+ void CGStdGraphObj::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { if(GraphElementType()!=GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED) return; if(id==CHARTEVENT_CHART_CHANGE) { if(ExtToolkit==NULL) return; for(int i=0;i<this.Pivots();i++) { ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i); } ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance()); ExtToolkit.OnChartEvent(id,lparam,dparam,sparam); } } //+------------------------------------------------------------------+
For now, the handler processes only the chart change event.
If the object is not extended, leave the handler. If the chart change event has been detected, check the validity of the pointer to the toolkit object of the extended standard graphical object. If no toolkit is created, exit. Next, in the loop by the number of the graphical object pivot points, set the new graphical object price/time coordinates to the toolkit object. Next, set its new screen coordinates and call the handler of the toolkit object events, in which all forms are relocated to the new screen coordinates based on the price/time coordinates newly passed to the toolkit object.
When removing an extended standard graphical object from the chart, we also need to remove the form objects of its toolkit object in case such an object was created for the graphical object. Removing graphical objects from the chart is handled in the collection class of graphical elements in \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh.
In the method handling the removal of extended graphical objects, add the code block for removing all form objects from the toolkit object:
//+------------------------------------------------------------------+ //| Handle the removal of extended graphical objects | //+------------------------------------------------------------------+ void CGraphElementsCollection::DeleteExtendedObj(CGStdGraphObj *obj) { if(obj==NULL) return; //--- Save the ID of the graphical object chart and the number of subordinate objects in its list long chart_id=obj.ChartID(); int total=obj.GetNumDependentObj(); //--- If the list of subordinate objects is not empty (this is the base object) if(total>0) { CGStdGraphObjExtToolkit *toolkit=obj.GetExtToolkit(); if(toolkit!=NULL) { toolkit.DeleteAllControlPointForm(); } //--- In the loop, move along all dependent objects and remove them for(int n=total-1;n>WRONG_VALUE;n--) { //--- Get the next graphical object CGStdGraphObj *dep=obj.GetDependentObj(n); if(dep==NULL) continue; //--- If failed to remove it from the chart, display the appropriate message in the journal if(!::ObjectDelete(dep.ChartID(),dep.Name())) CMessage::ToLog(DFUN+dep.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART); } //--- Upon the loop completion, update the chart to display the changes and exit the method ::ChartRedraw(chart_id); return; } //--- If this is a subordinate object else if(obj.BaseObjectID()>0) { //--- Get the base object name and its ID string base_name=obj.BaseName(); long base_id=obj.BaseObjectID(); //--- Get the base object from the graphical object collection list CGStdGraphObj *base=GetStdGraphObject(base_name,chart_id); if(base==NULL) return; //--- get the number of dependent objects in its list int count=base.GetNumDependentObj(); //--- In the loop, move along all its dependent objects and remove them for(int n=count-1;n>WRONG_VALUE;n--) { //--- Get the next graphical object CGStdGraphObj *dep=base.GetDependentObj(n); //--- If failed to get the pointer or the object has already been removed from the chart, move on to the next one if(dep==NULL || !this.IsPresentGraphObjOnChart(dep.ChartID(),dep.Name())) continue; //--- If failed to delete the graphical object from the chart, //--- display the appropriate message in the journal and move on to the next one if(!::ObjectDelete(dep.ChartID(),dep.Name())) { CMessage::ToLog(DFUN+dep.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART); continue; } } //--- Remove the base object from the chart and from the list if(!::ObjectDelete(base.ChartID(),base.Name())) CMessage::ToLog(DFUN+base.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART); } //--- Update the chart for displaying the changes ::ChartRedraw(chart_id); } //+------------------------------------------------------------------+
Here we simply retrieve the pointer to the toolkit object of the extended graphical object. If the pointer is valid, call the method for removing all created forms of the toolkit object of the extended standard graphical object I have previously considered.
In the handler of the graphical element collection class events, add handling the chart changes for extended standard graphical objects:
//+------------------------------------------------------------------+ //| Event handler | //+------------------------------------------------------------------+ void CGraphElementsCollection::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { CGStdGraphObj *obj=NULL; ushort idx=ushort(id-CHARTEVENT_CUSTOM); if(id==CHARTEVENT_OBJECT_CHANGE || id==CHARTEVENT_OBJECT_DRAG || id==CHARTEVENT_OBJECT_CLICK || idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG || idx==CHARTEVENT_OBJECT_CLICK) { //--- Calculate the chart ID //--- If the event ID corresponds to an event from the current chart, the chart ID is received from ChartID //--- If the event ID corresponds to a user event, the chart ID is received from lparam //--- Otherwise, the chart ID is assigned to -1 long param=(id==CHARTEVENT_OBJECT_CLICK ? ::ChartID() : idx==CHARTEVENT_OBJECT_CLICK ? lparam : WRONG_VALUE); long chart_id=(param==WRONG_VALUE ? (lparam==0 ? ::ChartID() : lparam) : param); //--- Get the object, whose properties were changed or which was relocated, //--- from the collection list by its name set in sparam obj=this.GetStdGraphObject(sparam,chart_id); //--- If failed to get the object by its name, it is not on the list, //--- which means its name has been changed if(obj==NULL) { //--- Let's search the list for the object that is not on the chart obj=this.FindMissingObj(chart_id); //--- If failed to find the object here as well, exit if(obj==NULL) return; //--- Get the name of the renamed graphical object on the chart, which is not in the collection list string name_new=this.FindExtraObj(chart_id); //--- set a new name for the collection list object, which does not correspond to any graphical object on the chart, //--- and send an event with the new name of the object to the control program chart if(obj.SetNamePrev(obj.Name()) && obj.SetName(name_new)) ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_RENAME,obj.ChartID(),obj.TimeCreate(),obj.Name()); } //--- Update the properties of the obtained object //--- and check their change obj.PropertiesRefresh(); obj.PropertiesCheckChanged(); } //--- Handle chart changes for extended standard objects if(id==CHARTEVENT_CHART_CHANGE || idx==CHARTEVENT_CHART_CHANGE) { CArrayObj *list=this.GetListStdGraphObjectExt(); if(list!=NULL) { for(int i=0;i<list.Total();i++) { obj=list.At(i); if(obj==NULL) continue; obj.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam); } } } } //+------------------------------------------------------------------+
Here, if the chart change event has been detected, get the list of all extended standard graphical objects. In the loop by their number, get the next object, call its event handler and pass the "Chart changed" event value to it.
Test
To perform the test, I will use the EA from the previous article and save it in \MQL5\Experts\TestDoEasy\Part95\ as TestDoEasyPart95.mq5.
The only changes are slightly different names of price label objects attached to the base trend line object. Subordinate object names in the block of creating a composite graphical object of the EA event handler now feature the "Ext" affix so that the names correspond to the extended graphical object type:
if(id==CHARTEVENT_CLICK) { if(!IsCtrlKeyPressed()) return; //--- Get the chart click coordinates datetime time=0; double price=0; int sw=0; if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,sw,time,price)) { //--- Get the right point coordinates for a trend line datetime time2=iTime(Symbol(),PERIOD_CURRENT,1); double price2=iOpen(Symbol(),PERIOD_CURRENT,1); //--- Create the "Trend line" object string name_base="TrendLineExt"; engine.CreateLineTrend(name_base,0,true,time,price,time2,price2); //--- Get the object from the list of graphical objects by chart name and ID and pass its properties to the journal CGStdGraphObj *obj=engine.GraphGetStdGraphObjectExt(name_base,ChartID()); //--- Create the "Left price label" object string name_dep="PriceLeftExt"; engine.CreatePriceLabelLeft(name_dep,0,false,time,price); //--- Get the object from the list of graphical objects by chart name and ID and CGStdGraphObj *dep=engine.GraphGetStdGraphObject(name_dep,ChartID()); //--- add it to the list of graphical objects bound to the "Trend line" object obj.AddDependentObj(dep); //--- Set its pivot point by X and Y axis to the trend line left point dep.AddNewLinkedCoord(GRAPH_OBJ_PROP_TIME,0,GRAPH_OBJ_PROP_PRICE,0); //--- Create the "Right price label" object name_dep="PriceRightExt"; engine.CreatePriceLabelRight(name_dep,0,false,time2,price2); //--- Get the object from the list of graphical objects by chart name and ID and dep=engine.GraphGetStdGraphObject(name_dep,ChartID()); //--- add it to the list of graphical objects bound to the "Trend line" object obj.AddDependentObj(dep); //--- Set its pivot point by X and Y axis to the trend line right point dep.AddNewLinkedCoord(GRAPH_OBJ_PROP_TIME,1,GRAPH_OBJ_PROP_PRICE,1); } } engine.GetGraphicObjCollection().OnChartEvent(id,lparam,dparam,sparam);
Let's create a composite graphical object. The form objects are to be set to its pivot points during the creation.
These form objects have the coordinates in pixels from the upper left screen corner. Thus, if we move the chart, these screen coordinates should be recalculated so that the objects are set on the appropriate graphical object pivot points. This is what we are going to check.
Compile the EA and launch it on the chart:
We can see that the objects take their designated places when the chart changes. However, this happens with a lag.
When removing a graphical object, the appropriate form objects are removed as well.
What can we do with the lag? In fact, we do not need to see the movements live — these moves will always remain hidden when moving the chart (they are displayed now to handle an event response). The graphical object line itself is moved when the form objects are dragged by a mouse. Any interaction with the forms is performed on a fixed chart. So this result is quite sufficient, especially considering that the chart is updated only after the loop completion rather than at each loop iteration. In order to reduce the load, we can control the chart change completion displaying the changes and showing the object afterwards (this is only possible if the cursor hovers over the form object active area when it should become visible).
What's next?
In the next article, I will continue my work on composite graphical object events.
*Previous articles within the series:
Graphics in DoEasy library (Part 93): Preparing functionality for creating composite graphical objects
Graphics in DoEasy library (Part 94): Moving and deleting composite graphical objects
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/10387
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use