Designing and implementing new GUI widgets based on CChartObject class
Introduction
After I wrote a previous article on semi-automatic Expert Advisor with GUI interface it turned out that it would be desirable to enhance interface with some new functionalities for more complex indicators and Expert Advisors. After getting acquainted with MQL5 Standard Library classes I implemented new widgets.
I this article I will describe a process of using MQL5 Standard Library classes for GUI objects and how to implement new classes derived from CChartObjectEdit class: CChartObjectProgressBar, CChartObjectSpinner and CChartEditTable. CChartEditTable class uses a dynamic two-dimensional array of objects, this is a working example on how to implement a dynamic 2D array of objects in MQL5.
1. CChartObject and its descendants
If we do not use standard MQL5 library class we must use Object Functions to create and maintain any object on the chart.
Objects are created with ObjectCreate() function, and the type of the object is passed to the ObjectCreate() function as an ENUM_OBJECT value. All objects on the chart have their own properties, that can be of Integer, Double, or String type. All the properties are set and retrieved through dedicated functions: ObjectGetInteger(), ObjectSetInteger(), ObjectGetDouble(), ObjectSetDouble(), ObjectGetString(), ObjectSetString(). There are also function for deleting, moving and counting objects on any given chart.
Due to OOP paradigm in MQL5, handling of various chart objects can be done by using CChartObject class and its descendands.
CChartObject class is a base class for any of the graphical objects that can be put on a chart. Please observe the basic inheritance diagram for CChartObject below:
Figure 1. Inheritance diagram for CChartObject class
As we can see there are a few classes that are marked with a small triangle in the lower right corner.
These are classes that are parents for other classes. Basically, descendant classes enhance possibilities of a base class by adding new variables and methods that operate on the object. They may also differ in Create() and Type() methods to create a derived object and return its type.
Let me show this by example: CChartObjectTrend class is a parent to CChartObjectTrendByAngle, CChartObjectChannel, CChartObjectStdDevChannel, CChartObjectRegression and CChartObjectPitchfork classes.
The CChartObjectTrend is a base class for objects that have properties OBJPROP_RAY_RIGHT and OBJPROP_RAY_LEFT and is defined as follows:
class CChartObjectTrend : public CChartObject { public: //--- methods of access to properties of the object bool RayLeft() const; bool RayLeft(bool new_sel); bool RayRight() const; bool RayRight(bool new_sel); //--- method of creating the object bool Create(long chart_id,string name,int window, datetime time1,double price1,datetime time2,double price2); //--- method of identifying the object virtual int Type() const { return(OBJ_TREND); } //--- methods for working with files virtual bool Save(int file_handle); virtual bool Load(int file_handle); };
There are comments in the definition that allow to distinguish different types of methods.
Methods of access to properties of the object are RayLeft() and RayRight(). Their implementation is to call ObjectGetInteger() and ObjectSetInteger() methods that operate on CChartObjectTrend object.
bool CChartObjectTrend::RayLeft(bool new_ray) { //--- checking if(m_chart_id==-1) return(false); //--- return(ObjectSetInteger(m_chart_id,m_name,OBJPROP_RAY_LEFT,new_ray)); }
Create() method is responsible for creating and attaching object on the chart.
It calls ObjectCreate() method with OBJ_TREND as one of the parameters:
bool CChartObjectTrend::Create(long chart_id,string name,int window, datetime time1,double price1,datetime time2,double price2) { bool result=ObjectCreate(chart_id,name,OBJ_TREND,window,time1,price1,time2,price2); if(result) result&=Attach(chart_id,name,window,2); //--- return(result); }
Save() and Load() methods store and load object data on a hard drive by using FileWriteInteger() and FileLoadInteger() functions:
bool CChartObjectTrend::Save(int file_handle) { bool result; //--- checking if(file_handle<=0) return(false); if(m_chart_id==-1) return(false); //--- writing result=CChartObject::Save(file_handle); if(result) { //--- writing value of the "Ray left" property if(FileWriteInteger(file_handle,(int) ObjectGetInteger(m_chart_id,m_name, OBJPROP_RAY_LEFT),CHAR_VALUE)!=sizeof(char)) return(false); //--- writing value of the "Ray right" property if(FileWriteInteger(file_handle,(int) ObjectGetInteger(m_chart_id,m_name, OBJPROP_RAY_RIGHT),CHAR_VALUE)!=sizeof(char)) return(false); } //--- return(result); } bool CChartObjectTrend::Load(int file_handle) { bool result; //--- checking if(file_handle<=0) return(false); if(m_chart_id==-1) return(false); //--- reading result=CChartObject::Load(file_handle); if(result) { //--- reading value of the "Ray left" property if(!ObjectSetInteger(m_chart_id,m_name,OBJPROP_RAY_LEFT, FileReadInteger(file_handle,CHAR_VALUE)))return(false); //--- reading value of the "Ray right" property if(!ObjectSetInteger(m_chart_id,m_name,OBJPROP_RAY_RIGHT, FileReadInteger(file_handle,CHAR_VALUE))) return(false); } //--- return(result); }
Let's go quickly through definitions of descendand classes from CChartObjectTrend.
CChartObjectTrendByAngle class adds Angle() property modifier, and returns OBJ_TRENDBYANGLE object type:
class CChartObjectTrendByAngle : public CChartObjectTrend { public: //--- methods of access to properties of the object double Angle() const; bool Angle(double angle); //--- method of creating the object bool Create(long chart_id,string name,int window,datetime time1,double price1, datetime time2,double price2); //--- method of identifying the object virtual int Type() { return(OBJ_TRENDBYANGLE); } };
CChartObjectChannel class returns OBJ_CHANNEL object type and since it handles channels, three pairs of price/date parameters are passed to Create() method:
class CChartObjectChannel : public CChartObjectTrend { public: //--- method of creating the object bool Create(long chart_id,string name,int window,datetime time1,double price1, datetime time2,double price2,datetime time3,double price3); //--- method of identifying the object virtual int Type() const { return(OBJ_CHANNEL); } };
CChartObjectStdDevChannel class adds Deviations() property modifier and additional deviation parameter in Create() method:
class CChartObjectStdDevChannel : public CChartObjectTrend { public: //--- methods of access to properties of the object double Deviations() const; bool Deviations(double deviation); //--- method of creating the object bool Create(long chart_id,string name,int window, datetime time1,datetime time2,double deviation); //--- method of identifying the object virtual int Type() const { return(OBJ_STDDEVCHANNEL); } //--- methods for working with files virtual bool Save(int file_handle); virtual bool Load(int file_handle); };
CChartObjectRegression class creates regression trend line, only Create() and Type() methods are overridden from the ones in CChartObjectTrend class:
class CChartObjectRegression : public CChartObjectTrend { public: //--- method of creating the object bool Create(long chart_id,string name,int window,datetime time1,datetime time2); //--- method of identifying the object virtual int Type() const { return(OBJ_REGRESSION); } };
CChartObjectPitchfork class handles pitchfork type, also only Create() and Type() methods are changed:
class CChartObjectPitchfork : public CChartObjectTrend { public: //--- method of creating the object bool Create(long chart_id,string name,int window,datetime time1,double price1, datetime time2,double price2,datetime time3,double price3); //--- method of identifying the object virtual int Type() const { return(OBJ_CHANNEL); } };
This quick scan showed the basic rules that are applied when writing new graphic object class based on some other class:
- changing Create() method for object creation
- changing Type() method for returning object type
- adding property access modifiers
Not all rules must be applied, one may only add new access modifiers or add new variables and/or objects in the class.
Before we go further, let me explain how to use CChartObject methods on graphic objects.
Instead of using ObjectSet and ObjectGet methods family and use object properties it is sufficient to declare CChartObject or a descendant object and invoke methods that change its desired properties. In order to make it easier I am providing an example of an ordinary label.
Instead of writing :
void OnStart() { //--- string label_name="my_OBJ_LABEL_object"; if(ObjectFind(0,label_name)<0) { Print("Object ",label_name," not found. Error code = ",GetLastError()); ObjectCreate(0,label_name,OBJ_LABEL,0,0,0); ObjectSetInteger(0,label_name,OBJPROP_XDISTANCE,200); ObjectSetInteger(0,label_name,OBJPROP_YDISTANCE,300); ObjectSetInteger(0,label_name,OBJPROP_COLOR,White); ObjectSetString(0,label_name,OBJPROP_TEXT,UP); ObjectSetString(0,label_name,OBJPROP_FONT,"Wingdings"); ObjectSetInteger(0,label_name,OBJPROP_FONTSIZE,10); ObjectSetDouble(0,label_name,OBJPROP_ANGLE,-45); ObjectSetInteger(0,label_name,OBJPROP_SELECTABLE,false); ChartRedraw(0); } }
We can implement it using OOP paradigm:
1. Declare CChartObjectLabel object:
CChartObjectLabel label;
2. Operate on the object:
int OnInit() { //--- label.Create(0, label_name, 0, 0); label.X_Distance(200); label.Y_Distance(300); label.Color(White); label.Description(UP); label.Font("Wingdings"); label.FontSize(10); label.Angle(-45); label.Selectable(false); //--- return(0); }
As you can see the main difference is that we are no longer operating on a string label_ name:
string label_name="my_OBJ_LABEL_object";
and call ObjectSetInteger(), ObjectGetInteger(), ObjectSetDouble(), ObjectGetDouble() functions with label_name as one of the parameters, but we declare CChartObjectLabel object and use its methods. This is not only simplier to remember, and logical to implement but also faster to write.
MQL5 code editor provides us with code completion functionality when putting dot (.) after object instance. There is no need to traverse MQL5 documentation back and forth to see what OBJPROP property to put in order to set or get a given property.
Analogically for CChartObjectTrend class that was described previously, to get or set a ray left or right it is enough to declare CChartObjectTrend object and call RayRight() or RayLeft() method:
CChartObjectTrend trendline; trendline.RayRight(true);
2. ProgressBar
First widget that we will implement is ProgressBar. Progress bars show progress of some operation, from 0 to x percent.
To make it more robust, let's do not constrain the maximum value to 100, but to any positive integer value. We need a color strip that will change its size according to the progress value. The first thing that comes to mind is to use two rectangles, but I took another path: use two CChartObjectEdit objects, one inside another, with different background colors.
It simplifies coding and add text that can be put inside the progress bar to show its value. It would be nice if our Progress bar could be horizontal or vertical depending on one's needs.
2.1. ProgressBar implementation
CChartObjectProgress class is derived from CChartObjectEdit class.
I added private internal variables to hold value and constraints on the value: m_value, m_min, m_max.
Direction of the progress bar is set as integer value and is held by m_direction variable. Color is held by m_color variable. Type() method returns OBJ_EDIT value, as there is no value recognized for our purpose anyway. One may notice CChartObjectEdit m_bar variable inside class definition - this is the inside bar that changes its size depending on the m_value. Additional variables m_name and m_chart hold internally values for m_bar variable.
class CChartObjectProgressBar : public CChartObjectEdit { private: int m_value; int m_min; int m_max; int m_direction; color m_color; CChartObjectEdit m_bar; string m_name; long m_chart_id; public: int GetValue(); int GetMin(); int GetMax(); void SetValue(int val); void SetMin(int val); void SetMax(int val); void SetColor(color bgcol,color fgcol); bool Create(long chart_id,string name,int window,int X,int Y, int sizeX,int sizeY,int direction); //--- method of identifying the object virtual int Type() const { return(OBJ_EDIT); } };
Create() method creates ProgressBar object and attaches it to the chart.
You may notice the Y variable gets substracted from sizeY variable in case vertical bar is drawn, this is because normally CChartObjectEdit is drawn from top to bottom, and I wanted to draw the inside rectangle from the bottom upwards:
bool CChartObjectProgressBar::Create(long chart_id,string name,int window,int X,int Y, int sizeX,int sizeY,int direction=0) { bool result=ObjectCreate(chart_id,name,(ENUM_OBJECT)Type(),window,0,0,0); m_name=name; m_chart_id=chart_id; m_direction=direction; if(direction!=0) { Y=Y-sizeY; } ObjectSetInteger(chart_id,name,OBJPROP_BGCOLOR,White); ObjectSetInteger(chart_id,name,OBJPROP_COLOR,White); ObjectSetInteger(chart_id,name,OBJPROP_SELECTABLE,false); ObjectSetInteger(chart_id,name,OBJPROP_READONLY,true); result&=m_bar.Create(chart_id,name+"m_bar",window,X,Y,sizeX,sizeY); m_bar.Color(White); m_bar.ReadOnly(true); m_bar.Selectable(false); //--- if(result) result&=Attach(chart_id,name,window,1); result&=X_Distance(X); result&=Y_Distance(Y); result&=X_Size(sizeX); result&=Y_Size(sizeY); //--- return(result); }
SetColor() method sets background and foreground colors to both rectangles:
void CChartObjectProgressBar::SetColor(color bgCol,color fgCol=White) { m_color=bgCol; m_bar.BackColor(m_color); m_bar.Color(fgCol); }
SetValue() method is responsible both for setting m_val value and recalculating the inner rectangle object size.
Size is calculated differently for horizontal and vertical bars:
void CChartObjectProgressBar::SetValue(int val) { if(m_direction==0) // horizontal ProgressBar { double sizex=(double)ObjectGetInteger(m_chart_id,m_name,OBJPROP_XSIZE,0); double stepSize=sizex/(m_max-m_min); m_value=val; m_bar.Create(m_bar.ChartId(),m_bar.Name(),m_bar.Window(), m_bar.X_Distance(),m_bar.Y_Distance(),(int)MathFloor(stepSize*m_value),m_bar.Y_Size()); } else { double sizey=(double)ObjectGetInteger(m_chart_id,m_name,OBJPROP_YSIZE,0); double stepSize=sizey/(m_max-m_min); m_value=val; m_bar.Create(m_bar.ChartId(),m_bar.Name(),m_bar.Window(), m_bar.X_Distance(),(int)(this.Y_Distance()+sizey-MathFloor(stepSize*m_value)), m_bar.X_Size(),(int)MathFloor(stepSize*m_value)); } m_bar.Description(IntegerToString(m_value)); }
2.2. ProgressBar Demo
Since we have already implemented the CChartObjectProgressBar class it is a time to see it in action.
In order to place a new progress bar on the chart it is enough to declare CChartObjectProgressBar object and use Create() and appropriate property methods:
progressBar.Create(0, "progressBar1", 0, 10, 10, 200, 40); progressBar.SetColor(YellowGreen); progressBar.SetMin(0); progressBar.SetMax(100); progressBar.SetValue(0);
I wrote a demo Expert Advisor that puts six different progress bars on the chart and changes their value after any object is clicked on the screen.
Full source code for this and other demos is in the attachments, please observe the presentation below:
3. Spinner
Spinner widget is a widget that contains a field and two buttons. It used to increment or decrement a value in the edit field by clicking one of the buttons.
While designing the object I did not want to operate only on integer values, therefore Spinner was designed operate on double type. Spinner also has a possibility to define step size, that is the value to increment or decrement the current value. It should also have minimum and maximum value that cannot be crossed.
3.1. Spinner implementation
In MQL5 we have CChartObjectEdit and CChartObjectButton classes, which can be combined into one, CChartObjectSpinner class. CChartObjectSpinner inherits from CChartObjectEdit and contains two private member CChartObjectButton objects.
There are constraints for minimum and maximum m_value stored in m_min and m_max member variables and m_precision variable stores calculation precision to nth digit value. The methods needed are for access of the value, setting increment and descrement step size and setting value.
class CChartObjectSpinner: public CChartObjectEdit { private: double m_value; double m_stepSize; double m_min; double m_max; int m_precision; string m_name; long m_chart_id; CChartObjectButton m_up,m_down; public: double GetValue(); double GetMin(); double GetMax(); void SetValue(double val); void SetMin(double val); void SetMax(double val); double Inc(); double Dec(); bool Create(long chart_id,string name,int window,int X,int Y, int sizeX,int sizeY,double val,double stepSize,int precision); //--- method of identifying the object virtual int Type() const { return(OBJ_EDIT); } };
Create() method creates new CChartObjectSpinner and attaches it to the chart.
There are two CChartObjectButtons created on the right side of CChartObjectEdit, each having height of half of the CChartObjectEdit height.
Increment button has '+' sign and decrement button '-' sign.
bool CChartObjectSpinner::Create(long chart_id,string name,int window,int X,int Y, int sizeX,int sizeY,double val=0.0,double stepSize=1.0,int precision=8) { bool result=ObjectCreate(chart_id,name,(ENUM_OBJECT)Type(),window,0,0,0); m_name=name; m_chart_id=chart_id; m_value=val; m_stepSize=stepSize; m_precision=precision; ObjectSetInteger(chart_id,name,OBJPROP_BGCOLOR,White); ObjectSetInteger(chart_id,name,OBJPROP_COLOR,Black); ObjectSetInteger(chart_id,name,OBJPROP_SELECTABLE,false); ObjectSetInteger(chart_id,name,OBJPROP_READONLY,true); result&=m_up.Create(chart_id, name+"_up", window, X+sizeX, Y, 15, sizeY/2); result&=m_down.Create(chart_id, name+"_down", window, X+sizeX, Y+sizeY/2, 15, sizeY/2); m_up.Description("+"); m_down.Description("-"); ObjectSetString(chart_id,name,OBJPROP_TEXT,0,(DoubleToString(m_value,precision))); //--- if(result) result&=Attach(chart_id,name,window,1); result&=X_Distance(X); result&=Y_Distance(Y); result&=X_Size(sizeX); result&=Y_Size(sizeY); //--- return(result); }
SetValue() method sets private variable m_value to double value on condition that it is within <m_min, m_max> range.
void CChartObjectSpinner::SetValue(double val) { if(val>=m_min && val<=m_max) m_value=val; this.Description(DoubleToString(m_value)); }
Inc() method increments value by given step size, but no more than m_max value.
Please notice I had to use NormalizeDouble() function to compare double values to given precision.
double CChartObjectSpinner::Inc(void) { if(NormalizeDouble(m_max-m_value-m_stepSize,m_precision)>0.0) m_value+=m_stepSize; else m_value=m_max; this.Description(DoubleToString(m_value, m_precision)); m_up.State(false); return m_value; }
Dec() method decrements value by given step size, but no less than m_min value.
double CChartObjectSpinner::Dec(void) { if(NormalizeDouble(m_value-m_stepSize-m_min,m_precision)>0.0) m_value-=m_stepSize; else m_value=m_min; this.Description(DoubleToString(m_value,m_precision)); m_down.State(false); return m_value; }
3.2. Spinner Demo
It is time to test Spinner objects. In order to use them it is enough to declare CChartObjectSpinner object and use Create(), SetMin() and SetMax() methods.
spinner.Create(0, "spinner1", 0, 10, 10, 200, 40, 0.0, 0.4); spinner.SetMin(0); spinner.SetMax(100);
I prepared a demo that uses three Spinner widgets and adds all the values after any Spinner button is clicked.
This is done inside OnChartEvent() function;
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Check the event by pressing a mouse button if(id==CHARTEVENT_OBJECT_CLICK) { if (sparam=="spinner1_up") spinner.Inc(); if (sparam=="spinner1_down") spinner.Dec(); if (sparam=="spinner2_up") spinner2.Inc(); if (sparam=="spinner2_down") spinner2.Dec(); if (sparam=="spinner3_up") spinner3.Inc(); if (sparam=="spinner3_down") spinner3.Dec(); label.Description(DoubleToString(NormalizeDouble(spinner.GetValue()+spinner2.GetValue()+spinner3.GetValue(),10),10)); ChartRedraw(); } }
Please see the attached demo:
4. CChartObjectEditTable
In many multi time frame (MTF) Expert Advisors there are indicator values displayed for each time frame separately.
Sometimes each time frame has different indicator settings displayed in a form of 2D table of rectangles or squares of different colors. I designed a universal 2D table of such objects by creating CChartObjectEditTable class. This class can hold arbitrary amount of rows and columns, since I am using 2D dynamic array of objects.
During the design I decided to define a color for each cell separately and also add possibility to put different text strings on any cell. Cells have equal size, but I wanted to define their height and width and space between cells.
CChartObjectEditTable class holds CArrayObj pointer to a two dimensional array of objects, and m_rows and m_columns member variables hold number of rows and columns in the table.
There is m_baseName member variable that holds the prefix of all cell CChartObjectEdit objects within the table. GetColor(), SetColor(), GetText(), SetText() methods are for setting and getting color and text values to any desired cell. Delete() method deletes all objects created by Create() method.
class CChartObjectEditTable { private: CArrayObj *array2D; int m_rows; int m_cols; string m_baseName; public: bool Create(long chart_id,string name,int window,int rows,int cols,int startX,int startY, int sizeX,int sizeY,color Bg,int deltaX,int deltaY); bool Delete(); bool SetColor(int row,int col,color newColor); color GetColor(int row,int col); bool SetText(int row,int col,string newText); string GetText(int row,int col); };
Create() method creates two dimensional dynamic table of CChartObjectEdit objects.
Please observe how to create a 2D array of objects in MQL5: first we declare a pointer to 2D array and then we fill the array with a number of CArrayObj() objects, that is we create arrays within the array. All arrays can be seen as holders for columns of the table.
Each column contains rows, that hold CChartObjectEdit objects, each object being a single cell to display.
bool CChartObjectEditTable::Create(long chart_id,string name,int window,int rows=1,int cols=1, int startX=0,int startY=0,int sizeX=15,int sizeY=15, color Bg=White,int deltaX=5,int deltaY=5) { m_rows=rows; m_cols=cols; m_baseName=name; int i=0,j=0; array2D=new CArrayObj(); if (array2D==NULL) return false; for(j=0; j<m_cols; j++) { CArrayObj *new_array=new CArrayObj(); if (array2D==NULL) return false; array2D.Add(new_array); for(i=0; i<m_rows; i++) { CChartObjectEdit *new_edit=new CChartObjectEdit(); new_edit.Create(chart_id, name+IntegerToString(i)+":"+IntegerToString(j), window, startX+j*(sizeX+deltaX), startY+i*(sizeY+deltaY), sizeX, sizeY); new_edit.BackColor(Bg); new_edit.Color(White); new_edit.Selectable(false); new_edit.ReadOnly(true); new_edit.Description(""); new_array.Add(new_edit); } } return true; }
SetColor() method sets the color of any single cell. At first it finds column array and then nth element in column array.
Then the color value of the element is changed by invoking BackColor() method.
bool CChartObjectEditTable::SetColor(int row,int col,color newColor) { CArrayObj *sub_array; CChartObjectEdit *element; if((row>=0 && row<m_rows) && (col>=0 && col<m_cols)) { if(array2D!=NULL) { sub_array=array2D.At(col); element=(CChartObjectEdit*)sub_array.At(row); element.BackColor(newColor); return true; } } return false; }
GetColor() method has the same algorithm to find the cell as SetColor() method but it returns color value of any given cell.
color CChartObjectEditTable::GetColor(int row,int col) { CArrayObj *sub_array; CChartObjectEdit *element; if((row>=0 && row<m_rows) && (col>=0 && col<m_cols)) { if(array2D!=NULL) { sub_array=array2D.At(col); element=(CChartObjectEdit*)sub_array.At(row); return element.BackColor(); } } return NULL; }
SetText() method finds the element and sets its text value by invoking Description() method.
bool CChartObjectEditTable::SetText(int row,int col,string newText) { CArrayObj *sub_array; CChartObjectEdit *element; if((row>=0 && row<m_rows) && (col>=0 && col<m_cols)) { if(array2D!=NULL) { sub_array=array2D.At(col); element=(CChartObjectEdit*)sub_array.At(row); element.Description(newText); return true; } } return false; }
Delete() method deletes all objects created by Create() method.
At first it clears all column arrays and then it deletes array2D object from memory.
bool CChartObjectEditTable::Delete(void) { for(int j=0; j<m_cols; j++) { CArrayObj *column_array=array2D.At(j); column_array.Clear(); delete column_array; } delete array2D; return true; }
4.2. CChartObjectEditTable Demo
In order to use CChartObjectEditTable widget it is necessary to declare CChartEditTable object and use the Create() method with parameters indicating how many rows and columns the table should contain.
Then by using property modifiers one can simply change color and text on any cell.
table.Create(0,"t",0,1,10,10,10,15,15,Yellow); table.SetColor(2,2,Red); table.SetText(2,2,"2");
Please see the script I prepared that demonstrates the possibilities of using CChartObjectEditTable object.
The source code of the script is in the attachment.
Conclusion
I described and introduced in the article a process of creating new chart widgets derived on CChartObject class.
The process of using implemented widgets is very straightforward and takes only a few lines of code.
For using the widgets please include ChartObjectsExtControls.mqh file in Expert Advisor or indicator code.
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use