DoEasy. Controls (Part 2): Working on the CPanel class
Contents
Concept
In the last article, I started an extensive topic on creating controls in the Windows Forms style. But I still have not corrected some errors and shortcomings tracing back to the start of handling graphical objects. For example, I have never tested how graphical objects behave when switching chart timeframes. They are simply not displayed on the chart, while messages appear in the journal that such objects have already been created. Accordingly, they are not created and not rendered. There are also some issues. For instance, only form objects can interact with the mouse. The graphical element object, which is a parent one for the form, has no mouse-handling functionality. This solution is reasonable since this is the minimum library graphical object that can be used for any graphical constructions. But if we need interaction, a form object should act as the minimum graphical object. But its descendant is a Panel control, which I started developing in the previous article. It does not react to the mouse as well. Although it should. This is the cost of consistently adding objects and their functionality to the library.
In the current article, I will fix some shortcomings and errors, as well as continue adding the functionality to the Panel control object. In particular, I will implement the methods for setting the parameters of the font used by default for all panel text objects. For example, we have a certain CPanel class object. It will allow attaching other controls to it. If an attached control has a text, the default font parameters are inherited from the panel it is attached to. In turn, the panel (as a separate graphical element) also has the ability to draw any texts inside itself, like all other library graphical elements. These texts will also use the font parameters by default. Of course, we can draw a new text on a graphical element by setting other font parameters right before it is displayed. The text will be displayed with these explicitly specified parameters.
Besides, any panel, as a control, should have the ability to display the panel frames along with the ability to control the frame parameters. Let's implement the ability to draw a frame with the default values and with explicitly specified parameters or without them.
Improving library classes
I have created the \MQL5\Include\DoEasy\GraphINI.mqh file to be able to quickly set various display styles of graphical elements.
It contains parameters of various color schemes, as well as types and display styles of various graphical elements. Later, it will be possible to add custom parameters using the existing ones as examples.
For more visual display of form style parameters, let's slightly change the order of indices and corresponding style values.
Simply move the parameters located first in the list to the very bottom and set their ownership:
//+------------------------------------------------------------------+ //| List of form style parameter indices | //+------------------------------------------------------------------+ enum ENUM_FORM_STYLE_PARAMS { //--- CForm FORM_STYLE_FRAME_SHADOW_OPACITY, // Shadow opacity FORM_STYLE_FRAME_SHADOW_BLUR, // Shadow blur FORM_STYLE_DARKENING_COLOR_FOR_SHADOW, // Form shadow color darkening FORM_STYLE_FRAME_SHADOW_X_SHIFT, // Shadow X axis shift FORM_STYLE_FRAME_SHADOW_Y_SHIFT, // Shadow Y axis shift //--- CPanel FORM_STYLE_FRAME_WIDTH_LEFT, // Panel frame width to the left FORM_STYLE_FRAME_WIDTH_RIGHT, // Panel frame width to the right FORM_STYLE_FRAME_WIDTH_TOP, // Panel frame width on top FORM_STYLE_FRAME_WIDTH_BOTTOM, // Panel frame width below }; #define TOTAL_FORM_STYLE_PARAMS (9) // Number of form style parameters //+------------------------------------------------------------------+ //| Array containing form style parameters | //+------------------------------------------------------------------+ int array_form_style[TOTAL_FORM_STYLES][TOTAL_FORM_STYLE_PARAMS]= { //--- "Flat form" style parameters { //--- CForm 80, // Shadow opacity 4, // Shadow blur 80, // Form shadow color darkening 2, // Shadow X axis shift 2, // Shadow Y axis shift //--- CPanel 3, // Panel frame width to the left 3, // Panel frame width to the right 3, // Panel frame width on top 3, // Panel frame width below }, //--- "Embossed form" style parameters { //--- CForm 80, // Shadow opacity 4, // Shadow blur 80, // Form shadow color darkening 2, // Shadow X axis shift 2, // Shadow Y axis shift //--- CPanel 3, // Panel frame width to the left 3, // Panel frame width to the right 3, // Panel frame width on top 3, // Panel frame width below }, }; //+------------------------------------------------------------------+
This is by no means a critical improvement, but the correct structuring of the parameters will subsequently facilitate their readability.
In \MQL5\Include\DoEasy\Data.mqh, add the new message indices:
//--- CGraphElementsCollection MSG_GRAPH_ELM_COLLECTION_ERR_OBJ_ALREADY_EXISTS, // Error. A chart control object already exists with chart id MSG_GRAPH_ELM_COLLECTION_ERR_FAILED_CREATE_CTRL_OBJ,// Failed to create chart control object with chart ID MSG_GRAPH_ELM_COLLECTION_ERR_FAILED_GET_CTRL_OBJ, // Failed to get chart control object with chart ID MSG_GRAPH_ELM_COLLECTION_ERR_GR_OBJ_ALREADY_EXISTS,// Such graphical object already exists: MSG_GRAPH_ELM_COLLECTION_ERR_EMPTY_OBJECT, // Error! Empty object MSG_GRAPH_ELM_COLLECTION_ERR_FAILED_GET_ELEMENT, // Failed to get a graphical element from the list
and the message texts corresponding to the newly added indices:
//--- CGraphElementsCollection {"Ошибка. Уже существует объект управления чартами с идентификатором чарта ","Error. A chart control object already exists with chart id "}, {"Не удалось создать объект управления чартами с идентификатором чарта ","Failed to create chart control object with chart id "}, {"Не удалось получить объект управления чартами с идентификатором чарта ","Failed to get chart control object with chart id "}, {"Такой графический объект уже существует: ","Such a graphic object already exists: "}, {"Ошибка! Пустой объект","Error! Empty object"}, {"Не удалось получить графический элемент из списка","Failed to get graphic element from list"},
The flag set allows for the font style management. However, we should have an enumeration to be able to select the necessary font style and width types from the list. If we pass an enumeration as a font style or width type to a method, we are able to select a style and type from the drop-down list, which is much more convenient than remembering the flag names.
Besides, in most cases, the panel can be displayed with a frame around an object. Therefore, we need a macro substitution specifying the default panel frame width.
In \MQL5\Include\DoEasy\Defines.mqh, create the macro substitution specifying the default width of all panel frame sides:
//--- 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 CLR_FORE_COLOR (C'0x2D,0x43,0x48') // Default color for texts of objects on canvas #define DEF_FONT ("Calibri") // Default font #define DEF_FONT_SIZE (8) // Default font size #define OUTER_AREA_SIZE (16) // Size of one side of the outer area around the form workspace #define DEF_FRAME_WIDTH_SIZE (3) // Default form/panel/window frame width
as well as font style enumerations and width types at the very end of the file listing:
//+------------------------------------------------------------------+ //| Font style list | //+------------------------------------------------------------------+ enum ENUM_FONT_STYLE { FONT_STYLE_NORMAL=0, // Normal FONT_STYLE_ITALIC=FONT_ITALIC, // Italic FONT_STYLE_UNDERLINE=FONT_UNDERLINE, // Underline FONT_STYLE_STRIKEOUT=FONT_STRIKEOUT // Strikeout }; //+------------------------------------------------------------------+ //| FOnt width type list | //+------------------------------------------------------------------+ enum ENUM_FW_TYPE { FW_TYPE_DONTCARE=FW_DONTCARE, FW_TYPE_THIN=FW_THIN, FW_TYPE_EXTRALIGHT=FW_EXTRALIGHT, FW_TYPE_ULTRALIGHT=FW_ULTRALIGHT, FW_TYPE_LIGHT=FW_LIGHT, FW_TYPE_NORMAL=FW_NORMAL, FW_TYPE_REGULAR=FW_REGULAR, FW_TYPE_MEDIUM=FW_MEDIUM, FW_TYPE_SEMIBOLD=FW_SEMIBOLD, FW_TYPE_DEMIBOLD=FW_DEMIBOLD, FW_TYPE_BOLD=FW_BOLD, FW_TYPE_EXTRABOLD=FW_EXTRABOLD, FW_TYPE_ULTRABOLD=FW_ULTRABOLD, FW_TYPE_HEAVY=FW_HEAVY, FW_TYPE_BLACK=FW_BLACK }; //+------------------------------------------------------------------+
As you can see, I have simply set the appropriate flag value for each enumeration constant. But for the font style, I have introduced a new constant for the "normal" font - neither italic, nor underlined, and nor strikethrough. The value is equal to zero resetting previously enabled additional font style flags.
The form object class features the canvas-based graphical element animation class and form shadow object. Both objects are created using the 'new' operator. Upon completion, they are removed in the class destructor. Thus, these objects are always tracked and removed in time.
But the issue appeared when inheriting from the form object class. The panel object class is derived from the form object. As it turns out, the above mentioned objects are not removed causing a memory leak. You can launch the EA from the previous article to see for yourself. When removing it from the chart, the journal shows the message about the loss of 4 objects and the leakage of 512 bytes of memory:
4 undeleted objects left 1 object of type CAnimations left 3 objects of type CArrayObj left 512 bytes of leaked memory
I have been searching for the reason the objects are not deleted for a long time to no avail. Therefore, I will assign all these tasks to the terminal subsystem.
To do this, simply create the CArrayObj class object and add objects created in the CForm class to it. Upon completion, the terminal clears the memory of all objects located in the list.
Open \MQL5\Include\DoEasy\Objects\Graph\Form.mqh and declare such a list.
Also move the variables for storing the width of each form frame side to the protected class section:
//+------------------------------------------------------------------+ //| Form object class | //+------------------------------------------------------------------+ class CForm : public CGCnvElement { private: CArrayObj m_list_tmp; CArrayObj m_list_elements; // List of attached elements CAnimations *m_animations; // Pointer to the animation object CShadowObj *m_shadow_obj; // Pointer to the shadow object CMouseState m_mouse; // "Mouse status" class object ENUM_MOUSE_FORM_STATE m_mouse_form_state; // Mouse status relative to the form ushort m_mouse_state_flags; // Mouse status flags color m_color_frame; // Form frame color int m_offset_x; // Offset of the X coordinate relative to the cursor int m_offset_y; // Offset of the Y coordinate relative to the cursor //--- Reset the array size of (1) text, (2) rectangular and (3) geometric animation frames void ResetArrayFrameT(void); void ResetArrayFrameQ(void); void ResetArrayFrameG(void); //--- Return the name of the dependent object string CreateNameDependentObject(const string base_name) const { return ::StringSubstr(this.NameObj(),::StringLen(::MQLInfoString(MQL_PROGRAM_NAME))+1)+"_"+base_name; } //--- Create a new graphical object CGCnvElement *CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type, const int element_num, const string name, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool movable, const bool activity); //--- Create a shadow object void CreateShadowObj(const color colour,const uchar opacity); protected: int m_frame_width_left; // Form frame width to the left int m_frame_width_right; // Form frame width to the right int m_frame_width_top; // Form frame width at the top int m_frame_width_bottom; // Form frame width at the bottom //--- Initialize the variables void Initialize(void); void Deinitialize(void); public:
We are going to access the variables from the derived classes, so the variables should be stored in the protected section remaining visible in that class, as well as in the derived classes.
In the initialization method, clear the new list and set the sorted list flag for it. For the width of each of the form frame sides, set the new macro substitution value and add the animation object to the new list:
//+------------------------------------------------------------------+ //| Initialize the variables | //+------------------------------------------------------------------+ void CForm::Initialize(void) { this.m_list_elements.Clear(); this.m_list_elements.Sort(); this.m_list_tmp.Clear(); this.m_list_tmp.Sort(); this.m_shadow_obj=NULL; this.m_shadow=false; this.m_frame_width_right=DEF_FRAME_WIDTH_SIZE; this.m_frame_width_left=DEF_FRAME_WIDTH_SIZE; this.m_frame_width_top=DEF_FRAME_WIDTH_SIZE; this.m_frame_width_bottom=DEF_FRAME_WIDTH_SIZE; this.m_mouse_state_flags=0; this.m_offset_x=0; this.m_offset_y=0; CGCnvElement::SetInteraction(false); this.m_animations=new CAnimations(CGCnvElement::GetObject()); this.m_list_tmp.Add(m_animations); } //+------------------------------------------------------------------+
In the method creating a shadow object, add the object to the new list after it has been successfully created:
//+------------------------------------------------------------------+ //| Create the shadow object | //+------------------------------------------------------------------+ void CForm::CreateShadowObj(const color colour,const uchar opacity) { //--- ... //--- ... //--- Create a new shadow object and set the pointer to it in the variable this.m_shadow_obj=new CShadowObj(this.ChartID(),this.SubWindow(),this.CreateNameDependentObject("Shadow"),x,y,w,h); if(this.m_shadow_obj==NULL) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_FAILED_CREATE_SHADOW_OBJ)); return; } this.m_list_tmp.Add(m_shadow_obj); //--- ... //--- Move the form object to the foreground this.BringToTop(); } //+------------------------------------------------------------------+
This improvement saves us from uncontrolled and hard-to-find memory leaks.
I will continue my work on the class of the WinForms CPanel control object.
Let's implement handling the panel font parameters and its frame.
In the private section of the class in \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Panel.mqh, declare the variable for storing the font width type and the method returning flags set for the font:
//+------------------------------------------------------------------+ //| Panel object class of WForms controls | //+------------------------------------------------------------------+ class CPanel : public CForm { private: color m_fore_color; // Default text color for all panel objects ENUM_FW_TYPE m_bold_type; // Font width type ENUM_FRAME_STYLE m_border_style; // Panel frame style bool m_autoscroll; // Auto scrollbar flag int m_autoscroll_margin[2]; // Array of fields around the control during an auto scroll bool m_autosize; // Flag of the element auto resizing depending on the content ENUM_CANV_ELEMENT_AUTO_SIZE_MODE m_autosize_mode; // Mode of the element auto resizing depending on the content ENUM_CANV_ELEMENT_DOCK_MODE m_dock_mode; // Mode of binding element borders to the container int m_margin[4]; // Array of gaps of all sides between the fields of the current and adjacent controls int m_padding[4]; // Array of gaps of all sides inside controls //--- Return the font flags uint GetFontFlags(void); public:
When setting the font width type value to the CCanvas class, the value is specified in the m_bold_type variable.
The method returning the font flags returns all the parameters that are set for it: name, size, flags and angle. Since we only work with the flags here, we will call the method, which returns only the flags obtained from the CCanvas class font properties to avoid declaring local variables, containing font-related values, in each method.
In the public section of the class, declare the methods for handling the font style flags and font width types:
public: //--- (1) Set and (2) return the default text color of all panel objects void ForeColor(const color clr) { this.m_fore_color=clr; } color ForeColor(void) const { return this.m_fore_color; } //--- (1) Set and (2) return the Bold font flag void Bold(const bool flag); bool Bold(void); //--- (1) Set and (2) return the Italic font flag void Italic(const bool flag); bool Italic(void); //--- (1) Set and (2) return the Strikeout font flag void Strikeout(const bool flag); bool Strikeout(void); //--- (1) Set and (2) return the Underline font flag void Underline(const bool flag); bool Underline(void); //--- (1) Set and (2) return the font style void FontDrawStyle(ENUM_FONT_STYLE style); ENUM_FONT_STYLE FontDrawStyle(void); //--- (1) Set and (2) return the font width type void FontBoldType(ENUM_FW_TYPE type); ENUM_FW_TYPE FontBoldType(void) const { return this.m_bold_type; } //--- (1) Set and (2) return the frame style
...
and write the methods for setting and returning the panel frame properties:
//--- Return the gap (1) to the left, (2) at the top, (3) to the right and (4) at the bottom between the fields inside the control int PaddingLeft(void) const { return this.m_padding[0]; } int PaddingTop(void) const { return this.m_padding[1]; } int PaddingRight(void) const { return this.m_padding[2]; } int PaddingBottom(void) const { return this.m_padding[3]; } //--- Set the width of the form frame (1) to the left, (2) at the top, (3) to the right, (4) at the bottom and (5) on all sides of the control void FrameWidthLeft(const int value) { this.m_frame_width_left=value; } void FrameWidthTop(const int value) { this.m_frame_width_top=value; } void FrameWidthRight(const int value) { this.m_frame_width_right=value; } void FrameWidthBottom(const int value) { this.m_frame_width_bottom=value; } void FrameWidthAll(const int value) { this.FrameWidthLeft(value); this.FrameWidthTop(value); this.FrameWidthRight(value); this.FrameWidthBottom(value); } //--- Return the width of the form frame (1) to the left, (2) at the top, (3) to the right and (4) at the bottom int FrameWidthLeft(void) const { return this.m_frame_width_left; } int FrameWidthTop(void) const { return this.m_frame_width_top; } int FrameWidthRight(void) const { return this.m_frame_width_right; } int FrameWidthBottom(void) const { return this.m_frame_width_bottom; } //--- Constructors
In each constructor, add setting the default font width type:
CPanel(const string name) : CForm(::ChartID(),0,name,0,0,0,0) { CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_PANEL); this.m_type=OBJECT_DE_TYPE_GWF_PANEL; this.m_fore_color=CLR_FORE_COLOR; this.m_bold_type=FW_TYPE_NORMAL; this.MarginAll(3); this.PaddingAll(0); this.Initialize(); } //--- Destructor ~CPanel(); }; //+------------------------------------------------------------------+ //| Constructor indicating the chart and subwindow ID | //+------------------------------------------------------------------+ CPanel::CPanel(const long chart_id, const int subwindow, const string name, const int x, const int y, const int w, const int h) : CForm(chart_id,subwindow,name,x,y,w,h) { CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_PANEL); this.m_type=OBJECT_DE_TYPE_GWF_PANEL; this.m_fore_color=CLR_FORE_COLOR; this.m_bold_type=FW_TYPE_NORMAL; this.MarginAll(3); this.PaddingAll(0); this.Initialize(); } //+------------------------------------------------------------------+ //| Current chart constructor specifying the subwindow | //+------------------------------------------------------------------+ CPanel::CPanel(const int subwindow, const string name, const int x, const int y, const int w, const int h) : CForm(::ChartID(),subwindow,name,x,y,w,h) { CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_PANEL); this.m_type=OBJECT_DE_TYPE_GWF_PANEL; this.m_fore_color=CLR_FORE_COLOR; this.m_bold_type=FW_TYPE_NORMAL; this.MarginAll(3); this.PaddingAll(0); this.Initialize(); } //+------------------------------------------------------------------+ //| Constructor on the current chart in the main chart window | //+------------------------------------------------------------------+ CPanel::CPanel(const string name, const int x, const int y, const int w, const int h) : CForm(::ChartID(),0,name,x,y,w,h) { CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_PANEL); this.m_type=OBJECT_DE_TYPE_GWF_PANEL; this.m_fore_color=CLR_FORE_COLOR; this.m_bold_type=FW_TYPE_NORMAL; this.MarginAll(3); this.PaddingAll(0); this.Initialize(); } //+------------------------------------------------------------------+
The type is set for the panel fonts by default.
The private method returning the font flags:
//+------------------------------------------------------------------+ //| Return the font flags | //+------------------------------------------------------------------+ uint CPanel::GetFontFlags(void) { string name; int size; uint flags; uint angle; CGCnvElement::GetFont(name,size,flags,angle); return flags; } //+------------------------------------------------------------------+
Since the GetFont() method of the graphical element class should receive the variables by a link, while the passed variables are to contain the values obtained from the font parameters in the CCanvas class, here we declare all the necessary variables, get their values by calling the GetFont() method and return the obtained flags only.
The method setting the Bold font flag:
//+------------------------------------------------------------------+ //| Set the Bold font flag | //+------------------------------------------------------------------+ void CPanel::Bold(const bool flag) { uint flags=this.GetFontFlags(); if(flag) { this.m_bold_type=FW_TYPE_BOLD; CGCnvElement::SetFontFlags(flags | FW_BOLD); } else this.m_bold_type=FW_TYPE_NORMAL; } //+------------------------------------------------------------------+
Here we get the flags using the GetFontFlags() method considered above. If the flag passed in the method arguments is set, write the Bold value to the m_bold_type variable storing the font width type and set yet another flag FW_BOLD for the font flags.
If the flag passed in the method arguments is not set, the m_bold_type variable receives the default value.
The method returning the Bold font flag:
//+------------------------------------------------------------------+ //| Return the Bold font flag | //+------------------------------------------------------------------+ bool CPanel::Bold(void) { uint flags=this.GetFontFlags(); return(flags &FW_BOLD)==FW_BOLD; } //+------------------------------------------------------------------+
Here get the flags using the GetFontFlags() method and return the result of checking that the FW_BOLD flag is present in the variable.
The methods of setting and returning the remaining font flags are slightly different since they do not require setting to the flag value variable.
In all other respects, they are identical to the ones specified above:
//+------------------------------------------------------------------+ //| Set the Italic font flag | //+------------------------------------------------------------------+ void CPanel::Italic(const bool flag) { uint flags=this.GetFontFlags(); if(flag) CGCnvElement::SetFontFlags(flags | FONT_ITALIC); } //+------------------------------------------------------------------+ //| Return the Italic font flag | //+------------------------------------------------------------------+ bool CPanel::Italic(void) { uint flags=this.GetFontFlags(); return(flags &FONT_ITALIC)==FONT_ITALIC; } //+------------------------------------------------------------------+ //| Set the Strikeout font flag | //+------------------------------------------------------------------+ void CPanel::Strikeout(const bool flag) { uint flags=this.GetFontFlags(); if(flag) CGCnvElement::SetFontFlags(flags | FONT_STRIKEOUT); } //+------------------------------------------------------------------+ //| Return the Strikeout font flag | //+------------------------------------------------------------------+ bool CPanel::Strikeout(void) { uint flags=this.GetFontFlags(); return(flags &FONT_STRIKEOUT)==FONT_STRIKEOUT; } //+------------------------------------------------------------------+ //| Set the Underline font flag | //+------------------------------------------------------------------+ void CPanel::Underline(const bool flag) { uint flags=this.GetFontFlags(); if(flag) CGCnvElement::SetFontFlags(flags | FONT_UNDERLINE); } //+------------------------------------------------------------------+ //| Return the Underline font flag | //+------------------------------------------------------------------+ bool CPanel::Underline(void) { uint flags=this.GetFontFlags(); return(flags &FONT_UNDERLINE)==FONT_UNDERLINE; } //+------------------------------------------------------------------+
I believe, these methods are clear and do not need explanations.
The method setting the font style:
//+------------------------------------------------------------------+ //| Set the font style | //+------------------------------------------------------------------+ void CPanel::FontDrawStyle(ENUM_FONT_STYLE style) { switch(style) { case FONT_STYLE_ITALIC : this.Italic(true); break; case FONT_STYLE_UNDERLINE : this.Underline(true); break; case FONT_STYLE_STRIKEOUT : this.Strikeout(true); break; default: break; } } //+------------------------------------------------------------------+
Depending on the font style (italic, underlined, strikethrough) passed to the method, call the installation method corresponding to the style.
The method returning the font style:
//+------------------------------------------------------------------+ //| Return the font style | //+------------------------------------------------------------------+ ENUM_FONT_STYLE CPanel::FontDrawStyle(void) { return ( this.Italic() ? FONT_STYLE_ITALIC : this.Underline() ? FONT_STYLE_UNDERLINE : this.Strikeout() ? FONT_STYLE_UNDERLINE : FONT_STYLE_NORMAL ); } //+------------------------------------------------------------------+
Depending on the font style returned by the appropriate methods, the same font style is returned.
If none of the three styles is set, return Normal.
The method returning the font width type:
//+------------------------------------------------------------------+ //| Set the font width type | //+------------------------------------------------------------------+ void CPanel::FontBoldType(ENUM_FW_TYPE type) { this.m_bold_type=type; uint flags=this.GetFontFlags(); switch(type) { case FW_TYPE_DONTCARE : CGCnvElement::SetFontFlags(flags | FW_DONTCARE); break; case FW_TYPE_THIN : CGCnvElement::SetFontFlags(flags | FW_THIN); break; case FW_TYPE_EXTRALIGHT : CGCnvElement::SetFontFlags(flags | FW_EXTRALIGHT); break; case FW_TYPE_ULTRALIGHT : CGCnvElement::SetFontFlags(flags | FW_ULTRALIGHT); break; case FW_TYPE_LIGHT : CGCnvElement::SetFontFlags(flags | FW_LIGHT); break; case FW_TYPE_REGULAR : CGCnvElement::SetFontFlags(flags | FW_REGULAR); break; case FW_TYPE_MEDIUM : CGCnvElement::SetFontFlags(flags | FW_MEDIUM); break; case FW_TYPE_SEMIBOLD : CGCnvElement::SetFontFlags(flags | FW_SEMIBOLD); break; case FW_TYPE_DEMIBOLD : CGCnvElement::SetFontFlags(flags | FW_DEMIBOLD); break; case FW_TYPE_BOLD : CGCnvElement::SetFontFlags(flags | FW_BOLD); break; case FW_TYPE_EXTRABOLD : CGCnvElement::SetFontFlags(flags | FW_EXTRABOLD); break; case FW_TYPE_ULTRABOLD : CGCnvElement::SetFontFlags(flags | FW_ULTRABOLD); break; case FW_TYPE_HEAVY : CGCnvElement::SetFontFlags(flags | FW_HEAVY); break; case FW_TYPE_BLACK : CGCnvElement::SetFontFlags(flags | FW_BLACK); break; default : CGCnvElement::SetFontFlags(flags | FW_NORMAL); break; } } //+------------------------------------------------------------------+
Here, in the m_bold_type variable, set the value passed to the method. Get the font flags using the GetFontFlags() method.
Depending on the font width flag passed to the method, add yet another flag, corresponding to the specified type, to the obtained variable with the font flags.
As a result, m_bold_type features the enumeration value passed to the method, while the font flags features the flag with the font width type corresponding to the ENUM_FW_TYPE enumeration value.
When adding a graphical element object to the collection list, we first check its presence in the list. If such an object is already present, we either do not add it, inform of that in the journal and return the adding error, or do the same returning the pointer to the existing object instead of the error. This may be useful in case of the dynamic object creation in order to return the dynamically created but hidden object from the method and display it on the chart when attempting to create exactly the same object.
Let's create the enumeration to return various return codes from the method for adding a graphical element to the list.
In the file of the graphical element collection class \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh, write the following enumeration before declaring the class:
//+------------------------------------------------------------------+ //| Collection of graphical objects | //+------------------------------------------------------------------+ #resource "\\"+PATH_TO_EVENT_CTRL_IND; // Indicator for controlling graphical object events packed into the program resources enum ENUM_ADD_OBJ_RET_CODE // Enumerate the codes of returning the method for adding an object to the list { ADD_OBJ_RET_CODE_SUCCESS, // Successful ADD_OBJ_RET_CODE_EXIST, // Object exists in the collection list ADD_OBJ_RET_CODE_ERROR, // Failed to add to the collection list }; class CGraphElementsCollection : public CBaseObj
There are three return codes here:
- object successfully added to the list,
- object already exists in the collection list,
- failed to add the object to the collection list.
Now we can flexibly return the required result. The only possible error is a failure to add the object to the list. Other results indicate the possibility to continue the work — either the object has been created and added to the collection or such an object is already present and we can handle it.
In order to create graphical elements directly from the collection class, we need to add a prefix to their name — the program name with an underscore at the end. In the private section of the class, declare the variable for storing the graphical object prefix:
class CGraphElementsCollection : public CBaseObj { private: //--- ... bool m_is_graph_obj_event; // Event flag in the list of graphical objects int m_total_objects; // Number of graphical objects int m_delta_graph_obj; // Difference in the number of graphical objects compared to the previous check string m_name_prefix; // Object name prefix //--- Return the flag indicating the graphical element class object presence in the collection list of graphical elements
Also, add two private methods: the method either creating a new graphical element, or returning the ID of the existing one,
and the method returning the index of the specified graphical element in the collection list:
//--- Reset all interaction flags for all forms except the specified one void ResetAllInteractionExeptOne(CGCnvElement *form); //--- Add the element to the collection list bool AddCanvElmToCollection(CGCnvElement *element); //--- Add the element to the collectionl ist or return the existing one ENUM_ADD_OBJ_RET_CODE AddOrGetCanvElmToCollection(CGCnvElement *element,int &id); //--- Return the graphical elemnt index in the collection list int GetIndexGraphElement(const long chart_id,const string name);
The method returning the graphical element list by chart ID and object name needs a fix. The issue is that the names of all library graphical objects start with the program name. The name is set in the prefix of graphical object names. If we simply pass the object name to the search method, no such object will be found. This happens because the search is made for the name passed to the method, while graphical objects also have a prefix. Therefore, we need to check the presence of a name prefix in the search name and, if there is no prefix, then add it to the name of the object being searched for. In this case, the search will always work correctly — if there is already a prefix in the searched name, then nothing is added to the name and the value passed to the method is searched for. If the prefix is absent, it is added to the name, and the search is performed.
In the public section of the class, find the method returning the list of graphical elements by chart ID and object name and add to it the improvements described above:
//--- Return the list of graphical elements by chart ID and object name CArrayObj *GetListCanvElementByName(const long chart_id,const string name) { string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name; CArrayObj *list=CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),CANV_ELEMENT_PROP_CHART_ID,chart_id,EQUAL); return CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL); }
Here we declare a name to search, check if the name contains a prefix and, if not, add the prefix to the name. Otherwise, do not add anything.
Next, get the list of graphical elements by chart ID and return the list sorted by the searched object name. If the object is not found, the method returns NULL.
Let's write yet another method returning the graphical element by chart and name ID:
//--- Return the graphical element by chart ID and name CGCnvElement *GetCanvElement(const long chart_id,const string name) { CArrayObj *list=this.GetListCanvElementByName(chart_id,name); return(list!=NULL ? list.At(0) : NULL); } //--- Constructor
Here, get the list of graphical elements by chart ID and object name. If the list is obtained, return the pointer to the only object located in it. Otherwise, return NULL.
We need to clear the graphical element collection list to recreate all GUI elements when switching timeframes. Since the class destructor, which features clearing all lists, is called only when removing the program from the chart, we need to implement clearing in the OnDeinit() handler. Let's declare it:
//--- Update the list of (1) all graphical objects, (2) on the specified chart, fill in the data on the number of new ones and set the event flag void Refresh(void); void Refresh(const long chart_id); //--- Event handlers void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam); void OnDeinit(void); private: //--- Move all objects on canvas to the foreground void BringToTopAllCanvElm(void);
Replace this code block in each method creating a graphical element:
if(!this.AddCanvElmToCollection(obj)) { delete obj; return WRONG_VALUE; }
with the following one:
//--- Create a graphical element object on canvas on a specified chart and subwindow int CreateElement(const long chart_id, const int subwindow, const string name, const int x, const int y, const int w, const int h, const color clr, const uchar opacity, const bool movable, const bool activity, const bool redraw=false) { int id=this.m_list_all_canv_elm_obj.Total(); CGCnvElement *obj=new CGCnvElement(GRAPH_ELEMENT_TYPE_ELEMENT,id,0,chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,activity,redraw); ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id); if(res==ADD_OBJ_RET_CODE_ERROR) return WRONG_VALUE; if(res==ADD_OBJ_RET_CODE_EXIST) obj.SetID(id); obj.Erase(clr,opacity,redraw); return obj.ID(); } //--- Create a graphical element object on canvas on a specified chart and subwindow with the vertical gradient filling
Here we first get the return code from the method creating a new graphical element or returning the ID of the existing one. Next, if the method returns an error, return -1. If the return code indicates that the object exists, set the ID received from the AddOrGetCanvElmToCollection() method to the newly created object properties. The point is that we initially set the maximum value for a new object ID — the number of objects in the list, while the ID for the already existing object should be different. In this case, the ID is set to the variable passed by link to the method adding the object to the list or returning the existing object.
Thus, we either add a new object to the list, or get the pointer to the existing one and set its ID in it.
For the form object and panel creation methods, the code block will be slightly different:
//--- Create a graphical object form object on canvas on a specified chart and subwindow int CreateForm(const long chart_id, const int subwindow, const string name, const int x, const int y, const int w, const int h, const color clr, const uchar opacity, const bool movable, const bool activity, const bool shadow=false, const bool redraw=false) { int id=this.m_list_all_canv_elm_obj.Total(); CForm *obj=new CForm(chart_id,subwindow,name,x,y,w,h); ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id); if(res==ADD_OBJ_RET_CODE_ERROR) return WRONG_VALUE; obj.SetID(id); obj.SetActive(activity); obj.SetMovable(movable); obj.SetColorBackground(clr); obj.SetColorFrame(clr); obj.SetOpacity(opacity,false); obj.SetShadow(shadow); obj.DrawRectangle(0,0,obj.Width()-1,obj.Height()-1,obj.ColorFrame(),obj.Opacity()); obj.Done(); obj.Erase(clr,opacity,redraw); return obj.ID(); } //--- Create a graphical object form object on canvas on a specified chart and subwindow with the vertical gradient filling
This case is simpler. When receiving an error, return -1. The ID should not be restored since it is assigned in the class constructor for a new object rather than specified in its creation method.
In the method of panel object creation, add the panel frame width and type:
//--- Create graphical object WinForms Panel object on canvas on a specified chart and subwindow int CreatePanel(const long chart_id, const int subwindow, const string name, const int x, const int y, const int w, const int h, const color clr, const uchar opacity, const bool movable, const bool activity, const int frame_width=-1, ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL, const bool shadow=false, const bool redraw=false) { int id=this.m_list_all_canv_elm_obj.Total(); CPanel *obj=new CPanel(chart_id,subwindow,name,x,y,w,h); ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id); if(res==ADD_OBJ_RET_CODE_ERROR) return WRONG_VALUE; obj.SetID(id); obj.SetActive(activity); obj.SetMovable(movable); obj.SetColorBackground(clr); obj.SetColorFrame(clr); obj.SetOpacity(opacity,false); obj.SetShadow(shadow); obj.DrawRectangle(0,0,obj.Width()-1,obj.Height()-1,obj.ColorFrame(),obj.Opacity()); obj.Erase(clr,opacity,redraw); if(frame_width>0) obj.FrameWidthAll(frame_width); obj.SetActiveAreaShift(obj.FrameWidthLeft(),obj.FrameWidthBottom(),obj.FrameWidthRight(),obj.FrameWidthTop()); obj.DrawFormFrame(obj.FrameWidthTop(),obj.FrameWidthBottom(),obj.FrameWidthLeft(),obj.FrameWidthRight(),obj.ColorFrame(),obj.Opacity(),frame_style); obj.Done(); return obj.ID(); }
In the method body, fill the panel with color. If the frame width exceeds zero, set the width of all frame sides to the panel properties, set the active panel area inside the frame, draw the frame and save the panel appearance.
In the class constructor, set the value for the graphical object name prefix, clear the list of all graphical elements and set the sorted list flag for it:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CGraphElementsCollection::CGraphElementsCollection() { this.m_type=COLLECTION_GRAPH_OBJ_ID; this.m_name_prefix=this.m_name_program+"_"; ::ChartSetInteger(::ChartID(),CHART_EVENT_MOUSE_MOVE,true); ::ChartSetInteger(::ChartID(),CHART_EVENT_MOUSE_WHEEL,true); this.m_list_all_graph_obj.Type(COLLECTION_GRAPH_OBJ_ID); this.m_list_all_graph_obj.Sort(SORT_BY_CANV_ELEMENT_ID); this.m_list_all_graph_obj.Clear(); this.m_list_charts_control.Sort(); this.m_list_charts_control.Clear(); this.m_total_objects=0; this.m_is_graph_obj_event=false; this.m_list_deleted_obj.Clear(); this.m_list_deleted_obj.Sort(); this.m_list_all_canv_elm_obj.Clear(); this.m_list_all_canv_elm_obj.Sort(); } //+------------------------------------------------------------------+
The method adding the graphical element on canvas to the collection:
//+------------------------------------------------------------------+ //| Add the graphical element on canvas to the collection | //+------------------------------------------------------------------+ bool CGraphElementsCollection::AddCanvElmToCollection(CGCnvElement *element) { if(!this.m_list_all_canv_elm_obj.Add(element)) { CMessage::ToLog(DFUN+element.Name()+": ",MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST); return false; } return true; } //+------------------------------------------------------------------+
If failed to add the element to the list, inform of that in the journal and return false. Otherwise, return true.
The method adding the element to the collection list or returning the existing one:
//+------------------------------------------------------------------+ //| Add the element to the collectionl ist or return the existing one| //+------------------------------------------------------------------+ ENUM_ADD_OBJ_RET_CODE CGraphElementsCollection::AddOrGetCanvElmToCollection(CGCnvElement *element,int &id) { //--- If an invalid pointer is passed, notify of that and return the error code if(element==NULL) { CMessage::ToLog(DFUN,MSG_GRAPH_ELM_COLLECTION_ERR_EMPTY_OBJECT); return ADD_OBJ_RET_CODE_ERROR; } //--- If the graphical element with a specified chart ID and name is already present, if(this.IsPresentCanvElmInList(element.ChartID(),element.Name())) { //--- inform of the object existence, CMessage::ToLog(DFUN+element.Name()+": ",MSG_LIB_SYS_OBJ_ALREADY_IN_LIST); //--- get the element from the collection list. element=this.GetCanvElement(element.ChartID(),element.Name()); //--- If failed to get the object, inform of that and return the error code if(element==NULL) { CMessage::ToLog(DFUN,MSG_GRAPH_ELM_COLLECTION_ERR_FAILED_GET_ELEMENT); return ADD_OBJ_RET_CODE_ERROR; } //--- set the ID of the object obtained from the list to the value returned by the link //--- and return the object existence code in the list id=element.ID(); return ADD_OBJ_RET_CODE_EXIST; } //--- If failed to add the object to the list, remove it and return the error code if(!this.AddCanvElmToCollection(element)) { delete element; return ADD_OBJ_RET_CODE_ERROR; } //--- All is successful return ADD_OBJ_RET_CODE_SUCCESS; } //+------------------------------------------------------------------+
Each method string is thoroughly described in the comments, so I hope, all is clear here. In brief, the method receives the pointer to a newly created object. If such an object is present in the list, assign the pointer (to the existing list object) to the pointer passed to the method. The existing object ID is set to the variable passed in the method via the link. From the outside, the variable is assigned as the object ID. If there is no such object in the list, add it and return the operation success flag.
The method returning the graphical element index in the collection list:
//+------------------------------------------------------------------+ //| Return the graphical elemnt index in the collection list | //+------------------------------------------------------------------+ int CGraphElementsCollection::GetIndexGraphElement(const long chart_id,const string name) { for(int i=0;i<this.m_list_all_canv_elm_obj.Total();i++) { CGCnvElement *obj=this.m_list_all_canv_elm_obj.At(i); if(obj==NULL) continue; if(obj.ChartID()==chart_id && obj.Name()==name) return i; } return WRONG_VALUE; } //+------------------------------------------------------------------+
Here we get the next object in the loop by all graphical elements. Return the loop index if the chart ID and name are equal to the ones passed to the method. Upon the loop completion, return -1.
Here I should clarify why I do not apply fast search using the CSelect library class. The fact is that I am looking for the object index in the complete list of the entire collection, whereas sorting the list by properties creates new lists and we get the object index from the sorted list rather than from the collection list. Naturally, their indices in most cases will not match.
For the same reason, I will fix the error in the method that finds an object present in the collection but absent on the chart. The method also returns the pointer to the object and the object index in the list:
//+------------------------------------------------------------------+ //|Find an object present in the collection but not on a chart | //| Return the pointer to the object and its index in the list. | //+------------------------------------------------------------------+ CGStdGraphObj *CGraphElementsCollection::FindMissingObj(const long chart_id,int &index) { index=WRONG_VALUE; for(int i=0;i<this.m_list_all_graph_obj.Total();i++) { CGStdGraphObj *obj=this.m_list_all_graph_obj.At(i); if(obj==NULL) continue; if(!this.IsPresentGraphObjOnChart(obj.ChartID(),obj.Name())) { index=i; return obj; } } return NULL; } //+------------------------------------------------------------------+
Previously, we first received the object list by chart ID and looped over the resulting list. This was incorrect.
Now I am implementing the loop over the entire collection list and get the correct object index in the list, accordingly.
In the EA from the previous article, only form objects could interact with the mouse.
Graphical element objects should not have auto interaction with the mouse but all objects derived from the form object should inherit handling mouse events from it. The error was in the fact that I strictly checked the graphical element type in the event handler. The events were not handled if this was not a form.
Now we will change this check. If the graphical element type is a form or any further element along the inheritance hierarchy, then such objects should be processed in the event handler.
Let's fix this.
In the GetFormUnderCursor() method, add the following change:
//--- 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 or its descendants 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 or its descendants 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
The equality comparison ("==") was here previously. Now it is "greater than or equal". Since all values of graphical element type enumeration constants are in ascending order, all subsequent types will have a constant value greater than the value of the GRAPH_ELEMENT_TYPE_FORM constant.
Also, let's implement the change in the method resetting interaction flags for all forms except the specified one:
//+--------------------------------------------------------------------+ //| Reset all interaction flags for all forms except the specified one | //+--------------------------------------------------------------------+ void CGraphElementsCollection::ResetAllInteractionExeptOne(CGCnvElement *form_exept) { //--- 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 pointer to the object CGCnvElement *obj=this.m_list_all_canv_elm_obj.At(i); //--- if failed to receive the pointer, or it is not a form or its descendants, or it is not a form whose pointer has been passed to the method, move on if(obj==NULL || obj.TypeGraphElement()<GRAPH_ELEMENT_TYPE_FORM || (obj.Name()==form_exept.Name() && obj.ChartID()==form_exept.ChartID())) continue; //--- Reset the interaction flag for the current form in the loop obj.SetInteraction(false); } } //+------------------------------------------------------------------+
Previously, there was a comparison for inequality ("!=") here and all objects except for form objects were skipped. Now, all objects located below the form object in the inheritance hierarchy will be skipped.
In the SetZOrderMAX() method, slightly change the test text displayed on the graphical object (since the texts will also be changed in the test EA) and fix the error preventing objects from interacting with the mouse:
//+------------------------------------------------------------------+ //| Set ZOrde to the specified element | //| and adjust it in other elements | //+------------------------------------------------------------------+ bool CGraphElementsCollection::SetZOrderMAX(CGCnvElement *obj) { //--- ... //--- Temporarily declare a form object for drawing a text for visually displaying its ZOrder CForm *form=obj; //--- and draw a text specifying ZOrder on the form form.TextOnBG(0,form.TypeElementDescription()+": ID "+(string)form.ID()+", ZD "+(string)form.Zorder(),form.Width()/2,form.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',255,true,true); //--- Sort the list of graphical elements by an element ID this.m_list_all_canv_elm_obj.Sort(SORT_BY_CANV_ELEMENT_ID); //--- ... //--- ... //--- In the loop by the obtained list of remaining graphical element objects for(int i=0;i<list.Total();i++) { //--- get the next object CGCnvElement *elm=list.At(i); //--- If failed to get the object or if this is a control form for managing pivot points of an extended standard graphical object //--- or, if the object's ZOrder is zero, skip the object since there is no need in changing its ZOrder as it is the bottom one if(elm==NULL || elm.Type()==OBJECT_DE_TYPE_GFORM_CONTROL || elm.Zorder()==0) continue; //--- If failed to set the object's ZOrder to 1 less than it already is (decreasing ZOrder by 1), add 'false' to the 'res' value if(!elm.SetZorder(elm.Zorder()-1,false)) res &=false; //--- Temporarily (for the test purpose), if the element is a form, if(elm.Type()>=OBJECT_DE_TYPE_GFORM) { //--- assign the pointer to the element for the form and draw a text specifying ZOrder on the form form=elm; form.TextOnBG(0,form.TypeElementDescription()+": ID "+(string)form.ID()+", ZD "+(string)form.Zorder(),form.Width()/2,form.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',255,true,true); } } //--- Upon the loop completion, return the result set in 'res' return res; } //+------------------------------------------------------------------+
Write the deinitialization event handler:
//+------------------------------------------------------------------+ //| Deinitialization event handler | //+------------------------------------------------------------------+ void CGraphElementsCollection::OnDeinit(void) { this.m_list_all_canv_elm_obj.Clear(); } //+------------------------------------------------------------------+
Here all is simple. Clear the collection list of graphical elements.
Accordingly, the method should be called from the CEngine library main object in \MQL5\Include\DoEasy\Engine.mqh.
Open the class file and let its OnDeinit() handler get calling the method from the graphical element collection class:
//+------------------------------------------------------------------+ //| Deinitialize library | //+------------------------------------------------------------------+ void CEngine::OnDeinit(void) { this.m_indicators.GetList().Clear(); this.m_graph_objects.OnDeinit(); } //+------------------------------------------------------------------+
These are currently all the library improvements. Let's test the results.
Test
To perform the test, let's use the EA from the previous article and save it to \MQL5\Experts\TestDoEasy\Part102\ as TestDoEasyPart102.mq5.
I will not make any significant changes. Instead, I will only change the graphical object location coordinates, so that the distance between them is slightly shorter, and increase the panel size. To create the panel size, we will use the following code in the OnInit() handler:
//--- Create WinForms Panel object CPanel *pnl=NULL; obj_id=engine.GetGraphicObjCollection().CreatePanel(ChartID(),0,"WFPanel",elm.RightEdge()+20,50,230,150,array_clr[0],200,true,true); list=engine.GetListCanvElementByID(ChartID(),obj_id); pnl=list.At(0); if(pnl!=NULL) { pnl.FontDrawStyle(FONT_STYLE_NORMAL); pnl.Bold(true); Print(DFUN,EnumToString(pnl.FontDrawStyle())); Print(DFUN,EnumToString(pnl.FontBoldType())); pnl.SetFontSize(10); pnl.TextOnBG(0,pnl.TypeElementDescription()+": ID "+(string)pnl.ID()+", ZD "+(string)pnl.Zorder(),pnl.Width()/2,pnl.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',pnl.Opacity()); pnl.Update(true); } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
If the panel has been created successfully, set the font type for its texts to Normal together with the "bold" font and display the font style description and width type to the journal.
All texts displayed on all graphical element objects now automatically display the object type, ID and ZOrder, albeit in a slightly different format compared to the last article.
You can view all the changes in the attached files.
Compile the EA and launch it on the chart:
As we can see, all the necessary objects interact with the mouse, the panel now has the frame, while the font on it is displayed in bold as intended. No objects now disappear when switching charts, but they also do not save their new location. To fix this, we need to write object data to file and read it if necessary. I will do this as soon as I have all objects with their planned inheritance hierarchy.
What's next?
In the next article, I will continue the development of the panel object class.
*Previous articles within the series:
DoEasy. Controls (Part 1): First steps
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/10697
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use