Graphics in DoEasy library (Part 98): Moving pivot points of extended standard graphical objects
Contents
Concept
In the article 93, I started the development of composite graphical objects in the library. Then I was distracted by the necessity to improve the functionality of form objects based on the CCanvas class. We use the form objects in the composite graphical objects for creating pivot point controls of a graphical object included into an extended standard graphical object. So we have been in need for the new form object functionality.
In the previous article, I completed the improvement of objects based on the CCanvas class. Today, I will continue the development of extended standard graphical objects the composite graphical objects consist of.
The article objective is not a development of new classes. Instead, I will consider the improvement of the already prepared functionality for creating convenient tools for relocating pivot points of standard graphical objects. The article will describe the development of a composite graphical object prototype. We already created it in the previous articles. This is an ordinary trend line with additional price label objects at its ends:
Today I will deal with the issue of moving trend line pivot points together with price labels attached to the relocated side of the trend line. The line pivot points contain form objects for relocation. By grabbing and moving the object, we will move the appropriate trend line side as well. While the cursor is away from the trend line pivot point, the form object remains invisible. However, when the cursor approaches the pivot point at a certain distance (when entering the area of a completely transparent form), points with circles appear on the form:
This is how the trend line pivot point control appears on the chart. The form has the greater size than its active area. The active area of a form is the area that can be dragged to move the form. In contrast, the area of the form itself can be used to arrange other kinds of interaction with it using mouse buttons and mouse wheel.
Thus, if the mouse cursor is within the form but is outside its active area, we can implement, for example, the activation of the context menu of a composite graphical object upon pressing the right mouse button. If the cursor is in the active area, then, in addition to the context menu, we can grab this form with the mouse and move it. In this case, the end of the line the form is attached to moves as well.
Of course, this is just a test composite graphical object used to give the new functionality a try. After all the necessary tools for creating composite graphical objects and for processing form objects are created, I will create a small set of standard library composite graphical objects to be used to develop custom objects. Their creation will serve as an example and description of how to create your own objects of this kind.
For now, I am only developing the functionality of the library and create the necessary "bricks" to make custom objects from. The steps described, analyzed and implemented in my articles will serve as basis to be used 'as is' without the need to implement everything from scratch.
Improving library classes
Open \MQL5\Include\DoEasy\Defines.mqh and implement some changes.
The pivot point control forms are to contain a point and a circle. The applied color has previously been specified right in the code. Let's add the macro substitution featuring the default color:
//--- Pending request type IDs #define PENDING_REQUEST_ID_TYPE_ERR (1) // Type of a pending request created based on the server return code #define PENDING_REQUEST_ID_TYPE_REQ (2) // Type of a pending request created by request //--- Timeseries parameters #define SERIES_DEFAULT_BARS_COUNT (1000) // Required default amount of timeseries data #define PAUSE_FOR_SYNC_ATTEMPTS (16) // Amount of pause milliseconds between synchronization attempts #define ATTEMPTS_FOR_SYNC (5) // Number of attempts to receive synchronization with the server //--- Tick series parameters #define TICKSERIES_DEFAULT_DAYS_COUNT (1) // Required number of days for tick data in default series #define TICKSERIES_MAX_DATA_TOTAL (200000) // Maximum number of stored tick data of a single symbol //--- Parameters of the DOM snapshot series #define MBOOKSERIES_DEFAULT_DAYS_COUNT (1) // The default required number of days for DOM snapshots in the series #define MBOOKSERIES_MAX_DATA_TOTAL (200000) // Maximum number of stored DOM snapshots of a single symbol //--- Canvas parameters #define PAUSE_FOR_CANV_UPDATE (16) // Canvas update frequency #define CLR_CANV_NULL (0x00FFFFFF) // Zero for the canvas with the alpha channel #define OUTER_AREA_SIZE (16) // Size of one side of the outer area around the form workspace //--- 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_RADIUS (5) // Radius of the control point on the form for managing graphical object pivot points #define CTRL_POINT_COLOR (clrDodgerBlue) // 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 | //+------------------------------------------------------------------+
Rename the CTRL_POINT_SIZE macro substitution into CTRL_POINT_RADIUS since this is not its full size of the circle, but its radius. The macro substitution name was a little misleading when calculating the active area of the form object.
In the file of the graphical element object class \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh, slightly improve the method of creating a graphical element object. Unfortunately, there is no returning of the error code when calling the CreateBitmapLabel() method of the CCanvas class. Therefore, reset the last error code before calling the method. If failed to create a graphical label, display the error code in the journal. This slightly improves debugging.
//+------------------------------------------------------------------+ //| Create the graphical element object | //+------------------------------------------------------------------+ bool CGCnvElement::Create(const long chart_id, // Chart ID const int wnd_num, // Chart subwindow const string name, // Element name const int x, // X coordinate const int y, // Y coordinate const int w, // Width const int h, // Height const color colour, // Background color const uchar opacity, // Opacity const bool redraw=false) // Flag indicating the need to redraw { ::ResetLastError(); if(this.m_canvas.CreateBitmapLabel(chart_id,wnd_num,name,x,y,w,h,COLOR_FORMAT_ARGB_NORMALIZE)) { this.Erase(CLR_CANV_NULL); this.m_canvas.Update(redraw); this.m_shift_y=(int)::ChartGetInteger(chart_id,CHART_WINDOW_YDISTANCE,wnd_num); return true; } CMessage::ToLog(DFUN,::GetLastError(),true); return false; } //+------------------------------------------------------------------+
Why did I have to do this? When creating form objects, I spent a lot of time trying to understand why I am unable to create a graphical resource. Finally, it turned out that the created name of a graphical resource exceeded 63 characters. If the method for creating a graphical label of the CCanvas class reported the error, we would not have to search for anything. Instead, we would immediately get the error code and I would not have to pass along all loops of calling various methods in different classes.
However, this improvement will be of no help if the reosurce name length exceeds 63 characters:
ERR_RESOURCE_NAME_IS_TOO_LONG 4018 The resource name exceeds 63 characters
returning the error code
ERR_RESOURCE_NOT_FOUND 4016 Resource with such a name is not found in EX5
but this is still better as this immediately provokes the question: "Why is the graphical resource not created?"...
Let's make improvements in the file of the extended standard graphical object toolkit class in the file \MQL5\Include\DoEasy\Objects\Graph\Extend\CGStdGraphObjExtToolkit.mqh.
Each graphical object features one or several pivot points used to position the object on a chart. A form object is attached to each of the points. Standard graphical objects have their own points for relocation. They appear when selecting a graphical object. We are not going to manage extended standard graphical objects in the library. To implement the functionality, it will be more convenient for us to use form objects to move pivot points of graphical objects. There may be more such form objects than control points. Therefore, we cannot define the number of form objects by the number of pivot points of a graphical object. However, we need to know this number. Therefore, in the public section of the class, add the method returning the number of created form objects for managing pivot points and declare the method drawing the points of managing pivot points on fully transparent form objects:
//--- (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 (1) base object pivot points and (2) newly created form objects for managing reference points int GetNumPivotsBaseObj(void) const { return this.m_base_pivots; } int GetNumControlPointForms(void) const { return this.m_list_forms.Total(); } //--- Create form objects on the base object pivot points bool CreateAllControlPointForm(void); //--- Draw a reference point on the form void DrawControlPoint(CForm *form,const uchar opacity,const color clr); //--- Remove all form objects from the list void DeleteAllControlPointForm(void);
In the method creating a form object on the base object pivot point, shorten the name of a created object — replace "_TKPP_" with "_CP_":
//+------------------------------------------------------------------+ //| Create a form object on a base object reference point | //+------------------------------------------------------------------+ CForm *CGStdGraphObjExtToolkit::CreateNewControlPointForm(const int index) { string name=this.m_base_name+"_CP_"+(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()); } //+------------------------------------------------------------------+
Here (and in the test EA file) I had to shorten the name of the created form object since the name of the graphical resource exceeded 63 characters and no object was created. The reason lies in the dynamic resource creation method in the CCanvas class, where the name of the created resource consists of "::" symbols + the name passed to the method (and specified in the method described above) + chart ID + number of milliseconds passed since the system start + pseudo random number:
//+------------------------------------------------------------------+ //| Create dynamic resource | //+------------------------------------------------------------------+ bool CCanvas::Create(const string name,const int width,const int height,ENUM_COLOR_FORMAT clrfmt) { Destroy(); //--- prepare data array if(width>0 && height>0 && ArrayResize(m_pixels,width*height)>0) { //--- generate resource name m_rcname="::"+name+(string)ChartID()+(string)(GetTickCount()+MathRand()); //--- initialize data with zeros ArrayInitialize(m_pixels,0); //--- create dynamic resource if(ResourceCreate(m_rcname,m_pixels,width,height,0,0,0,clrfmt)) { //--- successfully created //--- complete initialization m_width =width; m_height=height; m_format=clrfmt; //--- succeed return(true); } } //--- error - destroy object Destroy(); return(false); } //+------------------------------------------------------------------+
Unfortunately, all this imposes serious restrictions on selecting an apprehensible name for the created object.
In the method creating form objects on the base object pivot points, calculate the indent from each side of the form object for specifying the location and size of the form active area which is to be located in the center of the form, while its size should be equal to the two values set in the CTRL_POINT_RADIUS macro substitution. Since we are dealing with the radius, we need to use two radius values, subtract them from the form width (the form height is equal to its width) and divide the obtained value by two so that the form active area is equal to the circle drawn in its center.
Specify the active area border indent from the form edge in the SetActiveAreaShift() method:
//+------------------------------------------------------------------+ //| 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 int x=(int)::floor((form.Width()-CTRL_POINT_RADIUS*2)/2);// Active area shift from the form edge form.SetActiveAreaShift(x,x,x,x); // Object active area is located in the center of the form, its size is equal to the two CTRL_POINT_RADIUS values form.SetFlagSelected(false,false); // Object is not selected form.SetFlagSelectable(false,false); // Object cannot be selected by mouse form.Erase(CLR_CANV_NULL,0); // Fill in the form with transparent color and set the full transparency //form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,clrSilver); // Draw an outlining rectangle for visual display of the form location //form.DrawRectangle(x,x,form.Width()-x-1,form.Height()-x-1,clrSilver);// Draw an outlining rectangle for visual display of the form active area location this.DrawControlPoint(form,0,CTRL_POINT_COLOR); // Draw a circle and 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; } //+------------------------------------------------------------------+
When creating the form, we are able to draw rectangles displaying the size of the form and its active area for debugging — these strings have been commented out. Use the new method considered below to draw the completely transparent circles at the center of the form (but is it a good idea to draw them at all?).
The method drawing a reference point on the form:
//+------------------------------------------------------------------+ //| Draw a reference point on the form | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::DrawControlPoint(CForm *form,const uchar opacity,const color clr) { if(form==NULL) return; form.DrawCircle((int)::floor(form.Width()/2),(int)::floor(form.Height()/2),CTRL_POINT_RADIUS,clr,opacity);// Draw a circle at the center of the form form.DrawCircleFill((int)::floor(form.Width()/2),(int)::floor(form.Height()/2),2,clr,opacity); // Draw a point at the center of the form } //+------------------------------------------------------------------+
The method contains two strings taken from the method considered above. Why? We need to either show or hide a point with a circle at the center of the form at various points in time. To achieve this, we will call the method by specifying the necessary non-transparency and color of shapes being drawn.
Remove handling the mouse cursor movement from the event handler as it is no longer needed:
//+------------------------------------------------------------------+ //| 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); } if(id==CHARTEVENT_MOUSE_MOVE) { for(int i=0;i<this.m_list_forms.Total();i++) { CForm *form=this.m_list_forms.At(i); if(form==NULL) continue; form.OnChartEvent(id,lparam,dparam,sparam); } ::ChartRedraw(this.m_base_chart_id); } } //+------------------------------------------------------------------+
Improve the abstract standard graphical object class in \MQL5\Include\DoEasy\Objects\Graph\Standard\GStdGraphObj.mqh.
In the public section of the class, declare the method allowing us to change the coordinates of the graphical object pivot points and of the objects bound to it simultaneously:
//--- Return (1) the list of dependent objects, (2) dependent graphical object by index and (3) the number of dependent objects CArrayObj *GetListDependentObj(void) { return &this.m_list; } CGStdGraphObj *GetDependentObj(const int index) { return this.m_list.At(index); } int GetNumDependentObj(void) { return this.m_list.Total(); } //--- Return the name of the dependent object by index string NameDependent(const int index); //--- Add the dependent graphical object to the list bool AddDependentObj(CGStdGraphObj *obj); //--- Change X and Y coordinates of the current and all dependent objects bool ChangeCoordsExtendedObj(const int x,const int y,const int modifier,bool redraw=false); //--- Return the object of data on pivot points CLinkedPivotPoint*GetLinkedPivotPoint(void) { return &this.m_linked_pivots; }
Declare three methods to access the form for managing graphical object pivot points:
//--- Return the number of base object pivot points for calculating the coordinates in the (1) current and (2) specified object int GetLinkedCoordsNum(void) const { return this.m_linked_pivots.GetNumLinkedCoords(); } int GetLinkedPivotsNum(CGStdGraphObj *obj) const { return(obj!=NULL ? obj.GetLinkedCoordsNum() : 0); } //--- Return the form for managing an object pivot point CForm *GetControlPointForm(const int index); //--- Return the number of form objects for managing reference points int GetNumControlPointForms(void); //--- Redraw the form for managing a reference point of an extended standard graphical object void RedrawControlPointForms(const uchar opacity,const color clr); private:
Add the method setting the time and price by screen coordinates:
//--- Symbol for the Chart object string ChartObjSymbol(void) const { return this.GetProperty(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL,0); } bool SetChartObjSymbol(const string symbol) { if(!::ObjectSetString(CGBaseObj::ChartID(),CGBaseObj::Name(),OBJPROP_SYMBOL,symbol)) return false; this.SetProperty(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL,0,symbol); return true; } //--- Set the time and price by screen coordinates bool SetTimePrice(const int x,const int y,const int modifier) { bool res=true; ENUM_OBJECT type=this.GraphObjectType(); if(type==OBJ_LABEL || type==OBJ_BUTTON || type==OBJ_BITMAP_LABEL || type==OBJ_EDIT || type==OBJ_RECTANGLE_LABEL) { res &=this.SetXDistance(x); res &=this.SetYDistance(y); } else { int subwnd=0; datetime time=0; double price=0; if(::ChartXYToTimePrice(this.ChartID(),x,y,subwnd,time,price)) { res &=this.SetTime(time,modifier); res &=this.SetPrice(price,modifier); } } return res; } //--- Return the flags indicating object visibility on timeframes
We need to recalculate screen coordinates into time/price coordinates to be able to handle graphical objects in X and Y screen coordinates. The method first checks the current object type. If it is constructed based on the screen coordinates, its screen coordinates change immediately. If the graphical object is based on time/price coordinates, we first need to convert the screen coordinates passed to the method into time and price values. The obtained values are then set in the graphical object parameters.
The method returning the form for managing the object pivot point:
//+------------------------------------------------------------------+ //| Return the form for managing an object pivot point | //+------------------------------------------------------------------+ CForm *CGStdGraphObj::GetControlPointForm(const int index) { return(this.ExtToolkit!=NULL ? this.ExtToolkit.GetControlPointForm(index) : NULL); } //+------------------------------------------------------------------+
Here all is simple: if the object of the extended standard graphical object toolkit exists, the form object by index is returned. Otherwise, return NULL.
The method returning the number of form objects for managing reference points:
//+------------------------------------------------------------------+ //| Return the number of form objects | //| for managing control points | //+------------------------------------------------------------------+ int CGStdGraphObj::GetNumControlPointForms(void) { return(this.ExtToolkit!=NULL ? this.ExtToolkit.GetNumControlPointForms() : 0); } //+------------------------------------------------------------------+
The method is similar to the one described above: if the object of the extended standard graphical object toolkit exists, the number of form objects is returned. Otherwise, return 0.
The method redrawing the form for managing a reference point of an extended standard graphical object:
//+------------------------------------------------------------------+ //| Redraw the form for managing the control point | //| of the extended standard graphical object | //+------------------------------------------------------------------+ void CGStdGraphObj::RedrawControlPointForms(const uchar opacity,const color clr) { //--- If the object has no extended standard graphical object toolkit, exit if(this.ExtToolkit==NULL) return; //--- Get the number of pivot point handling forms int total_form=this.GetNumControlPointForms(); //--- In the loop by the number of pivot point management forms for(int i=0;i<total_form;i++) { //--- get the next form object CForm *form=this.ExtToolkit.GetControlPointForm(i); if(form==NULL) continue; //--- draw a point and a circle with the specified non-transparency and color this.ExtToolkit.DrawControlPoint(form,opacity,clr); } //--- Get the total number of bound graphical objects int total_dep=this.GetNumDependentObj(); //--- In the loop by all bound graphical objects, for(int i=0;i<total_dep;i++) { //--- get the next graphical object from the list CGStdGraphObj *dep=this.GetDependentObj(i); if(dep==NULL) continue; //--- call the method for it dep.RedrawControlPointForms(opacity,clr); } } //+------------------------------------------------------------------+
The method is accompanied by detailed comments. Briefly, the idea is as follows: first, we redraw all form objects bound to the current object. As we know, dependent graphical objects can be attached to the object. These dependent objects also have form objects of their own. Therefore, let's call this method for each of the dependent objects in the loop by all dependent objects. In turn, the dependent objects loop over the list of their own dependent objects and call the same method for them. This is done till all form objects for all linked graphical objects are redrawn.
The method changing X and Y coordinates of the current and all dependent objects:
//+----------------------------------------------------------------------+ //| Change X and Y coordinates of the current and all dependent objects | //+----------------------------------------------------------------------+ bool CGStdGraphObj::ChangeCoordsExtendedObj(const int x,const int y,const int modifier,bool redraw=false) { //--- Set the new coordinates for the pivot point set in 'modifier' if(!this.SetTimePrice(x,y,modifier)) return false; //--- If the object is not a composite graphical object //--- or dependent graphical objects are not attached to the object, //--- there is nothing we can do here, return 'true' if(this.ExtToolkit==NULL || this.m_list.Total()==0) return true; //--- Get the graphical object bound to the 'modifier' point CGStdGraphObj *dep=this.GetDependentObj(modifier); if(dep==NULL) return false; //--- Get the object of the pivot point data of the attached graphical object CLinkedPivotPoint *pp=dep.GetLinkedPivotPoint(); if(pp==NULL) return false; //--- get the number of coordinate points the object is attached to int num=pp.GetNumLinkedCoords(); //--- In the loop by the object coordinate points, for(int j=0;j<num;j++) { //--- get the number of coordinate points of the base object for setting the X coordinate int numx=pp.GetBasePivotsNumX(j); //--- In the loop by each coordinate point for setting the X coordinate, for(int nx=0;nx<numx;nx++) { //--- get the property for setting the X coordinate, its modifier //--- and set it to the dependent graphical object attached to the 'modifier' point int prop_from=pp.GetPropertyX(j,nx); int modifier_from=pp.GetPropertyModifierX(j,nx); this.SetCoordXToDependentObj(dep,prop_from,modifier_from,nx); } //--- Get the number of coordinate points of the base object for setting the Y coordinate int numy=pp.GetBasePivotsNumY(j); //--- In the loop by each coordinate point for setting the Y coordinate, for(int ny=0;ny<numy;ny++) { //--- get the property for setting the Y coordinate, its modifier //--- and set it to the dependent graphical object attached to the 'modifier' point int prop_from=pp.GetPropertyY(j,ny); int modifier_from=pp.GetPropertyModifierY(j,ny); this.SetCoordYToDependentObj(dep,prop_from,modifier_from,ny); } } //--- Save the current properties of the dependent graphical object as the previous ones dep.PropertiesCopyToPrevData(); //--- Move the management control point to new coordinates this.ExtToolkit.SetBaseObjTimePrice(this.Time(modifier),this.Price(modifier),modifier); this.ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance()); //--- Redraw the chart, if the flag is enabled if(redraw) ::ChartRedraw(m_chart_id); //--- If all is successful, return 'true' return true; } //+------------------------------------------------------------------+
The method is accompanied by detailed comments. In short, we first change the coordinates of the specified pivot point of the current object. If the object has dependent graphical objects bound to it, they should be moved to the new coordinates. This is exactly what happens in the method.
Remove handling mouse movements from the event handler as it is no longer needed:
//+------------------------------------------------------------------+ //| 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; string name=this.Name(); 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,name); } if(id==CHARTEVENT_MOUSE_MOVE) { if(ExtToolkit!=NULL) ExtToolkit.OnChartEvent(id,lparam,dparam,name); } } //+------------------------------------------------------------------+
Let's improve the collection class of graphical objects in \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh.
When we move the cursor over the chart, while the chart features objects, the object the mouse cursor is hovering over is defined in the event handler. The handler of the mouse status relative to the object is then launched. When we hover the cursor over the object for handling pivot points of the extended standard graphical object, the form itself, as well as its index and the graphical object the form is attached to can be seen in the handler. In order not to look for this form and graphical object again outside the handler, we need to simply save the ID of the graphical object the form is attached to and the index of the form, above which the cursor is hovering, in the variables. This data allows us to quickly select the necessary objects from the lists and understand that the cursor is located above the form — by the value of these variables.
Insert these variables in the declaration of the method returning the pointer to the form located under the cursor:
//--- Return the pointer to the form located under the cursor CForm *GetFormUnderCursor(const int id, const long &lparam, const double &dparam, const string &sparam, ENUM_MOUSE_FORM_STATE &mouse_state, long &obj_ext_id, int &form_index);
The variables are to be passed to the method by a link. In other words, we will simply set the necessary values inside the method, and they will be saved in the appropriate variables for later use.
In the public section of the class, declare two methods returning the extended and standard graphical objects by ID:
//--- Return an (1) existing and (2) removed graphical object by chart name and ID CGStdGraphObj *GetStdGraphObject(const string name,const long chart_id); CGStdGraphObj *GetStdDelGraphObject(const string name,const long chart_id); //--- Return the existing (1) extended and (2) standard graphical object by its ID CGStdGraphObj *GetStdGraphObjectExt(const long id,const long chart_id); CGStdGraphObj *GetStdGraphObject(const long id,const long chart_id); //--- Return the list of (1) chart management objects and (2) removed graphical objects CArrayObj *GetListChartsControl(void) { return &this.m_list_charts_control; } CArrayObj *GetListDeletedObj(void) { return &this.m_list_deleted_obj; }
We need these methods to receive the pointer to the graphical object by its ID. Let's have a look at the implementation of these methods.
The method returning the existing extended standard graphical object by its ID:
//+------------------------------------------------------------------+ //| Return the existing extended standard | //| graphical object by its ID | //+------------------------------------------------------------------+ CGStdGraphObj *CGraphElementsCollection::GetStdGraphObjectExt(const long id,const long chart_id) { CArrayObj *list=this.GetListStdGraphObjectExt(); list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_CHART_ID,0,chart_id,EQUAL); list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_ID,0,id,EQUAL); return(list!=NULL && list.Total()>0 ? list.At(0) : NULL); } //+------------------------------------------------------------------+
Here we receive the list of extended standard graphical objects. Only objects with the specified chart ID remain in the list.
From the obtained list, select an object with the specified object ID. If the obtained list is valid and not empty, return the pointer to the necessary object contained in the list. Otherwise, return NULL.
The method returning the existing standard graphical object by its ID:
//+------------------------------------------------------------------+ //| Return the existing standard | //| graphical object by its ID | //+------------------------------------------------------------------+ CGStdGraphObj *CGraphElementsCollection::GetStdGraphObject(const long id,const long chart_id) { CArrayObj *list=this.GetList(GRAPH_OBJ_PROP_CHART_ID,0,chart_id); list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_ID,0,id,EQUAL); return(list!=NULL && list.Total()>0 ? list.At(0) : NULL); } //+------------------------------------------------------------------+
Here we receive the list of graphical objects by chart ID. In the obtained list, leave the object with the specified object ID.
If the obtained list is valid and not empty, return the pointer to the necessary object contained in the list. Otherwise, return NULL.
Let's improve the method returning the pointer to the form located under the cursor. We need to add and initialize two new variables for storing the ID of the extended standard graphical object and the index of the anchor point the form handles, as well as to insert the block for handling extended standard graphical objects — the search for forms, which are attached to these objects (and the mouse cursor is hovering over):
//+------------------------------------------------------------------+ //| Return the pointer to the form located under the cursor | //+------------------------------------------------------------------+ CForm *CGraphElementsCollection::GetFormUnderCursor(const int id, const long &lparam, const double &dparam, const string &sparam, ENUM_MOUSE_FORM_STATE &mouse_state, long &obj_ext_id, int &form_index) { //--- Set the ID of the extended standard graphical object to -1 //--- and the index of the anchor point managed by the form to -1 obj_ext_id=WRONG_VALUE; form_index=WRONG_VALUE; //--- Initialize the mouse status relative to the form mouse_state=MOUSE_FORM_STATE_NONE; //--- Declare the pointers to graphical element collection class objects CGCnvElement *elm=NULL; CForm *form=NULL; //--- Get the list of objects the interaction flag is set for (there should be only one object) CArrayObj *list=CSelect::ByGraphCanvElementProperty(GetListCanvElm(),CANV_ELEMENT_PROP_INTERACTION,true,EQUAL); //--- If managed to obtain the list and it is not empty if(list!=NULL && list.Total()>0) { //--- Get the only graphical element there elm=list.At(0); //--- If the element is a form object if(elm.TypeGraphElement()==GRAPH_ELEMENT_TYPE_FORM) { //--- Assign the pointer to the element for the form object pointer form=elm; //--- Get the mouse status relative to the form mouse_state=form.MouseFormState(id,lparam,dparam,sparam); //--- If the cursor is within the form, return the pointer to the form if(mouse_state>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL) return form; } } //--- If there is no a single form object with a specified interaction flag, //--- in the loop by all graphical element collection class objects int total=this.m_list_all_canv_elm_obj.Total(); for(int i=0;i<total;i++) { //--- get the next element elm=this.m_list_all_canv_elm_obj.At(i); if(elm==NULL) continue; //--- if the obtained element is a form object if(elm.TypeGraphElement()==GRAPH_ELEMENT_TYPE_FORM) { //--- Assign the pointer to the element for the form object pointer form=elm; //--- Get the mouse status relative to the form mouse_state=form.MouseFormState(id,lparam,dparam,sparam); //--- If the cursor is within the form, return the pointer to the form if(mouse_state>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL) return form; } } //--- If there is no a single form object from the collection list //--- Get the list of extended standard graphical objects list=this.GetListStdGraphObjectExt(); if(list!=NULL) { //--- in the loop by all extended standard graphical objects for(int i=0;i<list.Total();i++) { //--- get the next graphical object, CGStdGraphObj *obj_ext=list.At(i); if(obj_ext==NULL) continue; //--- get the object of its toolkit, CGStdGraphObjExtToolkit *toolkit=obj_ext.GetExtToolkit(); if(toolkit==NULL) continue; //--- handle the event of changing the chart for the current graphical object obj_ext.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam); //--- Get the total number of form objects created for the current graphical object total=toolkit.GetNumControlPointForms(); //--- In the loop by all form objects for(int j=0;j<total;j++) { //--- get the next form object, form=toolkit.GetControlPointForm(j); if(form==NULL) continue; //--- get the mouse status relative to the form mouse_state=form.MouseFormState(id,lparam,dparam,sparam); //--- If the cursor is inside the form, if(mouse_state>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL) { //--- set the object ID and form index //--- and return the pointer to the form obj_ext_id=obj_ext.ObjectID(); form_index=j; return form; } } } } //--- Nothing is found - return NULL return NULL; } //+------------------------------------------------------------------+
The entire logic of the added code block is described in the comments. In brief, we need to find a form object the mouse cursor is hovering over. First, we are looking for form objects stored in the list of collection class graphical elements. If not a single form is found, we should pass along all extended standard graphical objects while searching for their forms — the cursor can be over one of them. If this is the case, the ID of the extended standard graphical object the form is attached to is set in the variables passed to the method by the link. The form index is set as well so that we are able to know the pivot point and the graphical object managed by the form.
Now we should handle the interaction of the mouse cursor with form objects of extended standard graphical objects in the event handler. In addition, let's implement control over relocation of form objects so that they cannot enter the chart area in its upper right corner where the one-click trading mode activation button is located. This button always remains on top of all objects, so the relocated form should not enter its area to avoid accidental button clicks. If the one-click panel is already activated, the form should not enter its area as well since it may cause inconvenience when handling the form.
Let's have a look at the improvements and changes in the event handler:
//+------------------------------------------------------------------+ //| Event handler | //+------------------------------------------------------------------+ void CGraphElementsCollection::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { CGStdGraphObj *obj_std=NULL; // Pointer to the standard graphical object CGCnvElement *obj_cnv=NULL; // Pointer to the graphical element object on canvas ushort idx=ushort(id-CHARTEVENT_CUSTOM); if(id==CHARTEVENT_OBJECT_CHANGE || id==CHARTEVENT_OBJECT_DRAG || id==CHARTEVENT_OBJECT_CLICK || idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG || idx==CHARTEVENT_OBJECT_CLICK) { //--- Calculate the chart ID //--- If the event ID corresponds to an event from the current chart, the chart ID is received from ChartID //--- If the event ID corresponds to a user event, the chart ID is received from lparam //--- Otherwise, the chart ID is assigned to -1 long param=(id==CHARTEVENT_OBJECT_CLICK ? ::ChartID() : idx==CHARTEVENT_OBJECT_CLICK ? lparam : WRONG_VALUE); long chart_id=(param==WRONG_VALUE ? (lparam==0 ? ::ChartID() : lparam) : param); //--- Get the object, whose properties were changed or which was relocated, //--- from the collection list by its name set in sparam obj_std=this.GetStdGraphObject(sparam,chart_id); //--- If failed to get the object by its name, it is not on the list, //--- which means its name has been changed if(obj_std==NULL) { //--- Let's search the list for the object that is not on the chart obj_std=this.FindMissingObj(chart_id); //--- If failed to find the object here as well, exit if(obj_std==NULL) return; //--- Get the name of the renamed graphical object on the chart, which is not in the collection list string name_new=this.FindExtraObj(chart_id); //--- set a new name for the collection list object, which does not correspond to any graphical object on the chart, //--- and send an event with the new name of the object to the control program chart if(obj_std.SetNamePrev(obj_std.Name()) && obj_std.SetName(name_new)) ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_RENAME,obj_std.ChartID(),obj_std.TimeCreate(),obj_std.Name()); } //--- Update the properties of the obtained object //--- and check their change obj_std.PropertiesRefresh(); obj_std.PropertiesCheckChanged(); } //--- Handle standard graphical object events in the collection list for(int i=0;i<this.m_list_all_graph_obj.Total();i++) { //--- Get the next graphical object and obj_std=this.m_list_all_graph_obj.At(i); if(obj_std==NULL) continue; //--- call its event handler obj_std.OnChartEvent((id<CHARTEVENT_CUSTOM ? id : idx),lparam,dparam,sparam); } //--- Handle chart changes for extended standard objects if(id==CHARTEVENT_CHART_CHANGE || idx==CHARTEVENT_CHART_CHANGE) { CArrayObj *list=this.GetListStdGraphObjectExt(); if(list!=NULL) { for(int i=0;i<list.Total();i++) { obj_std=list.At(i); if(obj_std==NULL) continue; obj_std.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam); } } } //--- Handling mouse events of graphical objects on canvas //--- If the event is not a chart change else { //--- Check whether the mouse button is pressed bool pressed=(this.m_mouse.ButtonKeyState(id,lparam,dparam,sparam)==MOUSE_BUTT_KEY_STATE_LEFT ? true : false); ENUM_MOUSE_FORM_STATE mouse_state=MOUSE_FORM_STATE_NONE; //--- Declare static variables for the active form and status flags static CForm *form=NULL; static bool pressed_chart=false; static bool pressed_form=false; static bool move=false; //--- Declare static variables for the form index of managing an extended standard graphical object and its ID static int form_index=WRONG_VALUE; static long graph_obj_id=WRONG_VALUE; //--- If the button is not pressed on the chart and the movement flag is not set, get the form, above which the cursor is located if(!pressed_chart && !move) form=this.GetFormUnderCursor(id,lparam,dparam,sparam,mouse_state,graph_obj_id,form_index); //--- If the button is not pressed, reset all flags and enable the chart tools if(!pressed) { pressed_chart=false; pressed_form=false; move=false; this.SetChartTools(::ChartID(),true); } //--- If this is a mouse movement event and the movement flag is active, move the form, above which the cursor is located (if the pointer to it is valid) if(id==CHARTEVENT_MOUSE_MOVE && move) { if(form!=NULL) { //--- calculate the cursor movement relative to the form coordinate origin int x=this.m_mouse.CoordX()-form.OffsetX(); int y=this.m_mouse.CoordY()-form.OffsetY(); //--- get the width and height of the chart the form is located at int chart_width=(int)::ChartGetInteger(form.ChartID(),CHART_WIDTH_IN_PIXELS,form.SubWindow()); int chart_height=(int)::ChartGetInteger(form.ChartID(),CHART_HEIGHT_IN_PIXELS,form.SubWindow()); //--- If the form is outside the extended standard graphical object if(form_index==WRONG_VALUE) { //--- Adjust the calculated form coordinates if the form is out of the chart range if(x<0) x=0; if(x>chart_width-form.Width()) x=chart_width-form.Width(); if(y<0) y=0; if(y>chart_height-form.Height()) y=chart_height-form.Height(); //--- If the chart has no one-click trading panel, if(!::ChartGetInteger(form.ChartID(),CHART_SHOW_ONE_CLICK)) { //--- calculate the form coordinate so that the form does not enter the one-click trading button during relocation if(y<17 && x<41) y=17; } //--- If the chart has the one-click trading panel enabled, else { //--- calculate the form coordinate so that the form does not enter the one-click trading panel area during relocation if(y<80 && x<192) y=80; } } //--- If the form is included into the extended standard graphical object else { if(graph_obj_id>WRONG_VALUE) { //--- Get the list of objects by the object ID (there should be one object) CArrayObj *list_ext=CSelect::ByGraphicStdObjectProperty(GetListStdGraphObjectExt(),GRAPH_OBJ_PROP_ID,0,graph_obj_id,EQUAL); //--- If managed to obtain the list and it is not empty, if(list_ext!=NULL && list_ext.Total()>0) { //--- get the graphical object from the list CGStdGraphObj *ext=list_ext.At(0); //--- If the pointer to the object is received, if(ext!=NULL) { //--- get the object type ENUM_OBJECT type=ext.GraphObjectType(); //--- If the object is constructed based on screen coordinates, set the coordinates to the object if(type==OBJ_LABEL || type==OBJ_BUTTON || type==OBJ_BITMAP_LABEL || type==OBJ_EDIT || type==OBJ_RECTANGLE_LABEL) { ext.SetXDistance(x); ext.SetYDistance(y); } //--- otherwise, if the object is based on time/price coordinates else { //--- calculate the coordinate shift and limit the coordinates so that they are not out of the chart range int shift=(int)::ceil(form.Width()/2)+1; if(x+shift<0) x=-shift; if(x+shift>chart_width) x=chart_width-shift; if(y+shift<0) y=-shift; if(y+shift>chart_height) y=chart_height-shift; //--- set calculated coordinates in the object ext.ChangeCoordsExtendedObj(x+shift,y+shift,form_index); } } } } } //--- Move the form by the obtained coordinates form.Move(x,y,true); } } //--- Display debugging comments on the chart Comment ( (form!=NULL ? form.Name()+":" : ""),"\n", EnumToString((ENUM_CHART_EVENT)id),"\n", EnumToString(this.m_mouse.ButtonKeyState(id,lparam,dparam,sparam)), "\n",EnumToString(mouse_state), "\npressed=",pressed,", move=",move,(form!=NULL ? ", Interaction="+(string)form.Interaction() : ""), "\npressed_chart=",pressed_chart,", pressed_form=",pressed_form, "\nform_index=",form_index,", graph_obj_id=",graph_obj_id ); //--- If the cursor is not above the form if(form==NULL) { //--- If the mouse button is pressed if(pressed) { //--- If the button is still pressed and held on the form, exit if(pressed_form) { return; } //--- If the button hold flag is not enabled yet, set the flags and enable chart tools if(!pressed_chart) { pressed_chart=true; // Button is held on the chart pressed_form=false; // Cursor is not above the form move=false; // movement disabled this.SetChartTools(::ChartID(),true); } } //--- If the mouse button is not pressed else { //--- Get the list of extended standard graphical objects CArrayObj *list_ext=GetListStdGraphObjectExt(); //--- In the loop by all extended graphical objects, int total=list_ext.Total(); for(int i=0;i<total;i++) { //--- get the next graphical object CGStdGraphObj *obj=list_ext.At(i); if(obj==NULL) continue; //--- and redraw it without a point and a circle obj.RedrawControlPointForms(0,CTRL_POINT_COLOR); } } } //--- If the cursor is above the form else { //--- If the button is still pressed and held on the chart, exit if(pressed_chart) { return; } //--- If the flag of holding the button on the form is not set yet if(!pressed_form) { pressed_chart=false; // The button is not pressed on the chart this.SetChartTools(::ChartID(),false); //--- 'The cursor is inside the form, no mouse buttons are clicked' event handler if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED) { //--- If the cursor is above the extended graphical object pivot point control form, if(graph_obj_id>WRONG_VALUE) { //--- get the object by the object and chart IDs CGStdGraphObj *graph_obj=this.GetStdGraphObjectExt(graph_obj_id,form.ChartID()); if(graph_obj!=NULL) { //--- Get the toolkit of the extended standard graphical object CGStdGraphObjExtToolkit *toolkit=graph_obj.GetExtToolkit(); if(toolkit!=NULL) { //--- Draw a circle with a point on the form toolkit.DrawControlPoint(form,255,CTRL_POINT_COLOR); } } } } //--- 'The cursor is inside the form, a mouse button is clicked (any)' event handler if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_PRESSED) { this.SetChartTools(::ChartID(),false); //--- If the flag of holding the form is not set yet if(!pressed_form) { pressed_form=true; // set the flag of pressing on the form pressed_chart=false; // remove the flag of pressing on the chart } } //--- 'The cursor is inside the form, the mouse wheel is being scrolled' event handler workpiece if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_WHEEL) { } //--- 'The cursor is inside the active area, the mouse buttons are not clicked' event handler if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED) { //--- Set the cursor shift relative to the form initial coordinates form.SetOffsetX(this.m_mouse.CoordX()-form.CoordX()); form.SetOffsetY(this.m_mouse.CoordY()-form.CoordY()); //--- If the cursor is above the active area of the extended graphical object pivot point control form if(graph_obj_id>WRONG_VALUE) { //--- get the object by the object and chart IDs CGStdGraphObj *graph_obj=this.GetStdGraphObjectExt(graph_obj_id,form.ChartID()); if(graph_obj!=NULL) { //--- Get the toolkit of the extended standard graphical object CGStdGraphObjExtToolkit *toolkit=graph_obj.GetExtToolkit(); if(toolkit!=NULL) { //--- Draw a circle with a point on the form toolkit.DrawControlPoint(form,255,CTRL_POINT_COLOR); } } } } //--- 'The cursor is inside the active area, any mouse button is clicked' event handler if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED && !move) { pressed_form=true; // the flag of holding the mouse button on the form //--- If the left mouse button is pressed if(this.m_mouse.IsPressedButtonLeft()) { //--- Set flags and form parameters move=true; // movement flag form.SetInteraction(true); // flag of the form interaction with the environment form.BringToTop(); // form on the background - above all others this.ResetAllInteractionExeptOne(form); // Reset interaction flags for all forms except the current one form.SetOffsetX(this.m_mouse.CoordX()-form.CoordX()); // Cursor shift relative to the X coordinate form.SetOffsetY(this.m_mouse.CoordY()-form.CoordY()); // Cursor shift relative to the Y coordinate } } //--- 'The cursor is inside the active area, the mouse wheel is being scrolled' event handler workpiece if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL) { } //--- 'The cursor is inside the window scrolling area, no mouse buttons are clicked' event handler workpiece if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED) { } //--- 'The cursor is inside the window scrolling area, a mouse button is clicked (any)' event handler workpiece if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED) { } //--- 'The cursor is inside the window scrolling area, the mouse wheel is being scrolled' event handler workpiece if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL) { } } } } } //+------------------------------------------------------------------+
All improvements in the method are accompanied by detailed comments in the code. If you have any questions, feel free to ask them in the comments below.
These are all the improvements I was going to make today.
Test
To perform the test, let's use the EA from the previous article and save it to \MQL5\Experts\TestDoEasy\Part98\ as TestDoEasyPart98.mq5.
The only change to be implemented is the creation of three form objects:
//+------------------------------------------------------------------+ //| TestDoEasyPart98.mq5 | //| 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" //--- includes #include <DoEasy\Engine.mqh> //--- defines #define FORMS_TOTAL (3) // Number of created forms #define START_X (4) // Initial X coordinate of the shape #define START_Y (4) // Initial Y coordinate of the shape #define KEY_LEFT (188) // Left #define KEY_RIGHT (190) // Right #define KEY_ORIGIN (191) // Initial properties //--- input parameters sinput bool InpMovable = true; // Movable forms flag sinput ENUM_INPUT_YES_NO InpUseColorBG = INPUT_YES; // Use chart background color to calculate shadow color sinput color InpColorForm3 = clrCadetBlue; // Third form shadow color (if not background color) //--- global variables CEngine engine; color array_clr[]; //+------------------------------------------------------------------+
In this regard, let's slightly adjust the calculation of coordinates of each created form.
Form object declaration is set outside the loop.
The first form is to be constructed on the Y coordinate of 100, while the remaining ones — with the indent of 20 pixels from the bottom edge of the previous form:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Set EA global variables ArrayResize(array_clr,2); // Array of gradient filling colors array_clr[0]=C'26,100,128'; // Original ≈Dark-azure color array_clr[1]=C'35,133,169'; // Lightened original color //--- 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 //--- Create form objects CForm *form=NULL; for(int i=0;i<FORMS_TOTAL;i++) { //--- When creating an object, pass all the required parameters to it form=new CForm("Form_0"+string(i+1),30,(form==NULL ? 100 : form.BottomEdge()+20),100,30); if(form==NULL) continue; //--- Set activity and moveability flags for the form form.SetActive(true); form.SetMovable(true); //--- Set the form ID and the index in the list of objects form.SetID(i); form.SetNumber(0); // (0 - main form object) Auxiliary objects may be attached to the main one. The main object is able to manage them //--- Set the opacity of 200 form.SetOpacity(245); //--- 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(clrDarkBlue); //--- Draw the shadow drawing flag form.SetShadow(false); //--- 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(3,3,clr,200,4); //--- Fill the form background with a vertical gradient form.Erase(array_clr,form.Opacity(),true); //--- Draw an outlining rectangle at the edges of the form form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,form.ColorFrame(),form.Opacity()); form.Done(); //--- Display the text describing the gradient type and update the form //--- Text parameters: the text coordinates and the anchor point in the form center //--- Create a new text animation frame with the ID of 0 and display the text on the form form.TextOnBG(0,TextByLanguage("Тест 0","Test 0")+string(i+1),form.Width()/2,form.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',255,true,true); //--- Add the form to the list if(!engine.GraphAddCanvElmToCollection(form)) delete form; } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
In the OnChartEvent() handler, shorten the length of the graphical object name created by mouse clicks (the previous one was "TrendLineExt") to avoid exceeding 63 characters when creating a graphical resource:
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="TLineExt"; engine.CreateLineTrend(name_base,0,true,time,price,time2,price2); //--- Get the object from the list of graphical objects by chart name and ID 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); } } //--- Handling graphical element collection events
Compile the EA and launch it on the chart:
As we can see, the form does not enter the area where the one-click trading panel button is located, as well as does not enter the panel area if it has been activated. The forms for handling pivot points of an extended standard graphical object work as intended and do not leave the chart limits.
However, there are drawbacks as well. After creating a composite graphical object and moving its pivot points, they are located above the form objects. In some cases, this may turn out to be incorrect. For example, if we have a panel, the line dragged by the cursor should pass under the panel rather than be drawn above it. If we click each of the forms one by one, they are set above the composite graphical object and it is not drawn above these forms during a relocation. If we partially overlay three forms on top of each other, then when we hover over the second form, the first one becomes active. I will fix this. Here we need to use the "depth" of all forms' location relative to each other and to other objects on the chart.
What's next?
In the next article, I will continue my work on composite graphical objects and their functionality.
*Previous articles within the series:
Graphics in DoEasy library (Part 93): Preparing functionality for creating composite graphical objects
Graphics in DoEasy library (Part 94): Moving and deleting composite graphical objects
Graphics in DoEasy library (Part 95): Composite graphical object controls
Graphics in DoEasy library (Part 96): Graphics in form objects and handling mouse events
Graphics in DoEasy library (Part 97): Independent handling of form object movement
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/10521
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use