Русский Español Deutsch 日本語 Português
preview
The View and Controller components for tables in the MQL5 MVC paradigm: Simple controls

The View and Controller components for tables in the MQL5 MVC paradigm: Simple controls

MetaTrader 5Examples |
2 073 0
Artyom Trishkin
Artyom Trishkin

Contents


Introduction

As part of the development of the Table View control in MVC (Model-View-Controller) paradigm, we created a table model (the Model component) and started creating the View component. At the first stage, a basic object was created, which is the progenitor of all other graphical elements.

Today we will start developing simple controls that will later serve as building blocks for composite UI elements. Each control element will possess functionality for interaction with the user and with other elements. In other words, this essentially corresponds to the functionality of the Controller component.

Since in the MQL language the event model is integrated into objects created using chart events, event handling will be organized in all subsequent controls to implement the connection between the View component and the Controller component. To do this, refine the base class of graphical elements.

Next, create simple controls — a text label and various buttons. Each element will support drawing icons. This will make it possible to create completely different controls from simple buttons. If you look at the string of the tree view, where an icon is on the left and text is on the right, then this seems to be a separate control. But we can easily create such a control by using a regular button as a base. At the same time, it will be possible to adjust the string parameters so that it either reacts by changing the color when the mouse cursor is focused on and clicked, or it is static, but reacts to clicks.

All this can be implemented with just a few configuration lines after creating the object. And from such elements, we will continue to create complex composite controls that are fully interactive and ready to use.


Controller Component. Refining Base Classes

So, in order to implement our plans, we must slightly refine the already implemented classes, macro substitutions,  and enumerations. Most of the necessary functionality will be located in the base object of graphical elements. Therefore, it is it that will be mainly refined.

Previously, this class was at MQL5\Scripts\Tables\Controls\Base.mqh.
Today we will write a test indicator, so we want to create a new folder \Tables\Controls\ in the indicator directory \MQ5\Indicators\ and locate the Base.mqh file in it. That's what we'll be working on today.

Further, objects will contain lists of attached controls. Containers can be such objects, for example. In order for these lists to handle files correctly i.e. create objects stored in lists, it is necessary to declare all classes of elements being created in advance. Write a class declaration, new macro substitutions, enumerations, and enumeration constants into Base.mqh file:

//+------------------------------------------------------------------+
//|                                                         Base.mqh |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd." 
#property link      "https://www.mql5.com"

//+------------------------------------------------------------------+
//| Include libraries                                                |
//+------------------------------------------------------------------+
#include <Canvas\Canvas.mqh>              // CCanvas class
#include <Arrays\List.mqh>                // CList class

//--- Forward declaration of control element classes
class    CImagePainter;                   // Image drawing class
class    CLabel;                          // Text label class
class    CButton;                         // Simple button class
class    CButtonTriggered;                // Two-position button class
class    CButtonArrowUp;                  // Up arrow button class
class    CButtonArrowDown;                // Down arrow button class
class    CButtonArrowLeft;                // Left arrow button class
class    CButtonArrowRight;               // Right arrow button class
class    CCheckBox;                       // CheckBox control class
class    CRadioButton;                    // RadioButton control class

//+------------------------------------------------------------------+
//| Macro substitutions                                              |
//+------------------------------------------------------------------+
#define  clrNULL              0x00FFFFFF  // Transparent color for CCanvas
#define  MARKER_START_DATA    -1          // Data start marker in a file
#define  DEF_FONTNAME         "Calibri"   // Default font
#define  DEF_FONTSIZE         10          // Default font size

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ENUM_ELEMENT_TYPE                    // Enumeration of graphical element types
  {
   ELEMENT_TYPE_BASE = 0x10000,           // Basic object of graphical elements
   ELEMENT_TYPE_COLOR,                    // Color object
   ELEMENT_TYPE_COLORS_ELEMENT,           // Color object of the graphical object element
   ELEMENT_TYPE_RECTANGLE_AREA,           // Rectangular area of the element
   ELEMENT_TYPE_IMAGE_PAINTER,            // Object for drawing images
   ELEMENT_TYPE_CANVAS_BASE,              // Basic canvas object for graphical elements
   ELEMENT_TYPE_LABEL,                    // Text label
   ELEMENT_TYPE_BUTTON,                   // Simple button
   ELEMENT_TYPE_BUTTON_TRIGGERED,         // Two-position button
   ELEMENT_TYPE_BUTTON_ARROW_UP,          // Up arrow button
   ELEMENT_TYPE_BUTTON_ARROW_DOWN,        // Down arrow button
   ELEMENT_TYPE_BUTTON_ARROW_LEFT,        // Left arrow button
   ELEMENT_TYPE_BUTTON_ARROW_RIGHT,       // Right arrow button
   ELEMENT_TYPE_CHECKBOX,                 // CheckBox control
   ELEMENT_TYPE_RADIOBUTTON,              // RadioButton control
  };
  
enum ENUM_ELEMENT_STATE                   // Control state
  {
   ELEMENT_STATE_DEF,                     // By default (e.g. button released etc.)
   ELEMENT_STATE_ACT,                     // Activated (e.g. button pressed, etc.)
  };

enum ENUM_COLOR_STATE                     // Enumeration of element state colors
  {
   COLOR_STATE_DEFAULT,                   // Normal state color
   COLOR_STATE_FOCUSED,                   // Color when hovering over an element
   COLOR_STATE_PRESSED,                   // Color when clicking on an element
   COLOR_STATE_BLOCKED,                   // Blocked element color
  };
//+------------------------------------------------------------------+ 
//| Functions                                                        |
//+------------------------------------------------------------------+

When creating methods for saving and loading objects to/from files, each method has constant repeating strings without changing from method to method:

//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      return false;
//--- Save data start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileWriteLong(file_handle,-1)!=sizeof(long))
      return false;
//--- Save the object type
   if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE)
      return false;

//--- Save the ID
   if(::FileWriteInteger(file_handle,this.m_id,INT_VALUE)!=INT_VALUE)
      return false;
//--- Save the name
   if(::FileWriteArray(file_handle,this.m_name)!=sizeof(this.m_name))
      return false;

And there are the same methods Description and Print. So, it is reasonable to transfer these strings to load/save methods in the base object. Then they won't have to be written in every new load/save method in every new class where manipulations with files are provided.

Declare these methods in the base object:

public:
//--- Set (1) name and (2) ID
   void              SetName(const string name)                { ::StringToShortArray(name,this.m_name);    }
   void              SetID(const int id)                       { this.m_id=id;                              }
//--- Return (1) name and (2) ID
   string            Name(void)                          const { return ::ShortArrayToString(this.m_name);  }
   int               ID(void)                            const { return this.m_id;                          }

//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_BASE); }
   
//--- (1) Return and (2) display the object description in the journal
   virtual string    Description(void);
   virtual void      Print(void);
   
//--- Constructor/destructor
                     CBaseObj (void) : m_id(-1) { this.SetName(""); }
                    ~CBaseObj (void) {}
  };

And write their implementation:

//+------------------------------------------------------------------+
//| CBaseObj::Return the object description                          |
//+------------------------------------------------------------------+
string CBaseObj::Description(void)
  {
   string nm=this.Name();
   string name=(nm!="" ? ::StringFormat(" \"%s\"",nm) : nm);
   return ::StringFormat("%s%s ID %d",ElementDescription((ENUM_ELEMENT_TYPE)this.Type()),name,this.ID());
  }
//+------------------------------------------------------------------+
//| CBaseObj::Display the object description in the journal          |
//+------------------------------------------------------------------+
void CBaseObj::Print(void)
  {
   ::Print(this.Description());
  }
//+------------------------------------------------------------------+
//| CBaseObj::Save to file                                           |
//+------------------------------------------------------------------+
bool CBaseObj::Save(const int file_handle)
  {
//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      return false;
//--- Save data start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileWriteLong(file_handle,-1)!=sizeof(long))
      return false;
//--- Save the object type
   if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE)
      return false;

//--- Save the ID
   if(::FileWriteInteger(file_handle,this.m_id,INT_VALUE)!=INT_VALUE)
      return false;
//--- Save the name
   if(::FileWriteArray(file_handle,this.m_name)!=sizeof(this.m_name))
      return false;
   
//--- All is successful
   return true;
  }
//+------------------------------------------------------------------+
//| CBaseObj::Load from file                                         |
//+------------------------------------------------------------------+
bool CBaseObj::Load(const int file_handle)
  {
//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      return false;
//--- Load and check the data start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileReadLong(file_handle)!=-1)
      return false;
//--- Load the object type
   if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type())
      return false;

//--- Load the ID
   this.m_id=::FileReadInteger(file_handle,INT_VALUE);
//--- Load the name
   if(::FileReadArray(file_handle,this.m_name)!=sizeof(this.m_name))
      return false;
   
//--- All is successful
   return true;
  }

Now, for each new class, the Description and Print methods can be declared and implemented only if their logic differs from the logic prescribed in this class.

And in the methods of working with files in derived classes, instead of repeatedly writing the same code lines in each method of each class, we will simply address to the methods of working with files of this base object.

From all subsequent classes of this file (Base.mqh), remove all Print methods — they are already in the base object, and they completely repeat it.

In all methods of working with files, delete such strings:

//+------------------------------------------------------------------+
//| CColor::Save to file                                             |
//+------------------------------------------------------------------+
bool CColor::Save(const int file_handle)
  {
//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      return false;
//--- Save data start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileWriteLong(file_handle,-1)!=sizeof(long))
      return false;
//--- Save the object type
   if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE)
      return false;

//--- Save the color
   if(::FileWriteInteger(file_handle,this.m_color,INT_VALUE)!=INT_VALUE)
      return false;
//--- Save the ID
   if(::FileWriteInteger(file_handle,this.m_id,INT_VALUE)!=INT_VALUE)
      return false;
//--- Save the name
   if(::FileWriteArray(file_handle,this.m_name)!=sizeof(this.m_name))
      return false;
   
//--- All is successful
   return true;
  }

Now, instead of these strings, we just have a call of the base class method:

//+------------------------------------------------------------------+
//| CColor::Save to file                                             |
//+------------------------------------------------------------------+
bool CColor::Save(const int file_handle)
  {
//--- Save the parent object data
   if(!CBaseObj::Save(file_handle))
      return false;

//--- Save the color
   if(::FileWriteInteger(file_handle,this.m_color,INT_VALUE)!=INT_VALUE)
      return false;
   
//--- All is successful
   return true;
  }

Such changes have already been made in all methods of working with files in this file. We will take these changes into account when writing subsequent classes.

In the CColorElement class, replace identical duplicate strings in class constructors.

//+-----------------------------------------------------------------------------+
//| CColorControl::Constructor with setting the transparent colors of the object|
//+-----------------------------------------------------------------------------+
CColorElement::CColorElement(void)
  {
   this.InitColors(clrNULL,clrNULL,clrNULL,clrNULL);
   this.m_default.SetName("Default"); this.m_default.SetID(1);
   this.m_focused.SetName("Focused"); this.m_focused.SetID(2);
   this.m_pressed.SetName("Pressed"); this.m_pressed.SetID(3);
   this.m_blocked.SetName("Blocked"); this.m_blocked.SetID(4);
   this.SetCurrentAs(COLOR_STATE_DEFAULT);
   this.m_current.SetName("Current");
   this.m_current.SetID(0);
  }

using one method Init():

public:
//--- Return a new color
   color             NewColor(color base_color, int shift_red, int shift_green, int shift_blue);

//--- Class initialization
   void              Init(void);

//--- Initialize colors for different states

...

Its implementation:

//+------------------------------------------------------------------+
//| CColorControl::Class initialization                              |
//+------------------------------------------------------------------+
void CColorElement::Init(void)
  {
   this.m_default.SetName("Default"); this.m_default.SetID(1);
   this.m_focused.SetName("Focused"); this.m_focused.SetID(2);
   this.m_pressed.SetName("Pressed"); this.m_pressed.SetID(3);
   this.m_blocked.SetName("Blocked"); this.m_blocked.SetID(4);
   this.SetCurrentAs(COLOR_STATE_DEFAULT);
   this.m_current.SetName("Current");
   this.m_current.SetID(0);
  }
//+------------------------------------------------------------------+

If a transparent color is passed to the color initialization method, then it does not need to be changed in any way for any states.

Mind it in method implementation:

//+----------------------------------------------------------------------+
//| CColorControl::Set the colors for all states based on the current one|
//+----------------------------------------------------------------------+
void CColorElement::InitColors(const color clr)
  {
   this.InitDefault(clr);
   this.InitFocused(clr!=clrNULL ? this.NewColor(clr,-20,-20,-20) : clrNULL);
   this.InitPressed(clr!=clrNULL ? this.NewColor(clr,-40,-40,-40) : clrNULL);
   this.InitBlocked(clrWhiteSmoke);   
  }

In the CBound class, add a method that returns a flag for cursor presence inside a rectangular area. This is required when implementing the Controller component:

//+------------------------------------------------------------------+
//| Rectangular region class                                         |
//+------------------------------------------------------------------+
class CBound : public CBaseObj
  {
protected:
   CRect             m_bound;                                  // Rectangular area structure

public:
//--- Change the bounding rectangular (1) width, (2) height and (3) size
   void              ResizeW(const int size)                   { this.m_bound.Width(size);                                    }
   void              ResizeH(const int size)                   { this.m_bound.Height(size);                                   }
   void              Resize(const int w,const int h)           { this.m_bound.Width(w); this.m_bound.Height(h);               }
   
//--- Set (1) X, (2) Y and (3) both coordinates of the bounding rectangle
   void              SetX(const int x)                         { this.m_bound.left=x;                                         }
   void              SetY(const int y)                         { this.m_bound.top=y;                                          }
   void              SetXY(const int x,const int y)            { this.m_bound.LeftTop(x,y);                                   }
   
//--- (1) Set and (2) shift the bounding rectangle by the specified coordinates/offset size
   void              Move(const int x,const int y)             { this.m_bound.Move(x,y);                                      }
   void              Shift(const int dx,const int dy)          { this.m_bound.Shift(dx,dy);                                   }
   
//--- Returns the object coordinates, dimensions, and boundaries
   int               X(void)                             const { return this.m_bound.left;                                    }
   int               Y(void)                             const { return this.m_bound.top;                                     }
   int               Width(void)                         const { return this.m_bound.Width();                                 }
   int               Height(void)                        const { return this.m_bound.Height();                                }
   int               Right(void)                         const { return this.m_bound.right-(this.m_bound.Width()  >0 ? 1 : 0);}
   int               Bottom(void)                        const { return this.m_bound.bottom-(this.m_bound.Height()>0 ? 1 : 0);}

//--- Returns the flag indicating whether the cursor is inside the area
   bool              Contains(const int x,const int y)   const { return this.m_bound.Contains(x,y);                           }
   
//--- Return the object description
   virtual string    Description(void);

   
//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_RECTANGLE_AREA);                         }
   
//--- Constructors/destructor
                     CBound(void) { ::ZeroMemory(this.m_bound); }
                     CBound(const int x,const int y,const int w,const int h) { this.SetXY(x,y); this.Resize(w,h);             }
                    ~CBound(void) { ::ZeroMemory(this.m_bound); }
  };

Now it is necessary to add everything for the implementation of Controller component to the base class of the CCanvasBase graphical element canvas.

When graphic elements interact with the mouse, it is necessary to disable some properties of the chart, such as scrolling the chart with the wheel, the right mouse button menu, etc. Each object of the graphical elements will do this. But when you first start, you must remember the state of chart properties as it was before the program was launched. And after completing the work, return everything to its place.

To do this, in the private section of the CCanvasBase class declare variables for storing values of stored chart properties and declare a method for setting restrictions on chart properties:

//+------------------------------------------------------------------+
//| Base class of graphical elements canvas                          |
//+------------------------------------------------------------------+
class CCanvasBase : public CBaseObj
  {
private:
   bool              m_chart_mouse_wheel_flag;                 // Flag for sending mouse wheel scroll messages
   bool              m_chart_mouse_move_flag;                  // Flag for sending mouse cursor movement messages
   bool              m_chart_object_create_flag;               // Flag for sending messages about the graphical object creation event
   bool              m_chart_mouse_scroll_flag;                // Flag for scrolling the chart with the left button and mouse wheel



   
//--- Set chart restrictions (wheel scrolling, context menu, and crosshair)
   void              SetFlags(const bool flag);
   
protected:

UI elements can have two states (maybe more, but for now — two). For example, for a button — pressed, released. This means that we must control the color states of the element in its two states. In the protected section of the class, define a variable to store the state of the element, another set of color management objects, and separate transparency control for the background and foreground canvas:

protected:
   CCanvas           m_background;                             // Background canvas
   CCanvas           m_foreground;                             // Foreground canvas
   CBound            m_bound;                                  // Object boundaries
   CCanvasBase      *m_container;                              // Parent container object
   CColorElement     m_color_background;                       // Background color control object
   CColorElement     m_color_foreground;                       // Foreground color control object
   CColorElement     m_color_border;                           // Border color control object
   
   CColorElement     m_color_background_act;                   // Activated element background color control object
   CColorElement     m_color_foreground_act;                   // Activated element foreground color control object
   CColorElement     m_color_border_act;                       // Activated element frame color control object
   
   ENUM_ELEMENT_STATE m_state;                                 // Control state (e.g. buttons (on/off))
   long              m_chart_id;                               // Chart ID
   int               m_wnd;                                    // Chart subwindow index
   int               m_wnd_y;                                  // Cursor Y coordinate offset in the subwindow
   int               m_obj_x;                                  // Graphical object X coordinate
   int               m_obj_y;                                  // Graphical object Y coordinate
   uchar             m_alpha_bg;                               // Background transparency
   uchar             m_alpha_fg;                               // Foreground transparency
   uint              m_border_width;                           // Frame width
   string            m_program_name;                           // Program name
   bool              m_hidden;                                 // Hidden object flag
   bool              m_blocked;                                // Blocked element flag
   bool              m_focused;                                // Element flag in focus

Here, also declare methods for controlling the mouse cursor, color management, and virtual event handlers:

//--- Limit the graphical object by the container dimensions
   virtual void      ObjectTrim(void);
//--- Returns the flag indicating whether the cursor is inside the object
   bool              Contains(const int x,const int y);

   
//--- Check if the set color is equal to the specified one
   bool              CheckColor(const ENUM_COLOR_STATE state) const;
//--- Change the background, text, and border colors depending on the condition
   void              ColorChange(const ENUM_COLOR_STATE state);
   
//--- Initialize (1) the class object and (2) default object colors
   void              Init(void);
   virtual void      InitColors(void);

//--- (1) Cursor hovering (Focus), (2) button clicks (Press), (3) wheel scrolling (Wheel),
//--- (4) release (Release), (5) graphical object creation (Create) event handlers. Should be identified in the descendants
   virtual void      OnFocusEvent(const int id, const long lparam, const double dparam, const string sparam);
   virtual void      OnPressEvent(const int id, const long lparam, const double dparam, const string sparam);
   virtual void      OnReleaseEvent(const int id, const long lparam, const double dparam, const string sparam);
   virtual void      OnCreateEvent(const int id, const long lparam, const double dparam, const string sparam);
   virtual void      OnWheelEvent(const int id, const long lparam, const double dparam, const string sparam)      { return;   }  // handler is disabled here
//--- Handlers for custom events of the element when hovering, clicking, and scrolling the wheel in the object area
   virtual void      MouseMoveHandler(const int id, const long lparam, const double dparam, const string sparam)  { return;   }  // handler is disabled here
   virtual void      MousePressHandler(const int id, const long lparam, const double dparam, const string sparam) { return;   }  // handler is disabled here
   virtual void      MouseWheelHandler(const int id, const long lparam, const double dparam, const string sparam) { return;   }  // handler is disabled here
   
public:

In the public section of the class, add methods for getting element color management objects in the activated state and methods for getting colors in various states of the element:

public:
//--- Return the pointer to the canvas (1) background and (2) foreground
   CCanvas          *GetBackground(void)                       { return &this.m_background;                                                        }
   CCanvas          *GetForeground(void)                       { return &this.m_foreground;                                                        }
   
//--- Return the pointer to the color management object for the (1) background, (2) foreground and (3) border
   CColorElement    *GetBackColorControl(void)                 { return &this.m_color_background;                                                  }
   CColorElement    *GetForeColorControl(void)                 { return &this.m_color_foreground;                                                  }
   CColorElement    *GetBorderColorControl(void)               { return &this.m_color_border;                                                      }
   
//--- Return the pointer to the color management object for the (1) background, (2) foreground and (3) activated element border
   CColorElement    *GetBackColorActControl(void)              { return &this.m_color_background_act;                                              }
   CColorElement    *GetForeColorActControl(void)              { return &this.m_color_foreground_act;                                              }
   CColorElement    *GetBorderColorActControl(void)            { return &this.m_color_border_act;                                                  }
   
//--- Return the (1) background, (2) foreground and (3) border color
   color             BackColor(void)         const { return(!this.State() ? this.m_color_background.GetCurrent() : this.m_color_background_act.GetCurrent());  }
   color             ForeColor(void)         const { return(!this.State() ? this.m_color_foreground.GetCurrent() : this.m_color_foreground_act.GetCurrent());  }
   color             BorderColor(void)       const { return(!this.State() ? this.m_color_border.GetCurrent()     : this.m_color_border_act.GetCurrent());      }
   
//--- Return the DEFAULT color of (1) background, (2) foreground, (3) border
   color             BackColorDefault(void)  const { return(!this.State() ? this.m_color_background.GetDefault() : this.m_color_background_act.GetDefault());  }
   color             ForeColorDefault(void)  const { return(!this.State() ? this.m_color_foreground.GetDefault() : this.m_color_foreground_act.GetDefault());  }
   color             BorderColorDefault(void)const { return(!this.State() ? this.m_color_border.GetDefault()     : this.m_color_border_act.GetDefault());      }
   
//--- Return the FOCUSED preset color of the (1) background, (2) foreground, (3) border
   color             BackColorFocused(void)  const { return(!this.State() ? this.m_color_background.GetFocused() : this.m_color_background_act.GetFocused());  }
   color             ForeColorFocused(void)  const { return(!this.State() ? this.m_color_foreground.GetFocused() : this.m_color_foreground_act.GetFocused());  }
   color             BorderColorFocused(void)const { return(!this.State() ? this.m_color_border.GetFocused()     : this.m_color_border_act.GetFocused());      }
   
//--- Return the preset PRESSED color of the (1) background, (2) foreground, (3) frame
   color             BackColorPressed(void)  const { return(!this.State() ? this.m_color_background.GetPressed() : this.m_color_background_act.GetPressed());  }
   color             ForeColorPressed(void)  const { return(!this.State() ? this.m_color_foreground.GetPressed() : this.m_color_foreground_act.GetPressed());  }
   color             BorderColorPressed(void)const { return(!this.State() ? this.m_color_border.GetPressed()     : this.m_color_border_act.GetPressed());      }
   
//--- Return the BLOCKED color of (1) background, (2) foreground, (3) border
   color             BackColorBlocked(void)              const { return this.m_color_background.GetBlocked();                                      }
   color             ForeColorBlocked(void)              const { return this.m_color_foreground.GetBlocked();                                      }
   color             BorderColorBlocked(void)            const { return this.m_color_border.GetBlocked();                                          }
   
//--- Set background colors for all states

Now, in each of the color retrieving methods, the state of the element is checked (activated/deactivated) and the required color is returned according to the element state.

Add methods for setting the colors of the activated element and refine the methods for setting the colors of element states relative to the mouse cursor, given the state of the element as activated/non-activated:

//--- Set background colors for all states
   void              InitBackColorsAct(const color clr_default, const color clr_focused, const color clr_pressed, const color clr_blocked)
                       {
                        this.m_color_background_act.InitColors(clr_default,clr_focused,clr_pressed,clr_blocked);
                       }
   void              InitBackColorsAct(const color clr)        { this.m_color_background_act.InitColors(clr);                                      }

//--- Set foreground colors for all states
   void              InitForeColorsAct(const color clr_default, const color clr_focused, const color clr_pressed, const color clr_blocked)
                       {
                        this.m_color_foreground_act.InitColors(clr_default,clr_focused,clr_pressed,clr_blocked);
                       }
   void              InitForeColorsAct(const color clr)        { this.m_color_foreground_act.InitColors(clr);                                      }

//--- Set border colors for all states
   void              InitBorderColorsAct(const color clr_default, const color clr_focused, const color clr_pressed, const color clr_blocked)
                       {
                        this.m_color_border_act.InitColors(clr_default,clr_focused,clr_pressed,clr_blocked);
                       }
   void              InitBorderColorsAct(const color clr)      { this.m_color_border_act.InitColors(clr);                                          }

//--- Initialize the color of (1) background, (2) foreground and (3) frame with initial values
   void              InitBackColorActDefault(const color clr)  { this.m_color_background_act.InitDefault(clr);                                     }
   void              InitForeColorActDefault(const color clr)  { this.m_color_foreground_act.InitDefault(clr);                                     }
   void              InitBorderColorActDefault(const color clr){ this.m_color_border_act.InitDefault(clr);                                         }
   
//--- Initialize the color of (1) background, (2) foreground and (3) frame on hover with initial values
   void              InitBackColorActFocused(const color clr)  { this.m_color_background_act.InitFocused(clr);                                     }
   void              InitForeColorActFocused(const color clr)  { this.m_color_foreground_act.InitFocused(clr);                                     }
   void              InitBorderColorActFocused(const color clr){ this.m_color_border_act.InitFocused(clr);                                         }
   
//--- Initialize the color of (1) background, (2) foreground and (3) frame on click with initial values
   void              InitBackColorActPressed(const color clr)  { this.m_color_background_act.InitPressed(clr);                                     }
   void              InitForeColorActPressed(const color clr)  { this.m_color_foreground_act.InitPressed(clr);                                     }
   void              InitBorderColorActPressed(const color clr){ this.m_color_border_act.InitPressed(clr);                                         }
   
//--- Set the current background color to different states
   bool              BackColorToDefault(void)
                       {
                        return(!this.State() ? this.m_color_background.SetCurrentAs(COLOR_STATE_DEFAULT) :
                                               this.m_color_background_act.SetCurrentAs(COLOR_STATE_DEFAULT));
                       }
   bool              BackColorToFocused(void)
                       {
                        return(!this.State() ? this.m_color_background.SetCurrentAs(COLOR_STATE_FOCUSED) :
                                               this.m_color_background_act.SetCurrentAs(COLOR_STATE_FOCUSED));
                       }
   bool              BackColorToPressed(void)
                       {
                        return(!this.State() ? this.m_color_background.SetCurrentAs(COLOR_STATE_PRESSED) :
                                               this.m_color_background_act.SetCurrentAs(COLOR_STATE_PRESSED));
                       }
   bool              BackColorToBlocked(void)   { return this.m_color_background.SetCurrentAs(COLOR_STATE_BLOCKED);  }
   
//--- Set the current foreground color to different states
   bool              ForeColorToDefault(void)
                       { return(!this.State() ? this.m_color_foreground.SetCurrentAs(COLOR_STATE_DEFAULT) :
                                                this.m_color_foreground_act.SetCurrentAs(COLOR_STATE_DEFAULT));
                       }
   bool              ForeColorToFocused(void)
                       { return(!this.State() ? this.m_color_foreground.SetCurrentAs(COLOR_STATE_FOCUSED) :
                                                this.m_color_foreground_act.SetCurrentAs(COLOR_STATE_FOCUSED));
                       }
   bool              ForeColorToPressed(void)
                       { return(!this.State() ? this.m_color_foreground.SetCurrentAs(COLOR_STATE_PRESSED) :
                                                this.m_color_foreground_act.SetCurrentAs(COLOR_STATE_PRESSED));
                       }
   bool              ForeColorToBlocked(void)   { return this.m_color_foreground.SetCurrentAs(COLOR_STATE_BLOCKED);  }
   
//--- Set the current frame color to different states
   bool              BorderColorToDefault(void)
                       { return(!this.State() ? this.m_color_border.SetCurrentAs(COLOR_STATE_DEFAULT) :
                                                this.m_color_border_act.SetCurrentAs(COLOR_STATE_DEFAULT));
                       }
   bool              BorderColorToFocused(void)
                       { return(!this.State() ? this.m_color_border.SetCurrentAs(COLOR_STATE_FOCUSED) :
                                                this.m_color_border_act.SetCurrentAs(COLOR_STATE_FOCUSED));
                       }
   bool              BorderColorToPressed(void)
                       { return(!this.State() ? this.m_color_border.SetCurrentAs(COLOR_STATE_PRESSED) :
                                                this.m_color_border_act.SetCurrentAs(COLOR_STATE_PRESSED));
                       }
   bool              BorderColorToBlocked(void) { return this.m_color_border.SetCurrentAs(COLOR_STATE_BLOCKED);      }

Add methods for setting and returning the state of an element:

//--- Create OBJ_BITMAP_LABEL
   bool              Create(const long chart_id,const int wnd,const string object_name,const int x,const int y,const int w,const int h);

//--- (1) Set and (2) return the state
   void              SetState(ENUM_ELEMENT_STATE state)        { this.m_state=state; this.ColorsToDefault();                                       }
   ENUM_ELEMENT_STATE State(void)                        const { return this.m_state;                                                              }

//--- Return (1) the object's belonging to the program, the flag (2) of a hidden element, (3) a blocked element, (4) element in focus and (5) the graphical object name (background, text)

When setting the state of an element, after setting the status flag, all the colors of the element must be recorded as current. If the element is activated, for example, the button is pressed, then all the current colors are set as colors for the pressed button. Otherwise, current colors are taken from the colors list for the released button.

So far as we have now separated setting and returning transparency for the background and foreground, add new methods for transparency setting and returning:

   string            NameBG(void)                        const { return this.m_background.ChartObjectName();                                       }
   string            NameFG(void)                        const { return this.m_foreground.ChartObjectName();                                       }
   
//--- (1) Return and (2) set background transparency
   uchar             AlphaBG(void)                       const { return this.m_alpha_bg;                                                           }
   void              SetAlphaBG(const uchar value)             { this.m_alpha_bg=value;                                                            }
//--- (1) Return and (2) set the foreground transparency
   uchar             AlphaFG(void)                       const { return this.m_alpha_fg;                                                           }
   void              SetAlphaFG(const uchar value)             { this.m_alpha_fg=value;                                                            }

//--- Sets the background and foreground transparency
   void              SetAlpha(const uchar value)               { this.m_alpha_fg=this.m_alpha_bg=value;                                            }
   
//--- (1) Return and (2) set the frame width

Declare an event handler which is supposed to be called from the event handler of the control program:

//--- Event handler                                                    |
   void              OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam);
   
//--- Constructors/destructor
                     CCanvasBase(void) :
                        m_program_name(::MQLInfoString(MQL_PROGRAM_NAME)), m_chart_id(::ChartID()), m_wnd(0),
                        m_alpha_bg(0), m_alpha_fg(255), m_hidden(false), m_blocked(false), m_focused(false), m_border_width(0), m_wnd_y(0), m_state(0) { this.Init(); }
                     CCanvasBase(const string object_name,const long chart_id,const int wnd,const int x,const int y,const int w,const int h);
                    ~CCanvasBase(void);
  };

In the constructor, correctly specify properties of the font being drawn on the canvas and call the Init() method to store properties of the chart and mouse:

//+------------------------------------------------------------------+
//| CCanvasBase::Constructor                                         |
//+------------------------------------------------------------------+
CCanvasBase::CCanvasBase(const string object_name,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) :
   m_program_name(::MQLInfoString(MQL_PROGRAM_NAME)), m_wnd(wnd<0 ? 0 : wnd), m_alpha_bg(0), m_alpha_fg(255), m_hidden(false), m_blocked(false), m_focused(false), m_border_width(0), m_state(0)
  {
//--- Get the adjusted chart ID and the distance in pixels along the vertical Y axis
//--- between the upper frame of the indicator subwindow and the upper frame of the chart main window
   this.m_chart_id=this.CorrectChartID(chart_id);
   this.m_wnd_y=(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOW_YDISTANCE,this.m_wnd);
//--- If the graphical resource and graphical object are created
   if(this.Create(this.m_chart_id,this.m_wnd,object_name,x,y,w,h))
     {
      //--- Clear the background and foreground canvases and set the initial coordinate values,
      //--- names of graphic objects and properties of text drawn in the foreground
      this.Clear(false);
      this.m_obj_x=x;
      this.m_obj_y=y;
      this.m_color_background.SetName("Background");
      this.m_color_foreground.SetName("Foreground");
      this.m_color_border.SetName("Border");
      this.m_foreground.FontSet(DEF_FONTNAME,-DEF_FONTSIZE*10,FW_MEDIUM);
      this.m_bound.SetName("Perimeter");
      
      //--- Remember permissions for the mouse and chart tools
      this.Init();
     }
  }

In the class destructor, destroy the created graphic object and restore the stored properties of the chart and mouse permissions:

//+------------------------------------------------------------------+
//| CCanvasBase::Destructor                                          |
//+------------------------------------------------------------------+
CCanvasBase::~CCanvasBase(void)
  {
//--- Destroy the object
   this.Destroy();
//--- Return permissions for the mouse and chart tools
   ::ChartSetInteger(this.m_chart_id, CHART_EVENT_MOUSE_WHEEL, this.m_chart_mouse_wheel_flag);
   ::ChartSetInteger(this.m_chart_id, CHART_EVENT_MOUSE_MOVE, this.m_chart_mouse_move_flag);
   ::ChartSetInteger(this.m_chart_id, CHART_EVENT_OBJECT_CREATE, this.m_chart_object_create_flag);
   ::ChartSetInteger(this.m_chart_id, CHART_MOUSE_SCROLL, this.m_chart_mouse_scroll_flag);
   ::ChartSetInteger(this.m_chart_id, CHART_CONTEXT_MENU, this.m_chart_context_menu_flag);
   ::ChartSetInteger(this.m_chart_id, CHART_CROSSHAIR_TOOL, this.m_chart_crosshair_tool_flag);
  }

In the method of creating a graphical element, the name of the graphic object to be created should not have spaces. This can be corrected by replacing spaces in the name with underscores:

//+------------------------------------------------------------------+
//| CCanvasBase::Create background and foreground graphical objects  |
//+------------------------------------------------------------------+
bool CCanvasBase::Create(const long chart_id,const int wnd,const string object_name,const int x,const int y,const int w,const int h)
  {
//--- Get the adjusted chart ID
   long id=this.CorrectChartID(chart_id);
//--- Correct the passed object name
   string nm=object_name;
   ::StringReplace(nm," ","_");
//--- Create a graphical object name for the background and create a canvas
   string obj_name=nm+"_BG";
   if(!this.m_background.CreateBitmapLabel(id,(wnd<0 ? 0 : wnd),obj_name,x,y,(w>0 ? w : 1),(h>0 ? h : 1),COLOR_FORMAT_ARGB_NORMALIZE))
     {
      ::PrintFormat("%s: The CreateBitmapLabel() method of the CCanvas class returned an error creating a \"%s\" graphic object",__FUNCTION__,obj_name);
      return false;
     }
//--- Create a graphical object name for the foreground and create a canvas
   obj_name=nm+"_FG";
   if(!this.m_foreground.CreateBitmapLabel(id,(wnd<0 ? 0 : wnd),obj_name,x,y,(w>0 ? w : 1),(h>0 ? h : 1),COLOR_FORMAT_ARGB_NORMALIZE))
     {
      ::PrintFormat("%s: The CreateBitmapLabel() method of the CCanvas class returned an error creating a \"%s\" graphic object",__FUNCTION__,obj_name);
      return false;
     }
//--- If created successfully, enter the program name into the OBJPROP_TEXT property of the graphical object
   ::ObjectSetString(id,this.NameBG(),OBJPROP_TEXT,this.m_program_name);
   ::ObjectSetString(id,this.NameFG(),OBJPROP_TEXT,this.m_program_name);
   
//--- Set the dimensions of the rectangular area and return 'true'
   this.m_bound.SetXY(x,y);
   this.m_bound.Resize(w,h);
   return true;
  }

A method that returns the cursor location flag inside an object:

//+-----------------------------------------------------------------------------------+
//| CCanvasBase::Return the flag indicating whether the cursor is inside the object   |
//+-----------------------------------------------------------------------------------+
bool CCanvasBase::Contains(const int x,const int y)
  {
//--- check and return the result
   int left=::fmax(this.X(),this.ObjectX());
   int right=::fmin(this.Right(),this.ObjectRight());
   int top=::fmax(this.Y(),this.ObjectY());
   int bottom=::fmin(this.Bottom(),this.ObjectBottom());
   return(x>=left && x<=right && y>=top && y<=bottom);
  }

Since the object size and the canvas size may vary (the ObjectTrim method resizes the canvas without resizing the object), here one of the values is taken as the bounds within which the cursor is located: either object bound or a corresponding edge of the canvas. The method returns a flag of location of the coordinates passed to the method within the received bounds.

In the element lock method, the setting of the lock flag must be before calling the element drawing method, fix:

//+------------------------------------------------------------------+
//| CCanvasBase::Block the element                                   |
//+------------------------------------------------------------------+
void CCanvasBase::Block(const bool chart_redraw)
  {
//--- If the element has already been blocked, leave
   if(this.m_blocked)
      return;
//--- Set the current colors as the colors of the blocked element, 
//--- set the lock flag and redraw the object
   this.ColorsToBlocked();
   this.m_blocked=true;
   this.Draw(chart_redraw);
  }

This correction allows you to draw the blocked element correctly. Prior to this correction, when drawing an element, colors were taken from the default state, not from the blocked one, since the flag was set after the lock.

A Method for Setting Bans for the Chart:

//+------------------------------------------------------------------+
//| CCanvasBase::Set chart restrictions                              |
//| (wheel scrolling, context menu and crosshair)                    |
//+------------------------------------------------------------------+
void CCanvasBase::SetFlags(const bool flag)
  {
//--- If you need to set flags, and they have already been set before, leave
   if(flag && this.m_flags_state)
      return;
//--- If we need to reset the flags, and they have already been reset earlier, leave
   if(!flag && !this.m_flags_state)
      return;
//--- Set the required flag for the context menu,
//--- crosshair tool and scrolling the chart with the mouse wheel.
//--- After installation, remember the value of the set flag
   ::ChartSetInteger(this.m_chart_id, CHART_CONTEXT_MENU,  flag);
   ::ChartSetInteger(this.m_chart_id, CHART_CROSSHAIR_TOOL,flag);
   ::ChartSetInteger(this.m_chart_id, CHART_MOUSE_SCROLL,  flag);
   this.m_flags_state=flag;
//--- Update the chart to immediately apply the set flags
   ::ChartRedraw(this.m_chart_id);
  }

Class Initialization Method:

//+------------------------------------------------------------------+
//| CCanvasBase::Class initialization                                |
//+------------------------------------------------------------------+
void CCanvasBase::Init(void)
  {
//--- Remember permissions for the mouse and chart tools
   this.m_chart_mouse_wheel_flag   = ::ChartGetInteger(this.m_chart_id, CHART_EVENT_MOUSE_WHEEL);
   this.m_chart_mouse_move_flag    = ::ChartGetInteger(this.m_chart_id, CHART_EVENT_MOUSE_MOVE);
   this.m_chart_object_create_flag = ::ChartGetInteger(this.m_chart_id, CHART_EVENT_OBJECT_CREATE);
   this.m_chart_mouse_scroll_flag  = ::ChartGetInteger(this.m_chart_id, CHART_MOUSE_SCROLL);
   this.m_chart_context_menu_flag  = ::ChartGetInteger(this.m_chart_id, CHART_CONTEXT_MENU);
   this.m_chart_crosshair_tool_flag= ::ChartGetInteger(this.m_chart_id, CHART_CROSSHAIR_TOOL);
//--- Set permissions for the mouse and chart
   ::ChartSetInteger(this.m_chart_id, CHART_EVENT_MOUSE_WHEEL, true);
   ::ChartSetInteger(this.m_chart_id, CHART_EVENT_MOUSE_MOVE, true);
   ::ChartSetInteger(this.m_chart_id, CHART_EVENT_OBJECT_CREATE, true);
   
//--- Initialize the object default colors
   this.InitColors();
  }

Default Object Color Initialization Method:

//+------------------------------------------------------------------+
//| CCanvasBase::Initialize the object default colors                |
//+------------------------------------------------------------------+
void CCanvasBase::InitColors(void)
  {
//--- Initialize the background colors for the normal and activated states and make it the current background color
   this.InitBackColors(clrWhiteSmoke);
   this.InitBackColorsAct(clrWhiteSmoke);
   this.BackColorToDefault();
   
//--- Initialize the foreground colors for the normal and activated states and make it the current text color
   this.InitForeColors(clrBlack);
   this.InitForeColorsAct(clrBlack);
   this.ForeColorToDefault();
   
//--- Initialize the border colors for the normal and activated states and make it the current border color
   this.InitBorderColors(clrDarkGray);
   this.InitBorderColorsAct(clrDarkGray);
   this.BorderColorToDefault();
   
//--- Initialize the border color and foreground color for the disabled element
   this.InitBorderColorBlocked(clrLightGray);
   this.InitForeColorBlocked(clrSilver);
  }

A Method That Checks the Set Color for Equality to the Specified One:

//+------------------------------------------------------------------+
//| CCanvasBase::Check if the set color is equal to the specified one|
//+------------------------------------------------------------------+
bool CCanvasBase::CheckColor(const ENUM_COLOR_STATE state) const
  {
   bool res=true;
 //--- Depending on the event being checked
   switch(state)
     {
//--- check that all STANDARD background, text, and frame colors are equal to the preset values
      case COLOR_STATE_DEFAULT :
        res &=this.BackColor()==this.BackColorDefault();
        res &=this.ForeColor()==this.ForeColorDefault();
        res &=this.BorderColor()==this.BorderColorDefault();
        break;

//--- check if all FOCUSED background, text, and border colors are equal to the preset values
      case COLOR_STATE_FOCUSED :
        res &=this.BackColor()==this.BackColorFocused();
        res &=this.ForeColor()==this.ForeColorFocused();
        res &=this.BorderColor()==this.BorderColorFocused();
        break;
     
//--- check if all PRESSED background, text, and border colors are equal to the preset values
      case COLOR_STATE_PRESSED :
        res &=this.BackColor()==this.BackColorPressed();
        res &=this.ForeColor()==this.ForeColorPressed();
        res &=this.BorderColor()==this.BorderColorPressed();
        break;
     
//--- check if all BLOCKED background, text, and border colors are equal to the preset values
      case COLOR_STATE_BLOCKED :
        res &=this.BackColor()==this.BackColorBlocked();
        res &=this.ForeColor()==this.ForeColorBlocked();
        res &=this.BorderColor()==this.BorderColorBlocked();
        break;
        
      default: res=false;
        break;
     }
   return res;
  }

In order to change element colors only when the state of the element is toggled, this method returns the flag of already set colors corresponding to element state. If the current colors of the element are not equal to those set for the state checked, the method allows changing the color and redrawing the graphical element. If colors are already set according to the state of the element, there is no need to change the colors and redraw the object; the method bans color change.

A Method That Changes Colors of Object's Elements Based on an Event:

//+------------------------------------------------------------------+
//| CCanvasBase::Change the color of object elements by event        |
//+------------------------------------------------------------------+
void CCanvasBase::ColorChange(const ENUM_COLOR_STATE state)
  {
//--- Depending on the event, set the event colors as primary ones
   switch(state)
     {
      case COLOR_STATE_DEFAULT   :  this.ColorsToDefault(); break;
      case COLOR_STATE_FOCUSED   :  this.ColorsToFocused(); break;
      case COLOR_STATE_PRESSED   :  this.ColorsToPressed(); break;
      case COLOR_STATE_BLOCKED   :  this.ColorsToBlocked(); break;
      default                    :  break;
     }
  }

Depending on the event for which the color must be changed, current colors are set according to the event (element state).

Event handler:

//+------------------------------------------------------------------+
//| CCanvasBase::Event handler                                       |
//+------------------------------------------------------------------+
void CCanvasBase::OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- If the chart changes
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      //--- adjust the distance between the upper frame of the indicator subwindow and the upper frame of the chart main window
      this.m_wnd_y=(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOW_YDISTANCE,this.m_wnd);
     }

//--- If the element is blocked or hidden, leave
   if(this.IsBlocked() || this.IsHidden())
      return;
      
//--- Mouse cursor coordinates
   int x=(int)lparam;
   int y=(int)dparam-this.m_wnd_y;  // Adjust Y by the height of the indicator window

//--- Event of cursor movement or mouse button click
   if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- If the cursor is within the object
      if(this.Contains(x, y))
        {
         //--- If the object is not a part of the container, disable chart scrolling, the context menu, and the Crosshair tool
         if(this.m_container==NULL)
            this.SetFlags(false);
         //--- Get the state of the mouse buttons; if they are pressed, call the click handler
         if(sparam=="1" || sparam=="2" || sparam=="16")
            this.OnPressEvent(id, lparam, dparam, sparam);
         //--- buttons are not pressed - handle the cursor movement 
         else
            this.OnFocusEvent(id, lparam, dparam, sparam);
        }
      //--- Cursor outside the object
      else
        {
         //--- Handle the cursor moving beyond the object boundaries
         this.OnReleaseEvent(id,lparam,dparam,sparam);
         //--- If the object is not a part of the container, enable chart scrolling, the context menu, and the Crosshair tool
         if(this.m_container==NULL)
            this.SetFlags(true);
        }
     }
     
//--- Mouse wheel scroll event
   if(id==CHARTEVENT_MOUSE_WHEEL)
     {
      this.OnWheelEvent(id,lparam,dparam,sparam);
     }

//--- Graphical object creation event
   if(id==CHARTEVENT_OBJECT_CREATE)
     {
      this.OnCreateEvent(id,lparam,dparam,sparam);
     }
     
//--- If a custom chart event has arrived
   if(id>CHARTEVENT_CUSTOM)
     {
      //--- do not handle its own events 
      if(sparam==this.NameBG())
         return;

      //--- bring the custom event in line with the standard ones
      ENUM_CHART_EVENT chart_event=ENUM_CHART_EVENT(id-CHARTEVENT_CUSTOM);
      //--- If clicking an object
      if(chart_event==CHARTEVENT_OBJECT_CLICK)
        {
         this.MousePressHandler(chart_event, lparam, dparam, sparam);
        }
      //--- If the mouse cursor is moving
      if(chart_event==CHARTEVENT_MOUSE_MOVE)
        {
         this.MouseMoveHandler(chart_event, lparam, dparam, sparam);
        }
      //--- If the mouse wheel is scrolling
      if(chart_event==CHARTEVENT_MOUSE_WHEEL)
        {
         this.MouseWheelHandler(chart_event, lparam, dparam, sparam);
        }
     }
  }

The logic of handling the interaction of the mouse cursor with graphical elements is arranged in the base object of graphical elements. Virtual handlers are called for various events being monitored. Some handlers are implemented directly in this class, and some simply do nothing, and they must be implemented in descendant objects of this class.

The event handlers, which name ends in *Handler, are designated to handle interactions within controls between their constituent components. Whereas the handlers with *Event in their name directly handle chart events and send custom events to the chart, which can be used to determine the type of event and which control it was sent from. This will allow the user to handle such events in their program.

Out-of-focus Handler:

//+------------------------------------------------------------------+
//| CCanvasBase::Out of focus handler                                |
//+------------------------------------------------------------------+
void CCanvasBase::OnReleaseEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- The element is not in focus when the cursor is moved away
   this.m_focused=false;
//--- restore the original colors, reset the Focused flag and redraw the object
   if(!this.CheckColor(COLOR_STATE_DEFAULT))
     {
      this.ColorChange(COLOR_STATE_DEFAULT);
      this.Draw(true);
     }
  }

Cursor Hover Handler:

//+------------------------------------------------------------------+
//| CCanvasBase::Hover positioning handler                           |
//+------------------------------------------------------------------+
void CCanvasBase::OnFocusEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Element in focus
   this.m_focused=true;
//--- If the object colors are not for Focused mode
   if(!this.CheckColor(COLOR_STATE_FOCUSED))
     {
      //--- set the colors and the Focused flag and redraw the object
      this.ColorChange(COLOR_STATE_FOCUSED);
      this.Draw(true);
     }
  }

Object Press Handler:

//+------------------------------------------------------------------+
//| CCanvasBase::Object click handler                                |
//+------------------------------------------------------------------+
void CCanvasBase::OnPressEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- The element is in focus when clicked on
   this.m_focused=true;
//--- If the object colors are not for Pressed mode
   if(!this.CheckColor(COLOR_STATE_PRESSED))
     {
      //--- set the Pressed colors and redraw the object
      this.ColorChange(COLOR_STATE_PRESSED);
      this.Draw(true);
     }

   //--- send a custom event to the chart with the passed values in lparam, dparam, and the object name in sparam
   ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_OBJECT_CLICK, lparam, dparam, this.NameBG());
  }

Handler for the Graphic Object Creation Event:

//+------------------------------------------------------------------+
//| CCanvasBase::Graphical object creation event handler             |
//+------------------------------------------------------------------+
void CCanvasBase::OnCreateEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- if this is an object belonging to this program, leave
   if(this.IsBelongsToThis(sparam))
      return;
//--- bring the object to the front
   this.BringToTop(true);
  }

The logic of all handlers is commented on in detail in the code. In fact, only a reaction to an event in the form of color changing of the graphical element is arranged here and, where necessary, sending custom events to the chart. The last handler reacts to the creation of a graphic object on the chart and transfers graphical elements to the foreground. This will allow, for example, the panel to always remain in the foreground.

All these handlers are virtual, and should be redefined in inherited classes if necessary.

We are done with the refinement of the base object of all graphical elements. Now, based on the created Controller component in the base object and the earlier created View component, start creating the simplest graphic elements (which are also part of the View component). And they will become the "building blocks" from which complex controls will eventually be created, and in particular, the Table View control, upon which implementation we have been working on for several articles.


Simple controls

In the same folder \MQL5\Indicators\Tables\Controls\ create a new include file Controls.mqh.

To the created file, connect the file of the base object of graphical elements Base.mqh and add some macro substitutions and enumerations:

//+------------------------------------------------------------------+
//|                                                     Controls.mqh |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"

//+------------------------------------------------------------------+
//| Include libraries                                                |
//+------------------------------------------------------------------+
#include "Base.mqh"

//+------------------------------------------------------------------+
//| Macro substitutions                                              |
//+------------------------------------------------------------------+
#define  DEF_LABEL_W          40          // Text label default width
#define  DEF_LABEL_H          16          // Text label default height
#define  DEF_BUTTON_W         50          // Default button width
#define  DEF_BUTTON_H         16          // Default button height

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ENUM_ELEMENT_COMPARE_BY              // Compared properties
  {
   ELEMENT_SORT_BY_ID   =  0,             // Comparison by element ID
   ELEMENT_SORT_BY_NAME,                  // Comparison by element name
   ELEMENT_SORT_BY_TEXT,                  // Comparison by element text
   ELEMENT_SORT_BY_COLOR,                 // Comparison by element color
   ELEMENT_SORT_BY_ALPHA,                 // Comparison by element transparency
   ELEMENT_SORT_BY_STATE,                 // Comparison by element state
  };
//+------------------------------------------------------------------+ 
//| Functions                                                        |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Classes                                                          |
//+------------------------------------------------------------------+

In macro substitution, we have defined default sizes for text labels and buttons. In the enumeration, we have indicated the available properties of the base graphical element. You can use these properties to search for objects, sort them, and compare them. When adding new properties to any objects, add new constants to this enumeration.


Auxiliary Classes

Each graphic element can have an image in its composition. This will enable to draw icons for buttons, lines of text, etc.
Create a special class for drawing images, which will be an integral part of simple controls.

A Class for Drawing Images Within a Defined Area

An object of this class will be declared in the control class and will enable to specify the size of the area and its coordinates, inside which the image will be drawn. Provide the class with methods for drawing arrows for arrow buttons, checkboxes, and radio buttons. Later, add methods for drawing other icons and a method for drawing your own images. To the class we will pass a pointer to the canvas on which drawing is performed in the coordinates and bounds set in the class of the drawing object:
//+------------------------------------------------------------------+
//| Classes                                                          |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Image drawing class                                              |
//+------------------------------------------------------------------+
class CImagePainter : public CBaseObj
  {
protected:
   CCanvas          *m_canvas;                                 // Pointer to the canvas where we draw
   CBound            m_bound;                                  // Image coordinates and boundaries
   uchar             m_alpha;                                  // Transparency
   
//--- Check the canvas validity and correct dimensions
   bool              CheckBound(void);

public:
//--- (1) Assigns the canvas to draw on, (2) sets and (3) returns transparency
   void              CanvasAssign(CCanvas *canvas)             { this.m_canvas=canvas;                }
   void              SetAlpha(const uchar value)               { this.m_alpha=value;                  }
   uchar             Alpha(void)                         const { return this.m_alpha;                 }
   
//--- (1) Set the coordinates and (2) change the area size
   void              SetXY(const int x,const int y)            { this.m_bound.SetXY(x,y);             }
   void              SetSize(const int w,const int h)          { this.m_bound.Resize(w,h);            }
//--- Set the area coordinates and dimensions
   void              SetBound(const int x,const int y,const int w,const int h)
                       {
                        this.SetXY(x,y);
                        this.SetSize(w,h);
                       }

//--- Returns the image boundaries and dimensions
   int               X(void)                             const { return this.m_bound.X();             }
   int               Y(void)                             const { return this.m_bound.Y();             }
   int               Right(void)                         const { return this.m_bound.Right();         }
   int               Bottom(void)                        const { return this.m_bound.Bottom();        }
   int               Width(void)                         const { return this.m_bound.Width();         }
   int               Height(void)                        const { return this.m_bound.Height();        }
   
//--- Clear the area
   bool              Clear(const int x,const int y,const int w,const int h,const bool update=true);
//--- Draw a filled (1) up, (2) down, (3) left and (4) right arrow
   bool              ArrowUp(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              ArrowDown(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              ArrowLeft(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              ArrowRight(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   
//--- Draw (1) checked and (2) unchecked CheckBox
   bool              CheckedBox(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              UncheckedBox(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   
//--- Draw (1) checked and (2) unchecked RadioButton
   bool              CheckedRadioButton(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              UncheckedRadioButton(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   
//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_IMAGE_PAINTER);  }
   
//--- Constructors/destructor
                     CImagePainter(void) : m_canvas(NULL)               { this.SetBound(1,1,DEF_BUTTON_H-2,DEF_BUTTON_H-2); this.SetName("Image Painter");  }
                     CImagePainter(CCanvas *canvas) : m_canvas(canvas)  { this.SetBound(1,1,DEF_BUTTON_H-2,DEF_BUTTON_H-2); this.SetName("Image Painter");  }
                     CImagePainter(CCanvas *canvas,const int id,const string name) : m_canvas(canvas)
                       {
                        this.m_id=id;
                        this.SetName(name);
                        this.SetBound(1,1,DEF_BUTTON_H-2,DEF_BUTTON_H-2);
                       }
                     CImagePainter(CCanvas *canvas,const int id,const int dx,const int dy,const int w,const int h,const string name) : m_canvas(canvas)
                       {
                        this.m_id=id;
                        this.SetName(name);
                        this.SetBound(dx,dy,w,h);
                       }
                    ~CImagePainter(void) {}
  };

Let us consider class methods.

A Method for Comparing Two Drawing Objects:

//+------------------------------------------------------------------+
//| CImagePainter::Compare two objects                               |
//+------------------------------------------------------------------+
int CImagePainter::Compare(const CObject *node,const int mode=0) const
  {
   const CImagePainter *obj=node;
   switch(mode)
     {
      case ELEMENT_SORT_BY_NAME  :  return(this.Name() >obj.Name()   ? 1 : this.Name() <obj.Name() ? -1 : 0);
      case ELEMENT_SORT_BY_ALPHA :  return(this.Alpha()>obj.Alpha()  ? 1 : this.Alpha()<obj.Alpha()? -1 : 0);
      default                    :  return(this.ID()   >obj.ID()     ? 1 : this.ID()   <obj.ID()   ? -1 : 0);
     }
  }

This method is necessary to find the required drawing object. By default, the search is performed by the object ID. The method will be required when objects of controls contain lists where drawing objects are stored. At the moment, one drawing object will be declared in each control. Such drawing object is intended for drawing the main icon of the element.

A Method That Verifies the Canvas Validity and the Correct Size of the Image Area:

//+------------------------------------------------------------------+
//|CImagePainter::Check the canvas validity and correct dimensions   |
//+------------------------------------------------------------------+
bool CImagePainter::CheckBound(void)
  {
   if(this.m_canvas==NULL)
     {
      ::PrintFormat("%s: Error. First you need to assign the canvas using the CanvasAssign() method",__FUNCTION__);
      return false;
     }
   if(this.Width()==0 || this.Height()==0)
     {
      ::PrintFormat("%s: Error. First you need to set the area size using the SetSize() or SetBound() methods",__FUNCTION__);
      return false;
     }
   return true;
  }

If a pointer to the canvas is not passed to the object, or the width and height of the image area are not set, the method returns false. Otherwise — true.

A Method That Clears the Image Area:

//+------------------------------------------------------------------+
//| CImagePainter::Clear the area                                    |
//+------------------------------------------------------------------+
bool CImagePainter::Clear(const int x,const int y,const int w,const int h,const bool update=true)
  {
//--- If the image area is not valid, return 'false'
   if(!this.CheckBound())
      return false;
//--- Clear the entire image area with transparent color
   this.m_canvas.FillRectangle(x,y,x+w-1,y+h-1,clrNULL);
//--- If specified, update the canvas
   if(update)
      this.m_canvas.Update(false);
//--- All is successful
   return true;   
  }

The method completely clears the entire area of the image, filling it with a transparent color.

A Method That Draws a Shaded Up Arrow:

//+------------------------------------------------------------------+
//| CImagePainter::Draw a filled up arrow                            |
//+------------------------------------------------------------------+
bool CImagePainter::ArrowUp(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true)
  {
//--- If the image area is not valid, return 'false'
   if(!this.CheckBound())
      return false;

//--- Calculate the coordinates of the arrow corners inside the image area
   int hw=(int)::floor(w/2);  // Half width
   if(hw==0)
      hw=1;

   int x1 = x + 1;            // X. Base (left point)
   int y1 = y + h - 4;        // Y. Left base point
   int x2 = x1 + hw;          // X. Vertex (central top point)
   int y2 = y + 3;            // Y. Vertex (highest point)
   int x3 = x1 + w - 1;       // X. Base (right point)
   int y3 = y1;               // Y. Base (right point)

//--- Draw a triangle
   this.m_canvas.FillTriangle(x1, y1, x2, y2, x3, y3, ::ColorToARGB(clr, alpha));
   if(update)
      this.m_canvas.Update(false);
   return true;   
  }

A Method That Draws a Shaded Down Arrow:

//+------------------------------------------------------------------+
//| CImagePainter::Draw a filled down arrow                          |
//+------------------------------------------------------------------+
bool CImagePainter::ArrowDown(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true)
  {
//--- If the image area is not valid, return 'false'
   if(!this.CheckBound())
      return false;

//--- Calculate the coordinates of the arrow corners inside the image area
   int hw=(int)::floor(w/2);  // Half width
   if(hw==0)
      hw=1;

   int x1=x+1;                // X. Base (left point)
   int y1=y+4;                // Y. Left base point
   int x2=x1+hw;              // X. Vertex (central bottom point)
   int y2=y+h-3;              // Y. Vertex (lowest point)
   int x3=x1+w-1;             // X. Base (right point)
   int y3=y1;                 // Y. Base (right point)

//--- Draw a triangle
   this.m_canvas.FillTriangle(x1, y1, x2, y2, x3, y3, ::ColorToARGB(clr, alpha));
   if(update)
      this.m_canvas.Update(false);
   return true;   
  }

A Method That Draws a Shaded Left Arrow:

//+------------------------------------------------------------------+
//| CImagePainter::Draw a filled left arrow                          |
//+------------------------------------------------------------------+
bool CImagePainter::ArrowLeft(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true)
  {
//--- If the image area is not valid, return 'false'
   if(!this.CheckBound())
      return false;

//--- Calculate the coordinates of the arrow corners inside the image area
   int hh=(int)::floor(h/2);  // Half height
   if(hh==0)
      hh=1;

   int x1=x+w-4;              // X. Base (right side)
   int y1=y+1;                // Y. Base upper corner
   int x2=x+3;                // X. Vertex (left center point)
   int y2=y1+hh;              // Y. Central point (vertex)
   int x3=x1;                 // X. Bottom base corner
   int y3=y1+h-1;             // Y. Bottom base corner

//--- Draw a triangle
   this.m_canvas.FillTriangle(x1, y1, x2, y2, x3, y3, ::ColorToARGB(clr, alpha));

   if(update)
      this.m_canvas.Update(false);
   return true;
  }

A Method That Draws a Shaded Right Arrow:

//+------------------------------------------------------------------+
//| CImagePainter::Draw a filled right arrow                         |
//+------------------------------------------------------------------+
bool CImagePainter::ArrowRight(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true)
  {
//--- If the image area is not valid, return 'false'
   if(!this.CheckBound())
      return false;

//--- Calculate the coordinates of the arrow corners inside the image area
   int hh=(int)::floor(h/2);  // Half height
   if(hh==0)
      hh=1;

   int x1=x+4;                // X. Triangle base (left side)
   int y1=y+1;                // Y. Base upper corner
   int x2=x+w-3;              // X. Vertex (right center point)
   int y2=y1+hh;              // Y. Central point (vertex)
   int x3=x1;                 // X. Bottom base corner
   int y3=y1+h-1;             // Y. Bottom base corner

//--- Draw a triangle
   this.m_canvas.FillTriangle(x1, y1, x2, y2, x3, y3, ::ColorToARGB(clr, alpha));
   if(update)
      this.m_canvas.Update(false);
   return true;
  }

Inside the image area, an area for an arrow is defined with an indentation of one pixel on each side of the rectangular area, and a shaded arrow is drawn inside.

A Method That Draws a Checked CheckBox:

//+------------------------------------------------------------------+
//| CImagePainter::Draw a checked CheckBox                           |
//+------------------------------------------------------------------+
bool CImagePainter::CheckedBox(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true)
  {
//--- If the image area is not valid, return 'false'
   if(!this.CheckBound())
      return false;

//--- Rectangle coordinates
   int x1=x+1;                // Upper left corner, X
   int y1=y+1;                // Upper left corner, Y
   int x2=x+w-2;              // Bottom right corner, X
   int y2=y+h-2;              // Bottom right corner, Y

//--- Draw a rectangle
   this.m_canvas.Rectangle(x1, y1, x2, y2, ::ColorToARGB(clr, alpha));
   
//--- Checkmark coordinates
   int arrx[3], arry[3];
   
   arrx[0]=x1+(x2-x1)/4;      // X. Left point
   arrx[1]=x1+w/3;            // X. Central point
   arrx[2]=x2-(x2-x1)/4;      // X. Right point
   
   arry[0]=y1+1+(y2-y1)/2;    // Y. Left point
   arry[1]=y2-(y2-y1)/3;      // Y. Central point
   arry[2]=y1+(y2-y1)/3;      // Y. Right point
   
//--- Draw a "tick" with a double-thickness line
   this.m_canvas.Polyline(arrx, arry, ::ColorToARGB(clr, alpha));
   arrx[0]++;
   arrx[1]++;
   arrx[2]++;
   this.m_canvas.Polyline(arrx, arry, ::ColorToARGB(clr, alpha));
   
   if(update)
      this.m_canvas.Update(false);
   return true;
  }

A Method That Draws an Unchecked CheckBox:

//+------------------------------------------------------------------+
//| CImagePainter::Draw unchecked CheckBox                           |
//+------------------------------------------------------------------+
bool CImagePainter::UncheckedBox(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true)
  {
//--- If the image area is not valid, return 'false'
   if(!this.CheckBound())
      return false;

//--- Rectangle coordinates
   int x1=x+1;                // Upper left corner, X
   int y1=y+1;                // Upper left corner, Y
   int x2=x+w-2;              // Bottom right corner, X
   int y2=y+h-2;              // Bottom right corner, Y

//--- Draw a rectangle
   this.m_canvas.Rectangle(x1, y1, x2, y2, ::ColorToARGB(clr, alpha));
   
   if(update)
      this.m_canvas.Update(false);
   return true;
  }

A Method That Draws a Checked RadioButton:

//+------------------------------------------------------------------+
//| CImagePainter::Draw checked RadioButton                          |
//+------------------------------------------------------------------+
bool CImagePainter::CheckedRadioButton(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true)
  {
//--- If the image area is not valid, return 'false'
   if(!this.CheckBound())
      return false;

//--- Circle coordinates and radius
   int x1=x+1;                // Circle region upper left corner, X
   int y1=y+1;                // Circle region upper left corner, Y
   int x2=x+w-2;              // Circle region lower right corner, X
   int y2=y+h-2;              // Circle region lower right corner, Y
   
//--- Circle coordinates and radius
   int d=::fmin(x2-x1,y2-y1); // Shorter side diameter (width or height)
   int r=d/2;                 // Radius
   int cx=x1+r;               // Center X coordinate
   int cy=y1+r;               // Center Y coordinate

//--- Draw a circle
   this.m_canvas.CircleWu(cx, cy, r, ::ColorToARGB(clr, alpha));
   
//--- Label radius
   r/=2;
   if(r<1)
      r=1;
//--- Draw a label
   this.m_canvas.FillCircle(cx, cy, r, ::ColorToARGB(clr, alpha));
   
   if(update)
      this.m_canvas.Update(false);
   return true;
  }

A Method That Draws an Unchecked RadioButton:

//+------------------------------------------------------------------+
//| CImagePainter::Draw unchecked RadioButton                        |
//+------------------------------------------------------------------+
bool CImagePainter::UncheckedRadioButton(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true)
  {
//--- If the image area is not valid, return 'false'
   if(!this.CheckBound())
      return false;

//--- Circle coordinates and radius
   int x1=x+1;                // Circle region upper left corner, X
   int y1=y+1;                // Circle region upper left corner, Y
   int x2=x+w-2;              // Circle region lower right corner, X
   int y2=y+h-2;              // Circle region lower right corner, Y
   
//--- Circle coordinates and radius
   int d=::fmin(x2-x1,y2-y1); // Shorter side diameter (width or height)
   int r=d/2;                 // Radius
   int cx=x1+r;               // Center X coordinate
   int cy=y1+r;               // Center Y coordinate

//--- Draw a circle
   this.m_canvas.CircleWu(cx, cy, r, ::ColorToARGB(clr, alpha));
   
   if(update)
      this.m_canvas.Update(false);
   return true;
  }

These are simple methods that simply make it possible to draw the desired shapes without having to implement them yourself. Next, add other methods here that draw other icons for the design of graphical elements.

Methods For Saving a Drawing Area to a File and Uploading It From a File:

//+------------------------------------------------------------------+
//| CImagePainter::Save to file                                      |
//+------------------------------------------------------------------+
bool CImagePainter::Save(const int file_handle)
  {
//--- Save the parent object data
   if(!CBaseObj::Save(file_handle))
      return false;
  
//--- Save transparency
   if(::FileWriteInteger(file_handle,this.m_alpha,INT_VALUE)!=INT_VALUE)
      return false;
//--- Save area data
   if(!this.m_bound.Save(file_handle))
      return false;
      
//--- All is successful
   return true;
  }
//+------------------------------------------------------------------+
//| CImagePainter::Load from file                                    |
//+------------------------------------------------------------------+
bool CImagePainter::Load(const int file_handle)
  {
//--- Load parent object data
   if(!CBaseObj::Load(file_handle))
      return false;
      
//--- Load transparency
   this.m_alpha=(uchar)::FileReadInteger(file_handle,INT_VALUE);
//--- Load area data
   if(!this.m_bound.Load(file_handle))
      return false;
   
//--- All is successful
   return true;
  }

Now, we can start with the classes of simple controls. The minimum such object will be the text label class. Classes of other controls will be inherited from this element.
In the same Controls.mqh file, continue to write class codes.

The "Text Label" Control Class

This class will have a set of variables and methods that allow working with any control, setting and receiving object parameters, saving and loading its properties. The interactivity of all controls (the Controller component) has been added to the base class of all controls today. Now, discuss the text label class:

//+------------------------------------------------------------------+
//| Text label class                                                 |
//+------------------------------------------------------------------+
class CLabel : public CCanvasBase
  {
protected:
   CImagePainter     m_painter;                                // Drawing class
   ushort            m_text[];                                 // Text
   ushort            m_text_prev[];                            // Previous text
   int               m_text_x;                                 // Text X coordinate (offset relative to the object left border)
   int               m_text_y;                                 // Text Y coordinate (offset relative to the object upper border)
   
//--- (1) Set and (2) return the previous text
   void              SetTextPrev(const string text)            { ::StringToShortArray(text,this.m_text_prev);  }
   string            TextPrev(void)                      const { return ::ShortArrayToString(this.m_text_prev);}
      
//--- Delete the text
   void              ClearText(void);

public:
//--- Return the pointer to the drawing class
   CImagePainter    *Painter(void)                             { return &this.m_painter;           }
   
//--- (1) Set and (2) return the text
   void              SetText(const string text)                { ::StringToShortArray(text,this.m_text);       }
   string            Text(void)                          const { return ::ShortArrayToString(this.m_text);     }
   
//--- Return the text (1) X and (2) Y coordinate
   int               TextX(void)                         const { return this.m_text_x;                         }
   int               TextY(void)                         const { return this.m_text_y;                         }

//--- Set the text (1) X and (2) Y coordinate
   void              SetTextShiftH(const int x)                { this.m_text_x=x;                              }
   void              SetTextShiftV(const int y)                { this.m_text_y=y;                              }
   
//--- (1) Set the coordinates and (2) change the image area size
   void              SetImageXY(const int x,const int y)       { this.m_painter.SetXY(x,y);                    }
   void              SetImageSize(const int w,const int h)     { this.m_painter.SetSize(w,h);                  }
//--- Set the area coordinates and image area dimensions
   void              SetImageBound(const int x,const int y,const int w,const int h)
                       {
                        this.SetImageXY(x,y);
                        this.SetImageSize(w,h);
                       }
//--- Return the (1) X, (2) Y coordinate, (3) width, (4) height, (5) right, (6) bottom image area border
   int               ImageX(void)                        const { return this.m_painter.X();                    }
   int               ImageY(void)                        const { return this.m_painter.Y();                    }
   int               ImageWidth(void)                    const { return this.m_painter.Width();                }
   int               ImageHeight(void)                   const { return this.m_painter.Height();               }
   int               ImageRight(void)                    const { return this.m_painter.Right();                }
   int               ImageBottom(void)                   const { return this.m_painter.Bottom();               }

//--- Display the text
   void              DrawText(const int dx, const int dy, const string text, const bool chart_redraw);
   
//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);

//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_LABEL);                   }
   
//--- Constructors/destructor
                     CLabel(void);
                     CLabel(const string object_name, const string text, const int x, const int y, const int w, const int h);
                     CLabel(const string object_name, const string text, const int wnd, const int x, const int y, const int w, const int h);
                     CLabel(const string object_name, const long chart_id, const int wnd, const string text, const int x, const int y, const int w, const int h);
                    ~CLabel(void) {}
  };

The class defines two ushort arrays of characters for the current and past label texts. This allows you to have access to dimensions of the previous text when drawing, and to correctly erase the area covered by the text before displaying a new text on the canvas.

Consider the declared methods.

The class has four constructors that allow to create an object using different sets of parameters:

//+------------------------------------------------------------------+
//| CLabel::Default constructor. Build a label in the main window    |
//| of the current chart at coordinates 0,0 with default dimensions  |
//+------------------------------------------------------------------+
CLabel::CLabel(void) : CCanvasBase("Label",::ChartID(),0,0,0,DEF_LABEL_W,DEF_LABEL_H), m_text_x(0), m_text_y(0)
  {
//--- Assign the foreground canvas to the drawing object and
//--- reset the coordinates and dimensions, which makes it inactive
   this.m_painter.CanvasAssign(this.GetForeground());
   this.m_painter.SetXY(0,0);
   this.m_painter.SetSize(0,0);
//--- Set the current and previous text
   this.SetText("Label");
   this.SetTextPrev("");
//--- Background is transparent, foreground is not
   this.SetAlphaBG(0);
   this.SetAlphaFG(255);
  }
//+-----------------------------------------------------------------------------+
//| CLabel::Parametric constructor. Build a label in the main window            |
//| of the current chart with the specified text, coordinates and dimensions    |
//+-----------------------------------------------------------------------------+
CLabel::CLabel(const string object_name, const string text,const int x,const int y,const int w,const int h) :
   CCanvasBase(object_name,::ChartID(),0,x,y,w,h), m_text_x(0), m_text_y(0)
  {
//--- Assign the foreground canvas to the drawing object and
//--- reset the coordinates and dimensions, which makes it inactive
   this.m_painter.CanvasAssign(this.GetForeground());
   this.m_painter.SetXY(0,0);
   this.m_painter.SetSize(0,0);
//--- Set the current and previous text
   this.SetText(text);
   this.SetTextPrev("");
//--- Background is transparent, foreground is not
   this.SetAlphaBG(0);
   this.SetAlphaFG(255);
  }
//+-----------------------------------------------------------------------------+
//| CLabel::Parametric constructor. Builds a label in the specified window      |
//| of the current chart with the specified text, coordinates and dimensions    |
//+-----------------------------------------------------------------------------+
CLabel::CLabel(const string object_name, const string text,const int wnd,const int x,const int y,const int w,const int h) :
   CCanvasBase(object_name,::ChartID(),wnd,x,y,w,h), m_text_x(0), m_text_y(0)
  {
//--- Assign the foreground canvas to the drawing object and
//--- reset the coordinates and dimensions, which makes it inactive
   this.m_painter.CanvasAssign(this.GetForeground());
   this.m_painter.SetXY(0,0);
   this.m_painter.SetSize(0,0);
//--- Set the current and previous text
   this.SetText(text);
   this.SetTextPrev("");
//--- Background is transparent, foreground is not
   this.SetAlphaBG(0);
   this.SetAlphaFG(255);
  }
//+------------------------------------------------------------------------------+
//| CLabel::Parametric constructor. Builds a label in the specified window       |
//| of the specified chart with the specified text, coordinates and dimensions   |
//+------------------------------------------------------------------------------+
CLabel::CLabel(const string object_name,const long chart_id,const int wnd,const string text,const int x,const int y,const int w,const int h) :
   CCanvasBase(object_name,chart_id,wnd,x,y,w,h), m_text_x(0), m_text_y(0)
  {
//--- Assign the foreground canvas to the drawing object and
//--- reset the coordinates and dimensions, which makes it inactive
   this.m_painter.CanvasAssign(this.GetForeground());
   this.m_painter.SetXY(0,0);
   this.m_painter.SetSize(0,0);
//--- Set the current and previous text
   this.SetText(text);
   this.SetTextPrev("");
//--- Background is transparent, foreground is not
   this.SetAlphaBG(0);
   this.SetAlphaFG(255);
  }

The drawing (element icon) area is set to zero dimensions, which means that the element does not have an icon. The text of the element is set and full transparency is assigned for the background, while full opacity is assigned for the foreground.

A Method for Comparing Two Objects:

//+------------------------------------------------------------------+
//| CLabel::Compare two objects                                      |
//+------------------------------------------------------------------+
int CLabel::Compare(const CObject *node,const int mode=0) const
  {
   const CLabel *obj=node;
   switch(mode)
     {
      case ELEMENT_SORT_BY_NAME  :  return(this.Name()     >obj.Name()      ? 1 : this.Name()     <obj.Name()      ? -1 : 0);
      case ELEMENT_SORT_BY_TEXT  :  return(this.Text()     >obj.Text()      ? 1 : this.Text()     <obj.Text()      ? -1 : 0);
      case ELEMENT_SORT_BY_COLOR :  return(this.ForeColor()>obj.ForeColor() ? 1 : this.ForeColor()<obj.ForeColor() ? -1 : 0);
      case ELEMENT_SORT_BY_ALPHA :  return(this.AlphaFG()  >obj.AlphaFG()   ? 1 : this.AlphaFG()  <obj.AlphaFG()   ? -1 : 0);
      default                    :  return(this.ID()       >obj.ID()        ? 1 : this.ID()       <obj.ID()        ? -1 : 0);
     }
  }

Comparison is possible by object name, label text, color, transparency, and identifier. By default, objects are compared by the object ID, since when objects are in the same list, it is better to distinguish them by IDs for quick access to the required one.

A Method That Erases The Label Text:

//+------------------------------------------------------------------+
//| CLabel::Delete the text                                          |
//+------------------------------------------------------------------+
void CLabel::ClearText(void)
  {
   int w=0, h=0;
   string text=this.TextPrev();
//--- Get the dimensions of the previous text
   if(text!="")
      this.m_foreground.TextSize(text,w,h);
//--- If the dimensions are received, draw a transparent rectangle in place of the text erasing it
   if(w>0 && h>0)
      this.m_foreground.FillRectangle(this.AdjX(this.m_text_x),this.AdjY(this.m_text_y),this.AdjX(this.m_text_x+w),this.AdjY(this.m_text_y+h),clrNULL);
//--- Otherwise, clear the entire foreground
   else
      this.m_foreground.Erase(clrNULL);
  }

If the text was written earlier, then you can erase it by painting it over with a completely transparent rectangle according to the size of the text. If there was no text before, the entire canvas area of the object is erased.

A method for displaying text on canvas:

//+------------------------------------------------------------------+
//| CLabel::Display the text                                         |
//+------------------------------------------------------------------+
void CLabel::DrawText(const int dx,const int dy,const string text,const bool chart_redraw)
  {
//--- Clear the previous text and set the new one
   this.ClearText();
   this.SetText(text);
//--- Display the set text
   this.m_foreground.TextOut(this.AdjX(dx),this.AdjY(dy),this.Text(),::ColorToARGB(this.ForeColor(),this.AlphaFG()));
   
//--- If the text goes beyond the object right border
   if(this.Width()-dx<this.m_foreground.TextWidth(text))
     {
      //--- Get the dimensions of the "..." text 
      int w=0,h=0;
      this.m_foreground.TextSize("... ",w,h);
      if(w>0 && h>0)
        {
         //--- Erase the text at the right edge of the object to the size of the "..." text and replace the end of the label text with "..."
         this.m_foreground.FillRectangle(this.AdjX(this.Width()-w),this.AdjY(this.m_text_y),this.AdjX(this.Width()),this.AdjY(this.m_text_y+h),clrNULL);
         this.m_foreground.TextOut(this.AdjX(this.Width()-w),this.AdjY(dy),"...",::ColorToARGB(this.ForeColor(),this.AlphaFG()));
        }
     }
//--- Update the foreground canvas and remember the new text coordinates
   this.m_foreground.Update(chart_redraw);
   this.m_text_x=dx;
   this.m_text_y=dy;
//--- Save the rendered text as the previous one
   this.SetTextPrev(text);
  }

Here, the previous text on the canvas is first erased, and then a new one is displayed. If the new text overruns the bounds of the object, then a colon is displayed on the right where it overruns the element, indicating that the text does not fit into the object area, something like this: “This text does not fit...".

A method that draws the appearance:

//+------------------------------------------------------------------+
//| CLabel::Draw the appearance                                      |
//+------------------------------------------------------------------+
void CLabel::Draw(const bool chart_redraw)
  {
   this.DrawText(this.m_text_x,this.m_text_y,this.Text(),chart_redraw);
  }

Here, the method for drawing a label text is simply called.

Methods of Manipulations With Files:

//+------------------------------------------------------------------+
//| CLabel::Save to file                                             |
//+------------------------------------------------------------------+
bool CLabel::Save(const int file_handle)
  {
//--- Save the parent object data
   if(!CCanvasBase::Save(file_handle))
      return false;
  
//--- Save the text
   if(::FileWriteArray(file_handle,this.m_text)!=sizeof(this.m_text))
      return false;
//--- Save the previous text
   if(::FileWriteArray(file_handle,this.m_text_prev)!=sizeof(this.m_text_prev))
      return false;
//--- Save the text X coordinate
   if(::FileWriteInteger(file_handle,this.m_text_x,INT_VALUE)!=INT_VALUE)
      return false;
//--- Save the text Y coordinate
   if(::FileWriteInteger(file_handle,this.m_text_y,INT_VALUE)!=INT_VALUE)
      return false;
   
//--- All is successful
   return true;
  }
//+------------------------------------------------------------------+
//| CLabel::Load from file                                           |
//+------------------------------------------------------------------+
bool CLabel::Load(const int file_handle)
  {
//--- Load parent object data
   if(!CCanvasBase::Load(file_handle))
      return false;
      
//--- Load the text
   if(::FileReadArray(file_handle,this.m_text)!=sizeof(this.m_text))
      return false;
//--- Load the previous text
   if(::FileReadArray(file_handle,this.m_text_prev)!=sizeof(this.m_text_prev))
      return false;
//--- Load the text X coordinate
   this.m_text_x=::FileReadInteger(file_handle,INT_VALUE);
//--- Load the text Y coordinate
   this.m_text_y=::FileReadInteger(file_handle,INT_VALUE);
   
//--- All is successful
   return true;
  }

Based on the considered class, create a simple button class.

The “Simple Button" Control Class

//+------------------------------------------------------------------+
//| Simple button class                                              |
//+------------------------------------------------------------------+
class CButton : public CLabel
  {
public:
//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);

//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle)               { return CLabel::Save(file_handle); }
   virtual bool      Load(const int file_handle)               { return CLabel::Load(file_handle); }
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_BUTTON);      }
   
//--- Constructors/destructor
                     CButton(void);
                     CButton(const string object_name, const string text, const int x, const int y, const int w, const int h);
                     CButton(const string object_name, const string text, const int wnd, const int x, const int y, const int w, const int h);
                     CButton(const string object_name, const long chart_id, const int wnd, const string text, const int x, const int y, const int w, const int h);
                    ~CButton (void) {}
  };

The simple button class differs from the text label class only by the method of drawing the appearance.

The class has four constructors that allow to create a button with specified parameters:

//+-------------------------------------------------------------------+
//| CButton::Default constructor. Builds a button in the main window  |
//| of the current chart at coordinates 0,0 with default dimensions   |
//+-------------------------------------------------------------------+
CButton::CButton(void) : CLabel("Button",::ChartID(),0,"Button",0,0,DEF_BUTTON_W,DEF_BUTTON_H)
  {
   this.SetState(ELEMENT_STATE_DEF);
   this.SetAlpha(255);
  }
//+-----------------------------------------------------------------------------+
//| CButton::Parametric constructor. Builds a button in the main window         |
//| of the current chart with the specified text, coordinates and dimensions    |
//+-----------------------------------------------------------------------------+
CButton::CButton(const string object_name,const string text,const int x,const int y,const int w,const int h) :
   CLabel(object_name,::ChartID(),0,text,x,y,w,h)
  {
   this.SetState(ELEMENT_STATE_DEF);
   this.SetAlpha(255);
  }
//+-------------------------------------------------------------------------------+
//| CButton::Parametric constructor. Builds a button in the specified window      |
//| of the current chart with the specified text, coordinates and dimensions      |
//+-------------------------------------------------------------------------------+
CButton::CButton(const string object_name,const string text,const int wnd,const int x,const int y,const int w,const int h) :
   CLabel(object_name,::ChartID(),wnd,text,x,y,w,h)
  {
   this.SetState(ELEMENT_STATE_DEF);
   this.SetAlpha(255);
  }
//+-------------------------------------------------------------------------------+
//| CButton::Parametric constructor. Builds a button in the specified window      |
//| of the specified chart with the specified text, coordinates and dimensions    |
//+-------------------------------------------------------------------------------+
CButton::CButton(const string object_name,const long chart_id,const int wnd,const string text,const int x,const int y,const int w,const int h) :
   CLabel(object_name,chart_id,wnd,text,x,y,w,h)
  {
   this.SetState(ELEMENT_STATE_DEF);
   this.SetAlpha(255);
  }

Set the "button not pressed" state and set full opacity for the background and foreground.

A method for comparing two objects:

//+------------------------------------------------------------------+
//| CButton::Compare two objects                                     |
//+------------------------------------------------------------------+
int CButton::Compare(const CObject *node,const int mode=0) const
  {
   const CButton *obj=node;
   switch(mode)
     {
      case ELEMENT_SORT_BY_NAME  :  return(this.Name()     >obj.Name()      ? 1 : this.Name()     <obj.Name()      ? -1 : 0);
      case ELEMENT_SORT_BY_TEXT  :  return(this.Text()     >obj.Text()      ? 1 : this.Text()     <obj.Text()      ? -1 : 0);
      case ELEMENT_SORT_BY_COLOR :  return(this.BackColor()>obj.BackColor() ? 1 : this.BackColor()<obj.BackColor() ? -1 : 0);
      case ELEMENT_SORT_BY_ALPHA :  return(this.AlphaBG()  >obj.AlphaBG()   ? 1 : this.AlphaBG()  <obj.AlphaBG()   ? -1 : 0);
      default                    :  return(this.ID()       >obj.ID()        ? 1 : this.ID()       <obj.ID()        ? -1 : 0);
     }
  }

The method is identical to that of the text label class. Most likely, if the buttons do not have any other properties, this method can be removed from the class, and the parent class method will be used.

A Method That Draws the Appearance of the Button:

//+------------------------------------------------------------------+
//| CButton::Draw the appearance                                     |
//+------------------------------------------------------------------+
void CButton::Draw(const bool chart_redraw)
  {
//--- Fill the button with the background color, draw the frame and update the background canvas
   this.Fill(this.BackColor(),false);
   this.m_background.Rectangle(this.AdjX(0),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG()));
   this.m_background.Update(false);
//--- Display the button text
   CLabel::Draw(false);
      
//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

First, fill the background with the set color, then draw a border and display the text of the button.

Based on this class, create a two-position button class.

The “Two-Position Button" Control Class

//+------------------------------------------------------------------+
//| Toggle button class                                              |
//+------------------------------------------------------------------+
class CButtonTriggered : public CButton
  {
public:
//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);

//--- Mouse button click event handler (Press)
   virtual void      OnPressEvent(const int id, const long lparam, const double dparam, const string sparam);

//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle)               { return CButton::Save(file_handle);      }
   virtual bool      Load(const int file_handle)               { return CButton::Load(file_handle);      }
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_BUTTON_TRIGGERED);  }
  
//--- Initialize the object default colors
   virtual void      InitColors(void);
   
//--- Constructors/destructor
                     CButtonTriggered(void);
                     CButtonTriggered(const string object_name, const string text, const int x, const int y, const int w, const int h);
                     CButtonTriggered(const string object_name, const string text, const int wnd, const int x, const int y, const int w, const int h);
                     CButtonTriggered(const string object_name, const long chart_id, const int wnd, const string text, const int x, const int y, const int w, const int h);
                    ~CButtonTriggered (void) {}
  };

The object has four constructors that allow you to create a button with the specified parameters:

//+------------------------------------------------------------------+
//| CButtonTriggered::Default constructor.                           |
//| Builds a button in the main window of the current chart          |
//| at 0,0 coordinates with default dimensions                       |
//+------------------------------------------------------------------+
CButtonTriggered::CButtonTriggered(void) : CButton("Button",::ChartID(),0,"Button",0,0,DEF_BUTTON_W,DEF_BUTTON_H)
  {
   this.InitColors();
  }
//+------------------------------------------------------------------+
//| CButtonTriggered::Parametric constructor.                        |
//| Builds a button in the main window of the current chart          |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CButtonTriggered::CButtonTriggered(const string object_name,const string text,const int x,const int y,const int w,const int h) :
   CButton(object_name,::ChartID(),0,text,x,y,w,h)
  {
   this.InitColors();
  }
//+------------------------------------------------------------------+
//| CButtonTriggered::Parametric constructor.                        |
//| Builds a button in the specified window of the current chart     |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CButtonTriggered::CButtonTriggered(const string object_name,const string text,const int wnd,const int x,const int y,const int w,const int h) :
   CButton(object_name,::ChartID(),wnd,text,x,y,w,h)
  {
   this.InitColors();
  }
//+------------------------------------------------------------------+
//| CButtonTriggered::Parametric constructor.                        |
//| Builds a button in the specified window of the specified chart   |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CButtonTriggered::CButtonTriggered(const string object_name,const long chart_id,const int wnd,const string text,const int x,const int y,const int w,const int h) :
   CButton(object_name,chart_id,wnd,text,x,y,w,h)
  {
   this.InitColors();
  }

The default color initialization method is called in each constructor:

//+------------------------------------------------------------------+
//| CButtonTriggered::Initialize the object default colors           |
//+------------------------------------------------------------------+
void CButtonTriggered::InitColors(void)
  {
//--- Initialize the background colors for the normal and activated states and make it the current background color
   this.InitBackColors(clrWhiteSmoke);
   this.InitBackColorsAct(clrLightBlue);
   this.BackColorToDefault();
   
//--- Initialize the foreground colors for the normal and activated states and make it the current text color
   this.InitForeColors(clrBlack);
   this.InitForeColorsAct(clrBlack);
   this.ForeColorToDefault();
   
//--- Initialize the border colors for the normal and activated states and make it the current border color
   this.InitBorderColors(clrDarkGray);
   this.InitBorderColorsAct(clrGreen);
   this.BorderColorToDefault();
   
//--- Initialize the border color and foreground color for the disabled element
   this.InitBorderColorBlocked(clrLightGray);
   this.InitForeColorBlocked(clrSilver);
  }

These are the default colors that are set for the newly created button. After creating an object, all the colors can be customized at your discretion.

The comparison methodhas been added with a comparison by the state of the button:

//+------------------------------------------------------------------+
//| CButtonTriggered::Compare two objects                            |
//+------------------------------------------------------------------+
int CButtonTriggered::Compare(const CObject *node,const int mode=0) const
  {
   const CButtonTriggered *obj=node;
   switch(mode)
     {
      case ELEMENT_SORT_BY_NAME  :  return(this.Name()     >obj.Name()      ? 1 : this.Name()     <obj.Name()      ? -1 : 0);
      case ELEMENT_SORT_BY_TEXT  :  return(this.Text()     >obj.Text()      ? 1 : this.Text()     <obj.Text()      ? -1 : 0);
      case ELEMENT_SORT_BY_COLOR :  return(this.BackColor()>obj.BackColor() ? 1 : this.BackColor()<obj.BackColor() ? -1 : 0);
      case ELEMENT_SORT_BY_ALPHA :  return(this.AlphaBG()  >obj.AlphaBG()   ? 1 : this.AlphaBG()  <obj.AlphaBG()   ? -1 : 0);
      case ELEMENT_SORT_BY_STATE :  return(this.State()    >obj.State()     ? 1 : this.State()    <obj.State()     ? -1 : 0);
      default                    :  return(this.ID()       >obj.ID()        ? 1 : this.ID()       <obj.ID()        ? -1 : 0);
     }
  }

A Method That Draws the Appearance of the Button:

//+------------------------------------------------------------------+
//| CButtonTriggered::Draw the appearance                            |
//+------------------------------------------------------------------+
void CButtonTriggered::Draw(const bool chart_redraw)
  {
//--- Fill the button with the background color, draw the frame and update the background canvas
   this.Fill(this.BackColor(),false);
   this.m_background.Rectangle(this.AdjX(0),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG()));
   this.m_background.Update(false);
//--- Display the button text
   CLabel::Draw(false);
      
//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

The method is identical to that of the parent class, and if there are no further improvements to the class, then the method can be deleted — the drawing method from the parent class will be used.

The two-position button has two states:

  1. Pressed,
  2. Released.

To track and switch its states, the mouse click handler OnPressEvent of the parent class has been redefined here:

//+------------------------------------------------------------------+
//| CButtonTriggered::Mouse button click event handler (Press)       |
//+------------------------------------------------------------------+
void CButtonTriggered::OnPressEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Set the button state to the opposite of the one already set
   ENUM_ELEMENT_STATE state=(this.State()==ELEMENT_STATE_DEF ? ELEMENT_STATE_ACT : ELEMENT_STATE_DEF);
   this.SetState(state);
   
//--- Call the parent object handler with the ID in lparam and the state in dparam
   CCanvasBase::OnPressEvent(id,this.m_id,this.m_state,sparam);
  }

Based on the CButton class, create four arrow buttons: up, down, left and right. The objects will use the image drawing class to draw arrows.

“Up Arrow Button” Control Class

//+------------------------------------------------------------------+
//| Up arrow button class                                            |
//+------------------------------------------------------------------+
class CButtonArrowUp : public CButton
  {
public:
//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);

//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle)               { return CButton::Save(file_handle);   }
   virtual bool      Load(const int file_handle)               { return CButton::Load(file_handle);   }
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_BUTTON_ARROW_UP);}
   
//--- Constructors/destructor
                     CButtonArrowUp(void);
                     CButtonArrowUp(const string object_name, const int x, const int y, const int w, const int h);
                     CButtonArrowUp(const string object_name, const int wnd, const int x, const int y, const int w, const int h);
                     CButtonArrowUp(const string object_name, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CButtonArrowUp (void) {}
  };

Four constructors allow you to create an object with the specified parameters:

//+------------------------------------------------------------------+
//| CButtonArrowUp::Default constructor.                             |
//| Builds a button in the main window of the current chart          |
//| at 0,0 coordinates with default dimensions                       |
//+------------------------------------------------------------------+
CButtonArrowUp::CButtonArrowUp(void) : CButton("Arrow Up Button",::ChartID(),0,"",0,0,DEF_BUTTON_W,DEF_BUTTON_H)
  {
   this.InitColors();
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CButtonArrowUp::Parametric constructor.                          |
//| Builds a button in the main window of the current chart          |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CButtonArrowUp::CButtonArrowUp(const string object_name,const int x,const int y,const int w,const int h) :
   CButton(object_name,::ChartID(),0,"",x,y,w,h)
  {
   this.InitColors();
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CButtonArrowUp::Parametric constructor.                          |
//| Builds a button in the specified window of the current chart     |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CButtonArrowUp::CButtonArrowUp(const string object_name,const int wnd,const int x,const int y,const int w,const int h) :
   CButton(object_name,::ChartID(),wnd,"",x,y,w,h)
  {
   this.InitColors();
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CButtonArrowUp::Parametric constructor.                          |
//| Builds a button in the specified window of the specified chart   |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CButtonArrowUp::CButtonArrowUp(const string object_name,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) :
   CButton(object_name,chart_id,wnd,"",x,y,w,h)
  {
   this.InitColors();
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }

Default colors are initialized in constructors and the coordinates as well as dimensions of the image area are set.

A Method That Draws the Appearance of the Button:

//+------------------------------------------------------------------+
//| CButtonArrowUp::Draw the appearance                              |
//+------------------------------------------------------------------+
void CButtonArrowUp::Draw(const bool chart_redraw)
  {
//--- Fill the button with the background color, draw the frame and update the background canvas
   this.Fill(this.BackColor(),false);
   this.m_background.Rectangle(this.AdjX(0),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG()));
   this.m_background.Update(false);
//--- Display the button text
   CLabel::Draw(false);
//--- Clear the drawing area
   this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
//--- Set the arrow color for the normal and disabled states of the button and draw the up arrow 
   color clr=(!this.IsBlocked() ? this.GetForeColorControl().NewColor(this.ForeColor(),90,90,90) : this.ForeColor());
   this.m_painter.ArrowUp(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),clr,this.AlphaFG(),true);
      
//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

The method is similar to that of drawing a button, but additionally an up arrow is displayed using the ArrowUp method of the drawing object.

All other classes are identical to the one considered, but drawing methods use icons corresponding to the purpose of the button.

“Down Arrow Button” Control Class

//+------------------------------------------------------------------+
//| Down arrow button class                                          |
//+------------------------------------------------------------------+
class CButtonArrowDown : public CButton
  {
public:
//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);

//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle)               { return CButton::Save(file_handle);      }
   virtual bool      Load(const int file_handle)               { return CButton::Load(file_handle);      }
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_BUTTON_ARROW_DOWN); }
   
//--- Constructors/destructor
                     CButtonArrowDown(void);
                     CButtonArrowDown(const string object_name, const int x, const int y, const int w, const int h);
                     CButtonArrowDown(const string object_name, const int wnd, const int x, const int y, const int w, const int h);
                     CButtonArrowDown(const string object_name, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CButtonArrowDown (void) {}
  };
//+------------------------------------------------------------------+
//| CButtonArrowDown::Default constructor.                           |
//| Builds a button in the main window of the current chart          |
//| at 0,0 coordinates with default dimensions                       |
//+------------------------------------------------------------------+
CButtonArrowDown::CButtonArrowDown(void) : CButton("Arrow Up Button",::ChartID(),0,"",0,0,DEF_BUTTON_W,DEF_BUTTON_H)
  {
   this.InitColors();
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CButtonArrowDown::Parametric constructor.                        |
//| Builds a button in the main window of the current chart          |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CButtonArrowDown::CButtonArrowDown(const string object_name,const int x,const int y,const int w,const int h) :
   CButton(object_name,::ChartID(),0,"",x,y,w,h)
  {
   this.InitColors();
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CButtonArrowDown::Parametric constructor.                        |
//| Builds a button in the specified window of the current chart     |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CButtonArrowDown::CButtonArrowDown(const string object_name,const int wnd,const int x,const int y,const int w,const int h) :
   CButton(object_name,::ChartID(),wnd,"",x,y,w,h)
  {
   this.InitColors();
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CButtonArrowDown::Parametric constructor.                        |
//| Builds a button in the specified window of the specified chart   |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CButtonArrowDown::CButtonArrowDown(const string object_name,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) :
   CButton(object_name,chart_id,wnd,"",x,y,w,h)
  {
   this.InitColors();
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CButtonArrowDown::Draw the appearance                            |
//+------------------------------------------------------------------+
void CButtonArrowDown::Draw(const bool chart_redraw)
  {
//--- Fill the button with the background color, draw the frame and update the background canvas
   this.Fill(this.BackColor(),false);
   this.m_background.Rectangle(this.AdjX(0),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG()));
   this.m_background.Update(false);
//--- Display the button text
   CLabel::Draw(false);
//--- Clear the drawing area
   this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
//--- Set the arrow color for the normal and disabled states of the button and draw the down arrow
   color clr=(!this.IsBlocked() ? this.GetForeColorControl().NewColor(this.ForeColor(),90,90,90) : this.ForeColor());
   this.m_painter.ArrowDown(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),clr,this.AlphaFG(),true);
      
//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

“Left Arrow Button” Control Class

//+------------------------------------------------------------------+
//| Left arrow button class                                          |
//+------------------------------------------------------------------+
class CButtonArrowLeft : public CButton
  {
public:
//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);

//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle)               { return CButton::Save(file_handle);      }
   virtual bool      Load(const int file_handle)               { return CButton::Load(file_handle);      }
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_BUTTON_ARROW_DOWN); }
   
//--- Constructors/destructor
                     CButtonArrowLeft(void);
                     CButtonArrowLeft(const string object_name, const int x, const int y, const int w, const int h);
                     CButtonArrowLeft(const string object_name, const int wnd, const int x, const int y, const int w, const int h);
                     CButtonArrowLeft(const string object_name, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CButtonArrowLeft (void) {}
  };
//+------------------------------------------------------------------+
//| CButtonArrowLeft::Default constructor.                           |
//| Builds a button in the main window of the current chart          |
//| at 0,0 coordinates with default dimensions                       |
//+------------------------------------------------------------------+
CButtonArrowLeft::CButtonArrowLeft(void) : CButton("Arrow Up Button",::ChartID(),0,"",0,0,DEF_BUTTON_W,DEF_BUTTON_H)
  {
   this.InitColors();
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CButtonArrowLeft::Parametric constructor.                        |
//| Builds a button in the main window of the current chart          |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CButtonArrowLeft::CButtonArrowLeft(const string object_name,const int x,const int y,const int w,const int h) :
   CButton(object_name,::ChartID(),0,"",x,y,w,h)
  {
   this.InitColors();
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CButtonArrowLeft::Parametric constructor.                        |
//| Builds a button in the specified window of the current chart     |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CButtonArrowLeft::CButtonArrowLeft(const string object_name,const int wnd,const int x,const int y,const int w,const int h) :
   CButton(object_name,::ChartID(),wnd,"",x,y,w,h)
  {
   this.InitColors();
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CButtonArrowLeft::Parametric constructor.                        |
//| Builds a button in the specified window of the specified chart   |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CButtonArrowLeft::CButtonArrowLeft(const string object_name,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) :
   CButton(object_name,chart_id,wnd,"",x,y,w,h)
  {
   this.InitColors();
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CButtonArrowLeft::Draw the appearance                            |
//+------------------------------------------------------------------+
void CButtonArrowLeft::Draw(const bool chart_redraw)
  {
//--- Fill the button with the background color, draw the frame and update the background canvas
   this.Fill(this.BackColor(),false);
   this.m_background.Rectangle(this.AdjX(0),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG()));
   this.m_background.Update(false);
//--- Display the button text
   CLabel::Draw(false);
//--- Clear the drawing area
   this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
//--- Set the arrow color for the normal and disabled states of the button and draw the left arrow
   color clr=(!this.IsBlocked() ? this.GetForeColorControl().NewColor(this.ForeColor(),90,90,90) : this.ForeColor());
   this.m_painter.ArrowLeft(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),clr,this.AlphaFG(),true);
      
//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

“Right Arrow Button” Control Class

//+------------------------------------------------------------------+
//| Right arrow button class                                         |
//+------------------------------------------------------------------+
class CButtonArrowRight : public CButton
  {
public:
//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);

//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle)               { return CButton::Save(file_handle);      }
   virtual bool      Load(const int file_handle)               { return CButton::Load(file_handle);      }
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_BUTTON_ARROW_DOWN); }
   
//--- Constructors/destructor
                     CButtonArrowRight(void);
                     CButtonArrowRight(const string object_name, const int x, const int y, const int w, const int h);
                     CButtonArrowRight(const string object_name, const int wnd, const int x, const int y, const int w, const int h);
                     CButtonArrowRight(const string object_name, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CButtonArrowRight (void) {}
  };
//+------------------------------------------------------------------+
//| CButtonArrowRight::Default constructor.                          |
//| Builds a button in the main window of the current chart          |
//| at 0,0 coordinates with default dimensions                       |
//+------------------------------------------------------------------+
CButtonArrowRight::CButtonArrowRight(void) : CButton("Arrow Up Button",::ChartID(),0,"",0,0,DEF_BUTTON_W,DEF_BUTTON_H)
  {
   this.InitColors();
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CButtonArrowRight::Parametric constructor.                       |
//| Builds a button in the main window of the current chart          |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CButtonArrowRight::CButtonArrowRight(const string object_name,const int x,const int y,const int w,const int h) :
   CButton(object_name,::ChartID(),0,"",x,y,w,h)
  {
   this.InitColors();
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CButtonArrowRight::Parametric constructor.                       |
//| Builds a button in the specified window of the current chart     |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CButtonArrowRight::CButtonArrowRight(const string object_name,const int wnd,const int x,const int y,const int w,const int h) :
   CButton(object_name,::ChartID(),wnd,"",x,y,w,h)
  {
   this.InitColors();
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CButtonArrowRight::Parametric constructor.                       |
//| Builds a button in the specified window of the specified chart   |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CButtonArrowRight::CButtonArrowRight(const string object_name,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) :
   CButton(object_name,chart_id,wnd,"",x,y,w,h)
  {
   this.InitColors();
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CButtonArrowRight::Draw the appearance                           |
//+------------------------------------------------------------------+
void CButtonArrowRight::Draw(const bool chart_redraw)
  {
//--- Fill the button with the background color, draw the frame and update the background canvas
   this.Fill(this.BackColor(),false);
   this.m_background.Rectangle(this.AdjX(0),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG()));
   this.m_background.Update(false);
//--- Display the button text
   CLabel::Draw(false);
//--- Clear the drawing area
   this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
//--- Set the arrow color for the normal and disabled states of the button and draw the right arrow
   color clr=(!this.IsBlocked() ? this.GetForeColorControl().NewColor(this.ForeColor(),90,90,90) : this.ForeColor());
   this.m_painter.ArrowRight(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),clr,this.AlphaFG(),true);
      
//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

The “Checkbox" Control Class

The "checkbox" control class is similar to the classes of arrow buttons. Here, the background will be completely transparent. I.e., only the text and the checkbox icon will be drawn. The checkbox has two states — checked and unchecked, which means it will be inherited from the two-position button class:

//+------------------------------------------------------------------+
//| Checkbox control class                                           |
//+------------------------------------------------------------------+
class CCheckBox : public CButtonTriggered
  {
public:
//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);

//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle)               { return CButton::Save(file_handle);   }
   virtual bool      Load(const int file_handle)               { return CButton::Load(file_handle);   }
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_CHECKBOX);       }
  
//--- Initialize the object default colors
   virtual void      InitColors(void);
   
//--- Constructors/destructor
                     CCheckBox(void);
                     CCheckBox(const string object_name, const string text, const int x, const int y, const int w, const int h);
                     CCheckBox(const string object_name, const string text, const int wnd, const int x, const int y, const int w, const int h);
                     CCheckBox(const string object_name, const long chart_id, const int wnd, const string text, const int x, const int y, const int w, const int h);
                    ~CCheckBox (void) {}
  };

All classes of controls have four constructors each:

//+------------------------------------------------------------------+
//| CCheckBox::Default constructor.                                  |
//| Builds a button in the main window of the current chart          |
//| at 0,0 coordinates with default dimensions                       |
//+------------------------------------------------------------------+
CCheckBox::CCheckBox(void) : CButtonTriggered("CheckBox",::ChartID(),0,"CheckBox",0,0,DEF_BUTTON_W,DEF_BUTTON_H)
  {
//--- Set default colors, background and foreground transparency,
//--- as well as coordinates and boundaries of the button icon image area
   this.InitColors();
   this.SetAlphaBG(0);
   this.SetAlphaFG(255);
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CCheckBox::Parametric constructor.                               |
//| Builds a button in the main window of the current chart          |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CCheckBox::CCheckBox(const string object_name,const string text,const int x,const int y,const int w,const int h) :
   CButtonTriggered(object_name,::ChartID(),0,text,x,y,w,h)
  {
//--- Set default colors, background and foreground transparency,
//--- as well as coordinates and boundaries of the button icon image area
   this.InitColors();
   this.SetAlphaBG(0);
   this.SetAlphaFG(255);
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CCheckBox::Parametric constructor.                               |
//| Builds a button in the specified window of the current chart     |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CCheckBox::CCheckBox(const string object_name,const string text,const int wnd,const int x,const int y,const int w,const int h) :
   CButtonTriggered(object_name,::ChartID(),wnd,text,x,y,w,h)
  {
//--- Set default colors, background and foreground transparency,
//--- as well as coordinates and boundaries of the button icon image area
   this.InitColors();
   this.SetAlphaBG(0);
   this.SetAlphaFG(255);
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CCheckBox::Parametric constructor.                               |
//| Builds a button in the specified window of the specified chart   |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CCheckBox::CCheckBox(const string object_name,const long chart_id,const int wnd,const string text,const int x,const int y,const int w,const int h) :
   CButtonTriggered(object_name,chart_id,wnd,text,x,y,w,h)
  {
//--- Set default colors, background and foreground transparency,
//--- as well as coordinates and boundaries of the button icon image area
   this.InitColors();
   this.SetAlphaBG(0);
   this.SetAlphaFG(255);
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }

Here, the default object colors are initialized, a fully transparent background and an opaque foreground are set. Then dimensions and coordinates of the image area are set.

The comparison method returns the result of calling the comparison method of the parent class:

//+------------------------------------------------------------------+
//| CCheckBox::Compare two objects                                   |
//+------------------------------------------------------------------+
int CCheckBox::Compare(const CObject *node,const int mode=0) const
  {
   return CButtonTriggered::Compare(node,mode);
  }

Default Object Color Initialization Method:

//+------------------------------------------------------------------+
//| CCheckBox::Initialize the object default colors                  |
//+------------------------------------------------------------------+
void CCheckBox::InitColors(void)
  {
//--- Initialize the background colors for the normal and activated states and make it the current background color
   this.InitBackColors(clrNULL);
   this.InitBackColorsAct(clrNULL);
   this.BackColorToDefault();
   
//--- Initialize the foreground colors for the normal and activated states and make it the current text color
   this.InitForeColors(clrBlack);
   this.InitForeColorsAct(clrBlack);
   this.InitForeColorFocused(clrNavy);
   this.InitForeColorActFocused(clrNavy);
   this.ForeColorToDefault();
   
//--- Initialize the border colors for the normal and activated states and make it the current border color
   this.InitBorderColors(clrNULL);
   this.InitBorderColorsAct(clrNULL);
   this.BorderColorToDefault();

//--- Initialize the border color and foreground color for the disabled element
   this.InitBorderColorBlocked(clrNULL);
   this.InitForeColorBlocked(clrSilver);
  }

The Method of Drawing the Checkbox Appearance:

//+------------------------------------------------------------------+
//| CCheckBox::Draw the appearance                                   |
//+------------------------------------------------------------------+
void CCheckBox::Draw(const bool chart_redraw)
  {
//--- Fill the button with the background color, draw the frame and update the background canvas
   this.Fill(this.BackColor(),false);
   this.m_background.Rectangle(this.AdjX(0),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG()));
   this.m_background.Update(false);
//--- Display the button text
   CLabel::Draw(false);
   
//--- Clear the drawing area
   this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
//--- Draw the checked icon for the active state of the button,
   if(this.m_state)
      this.m_painter.CheckedBox(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),this.ForeColor(),this.AlphaFG(),true);
//--- and unchecked - for inactive state
   else
      this.m_painter.UncheckedBox(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),this.ForeColor(),this.AlphaFG(),true);
      
//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Depending on the element state, either a square checked with a check mark or just an empty square is drawn.

Now, based on this object, create a “Radio button" control class.

The “Radio Button" Control Class

Since the radio button always works in a group — it can only be turned off when another group button is turned on, here we also should redefine the handler for clicking on an object of the parent class.

//+------------------------------------------------------------------+
//| Radio Button control class                                       |
//+------------------------------------------------------------------+
class CRadioButton : public CCheckBox
  {
public:
//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);

//--- Mouse button click event handler (Press)
   virtual void      OnPressEvent(const int id, const long lparam, const double dparam, const string sparam);

//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle)               { return CButton::Save(file_handle);   }
   virtual bool      Load(const int file_handle)               { return CButton::Load(file_handle);   }
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_RADIOBUTTON);    }
  
//--- Constructors/destructor
                     CRadioButton(void);
                     CRadioButton(const string object_name, const string text, const int x, const int y, const int w, const int h);
                     CRadioButton(const string object_name, const string text, const int wnd, const int x, const int y, const int w, const int h);
                     CRadioButton(const string object_name, const long chart_id, const int wnd, const string text, const int x, const int y, const int w, const int h);
                    ~CRadioButton (void) {}
  };

Constructors:

//+------------------------------------------------------------------+
//| CRadioButton::Default constructor.                               |
//| Builds a button in the main window of the current chart          |
//| at 0,0 coordinates with default dimensions                       |
//+------------------------------------------------------------------+
CRadioButton::CRadioButton(void) : CCheckBox("RadioButton",::ChartID(),0,"",0,0,DEF_BUTTON_H,DEF_BUTTON_H)
  {
  }
//+------------------------------------------------------------------+
//| CRadioButton::Parametric constructor.                            |
//| Builds a button in the main window of the current chart          |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CRadioButton::CRadioButton(const string object_name,const string text,const int x,const int y,const int w,const int h) :
   CCheckBox(object_name,::ChartID(),0,text,x,y,w,h)
  {
  }
//+------------------------------------------------------------------+
//| CRadioButton::Parametric constructor.                            |
//| Builds a button in the specified window of the current chart     |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CRadioButton::CRadioButton(const string object_name,const string text,const int wnd,const int x,const int y,const int w,const int h) :
   CCheckBox(object_name,::ChartID(),wnd,text,x,y,w,h)
  {
  }
//+------------------------------------------------------------------+
//| CRadioButton::Parametric constructor.                            |
//| Builds a button in the specified window of the specified chart   |
//| with the specified text, coordinates and dimensions              |
//+------------------------------------------------------------------+
CRadioButton::CRadioButton(const string object_name,const long chart_id,const int wnd,const string text,const int x,const int y,const int w,const int h) :
   CCheckBox(object_name,chart_id,wnd,text,x,y,w,h)
  {
  }

There is no need for any additional actions after calling the constructor of the parent class. Therefore, constructors have an empty body.

The comparison method returns the result of calling the comparison method of the parent class:

//+------------------------------------------------------------------+
//| CRadioButton::Compare two objects                                |
//+------------------------------------------------------------------+
int CRadioButton::Compare(const CObject *node,const int mode=0) const
  {
   return CCheckBox::Compare(node,mode);
  }

The Method of Drawing the Button Appearance:

//+------------------------------------------------------------------+
//| CRadioButton::Draw the appearance                                |
//+------------------------------------------------------------------+
void CRadioButton::Draw(const bool chart_redraw)
  {
//--- Fill the button with the background color, draw the frame and update the background canvas
   this.Fill(this.BackColor(),false);
   this.m_background.Rectangle(this.AdjX(0),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG()));
   this.m_background.Update(false);
//--- Display the button text
   CLabel::Draw(false);
   
//--- Clear the drawing area
   this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
//--- Draw the checked icon for the active state of the button,
   if(this.m_state)
      this.m_painter.CheckedRadioButton(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),this.ForeColor(),this.AlphaFG(),true);
//--- and unchecked - for inactive state
   else
      this.m_painter.UncheckedRadioButton(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),this.ForeColor(),this.AlphaFG(),true);
      
//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

This method is identical to the method of the parent class, but the radio button icons are drawn here — selected and not selected.

Mouse Button Click Event Handler:

//+------------------------------------------------------------------+
//| CRadioButton::Mouse button click event handler (Press)           |
//+------------------------------------------------------------------+
void CRadioButton::OnPressEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- If the button is already checked, leave
   if(this.m_state)
      return;
//--- Set the button state to the opposite of the one already set
   ENUM_ELEMENT_STATE state=(this.State()==ELEMENT_STATE_DEF ? ELEMENT_STATE_ACT : ELEMENT_STATE_DEF);
   this.SetState(state);
   
//--- Call the parent object handler with the ID in lparam and the state in dparam
   CCanvasBase::OnPressEvent(id,this.m_id,this.m_state,sparam);
  }

Here, if the button already has an enabled state, then no actions should be performed — leave the handler. If the button is disabled, invert its state and call the handler of the parent class (the base object of all CCanvasBase controls).

For today, these are all the controls that were minimally necessary to implement complex controls.
Let's test what we get here.


Testing the Result

In \MQL5\Indicators\Tables\ folder reate a new indicator named iTestLabel.mq5.

Set the number of calculated buffers and graphical series of the indicator to zero — no charts need to be drawn. Connect the created library of graphical elements. In its own separate window the indicator will draw graphical elements, which, when created, will be saved to a list, the class file of which is connected to the indicator file:

//+------------------------------------------------------------------+
//|                                                   iTestLabel.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 0
#property indicator_plots   0

//+------------------------------------------------------------------+
//| Include libraries                                                |
//+------------------------------------------------------------------+
#include <Arrays\ArrayObj.mqh>
#include "Controls\Controls.mqh"
  
CArrayObj         list;             // List for storing tested objects
CCanvasBase      *base =NULL;       // Pointer to the base graphical element
CLabel           *label1=NULL;      // Pointer to the Label graphical element
CLabel           *label2=NULL;      // Pointer to the Label graphical element
CLabel           *label3=NULL;      // Pointer to the Label graphical element
CButton          *button1=NULL;     // Pointer to the Button graphical element
CButtonTriggered *button_t1=NULL;   // Pointer to the ButtonTriggered graphical element
CButtonTriggered *button_t2=NULL;   // Pointer to the ButtonTriggered graphical element
CButtonArrowUp   *button_up=NULL;   // Pointer to the CButtonArrowUp graphical element
CButtonArrowDown *button_dn=NULL;   // Pointer to the CButtonArrowDown graphical element
CButtonArrowLeft *button_lt=NULL;   // Pointer to the CButtonArrowLeft graphical element
CButtonArrowRight*button_rt=NULL;   // Pointer to the CButtonArrowRight graphical element
CCheckBox        *checkbox_lt=NULL; // Pointer to the CCheckBox graphical element
CCheckBox        *checkbox_rt=NULL; // Pointer to the CCheckBox graphical element
CRadioButton     *radio_bt_lt=NULL; // Pointer to the CRadioButton graphical element
CRadioButton     *radio_bt_rt=NULL; // Pointer to the CRadioButton graphical element

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+

Here, for simplification, pointers to the created graphical elements are immediately created. After creating the element, we will use these pointers to work with objects.

Create all the objects indicator’s handler OnInit(). Let's do this: create one base object and color it so that it resembles a panel.

Inside this "substrate" implement all the graphical elements and specify this base object for them as a container.

In OnInit() implement such a code:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Search for the chart subwindow
   int wnd=ChartWindowFind();
//--- Create a basic graphical element
   list.Add(base=new CCanvasBase("Rectangle",0,wnd,100,40,260,160));
   base.SetAlphaBG(250);      // Transparency
   base.SetBorderWidth(6);    // Border width
   
//--- Initialize the background color, specify the color for the blocked element
//--- and set the default background color of the element as the current color 
   base.InitBackColors(clrWhiteSmoke);
   base.InitBackColorBlocked(clrLightGray);
   base.BackColorToDefault();
   
//--- Fill the background with color and draw a frame with an indent of one pixel from the set frame width
   base.Fill(base.BackColor(),false);
   uint wd=base.BorderWidth();
   base.GetBackground().Rectangle(0,0,base.Width()-1,base.Height()-1,ColorToARGB(clrDimGray));
   base.GetBackground().Rectangle(wd-2,wd-2,base.Width()-wd+1,base.Height()-wd+1,ColorToARGB(clrLightGray));
   base.Update(false);
//--- set the name and ID of the element and display its description in the journal
   base.SetName("Rectangle 1");
   base.SetID(1);
   base.Print();
   

//--- Create a text label inside the base object
//--- and specify the base element as a container for the label
   string text="Simple button:";
   int shift_x=20;
   int shift_y=8;
   int x=base.X()+shift_x-10;
   int y=base.Y()+shift_y+2;
   int w=base.GetForeground().TextWidth(text);
   int h=DEF_LABEL_H;
   list.Add(label1=new CLabel("Label 1",0,wnd,text,x,y,w,h));
   label1.SetContainerObj(base);
//--- Set the hover and click color to red
//--- (this is a change to the standard parameters of a text label after its creation).
   label1.InitForeColorFocused(clrRed);   
   label1.InitForeColorPressed(clrRed);
//--- Set the element ID, draw the element
//--- and display its description to the journal.
   label1.SetID(2);
   label1.Draw(false);
   label1.Print();
   
   
//--- Create a simple button inside the base object
//--- and specify the base element as a button container
   x=label1.Right()+shift_x;
   y=label1.Y();
   w=DEF_BUTTON_W;
   h=DEF_BUTTON_H;
   list.Add(button1=new CButton("Simple Button",0,wnd,"Button 1",x,y,w,h));
   button1.SetContainerObj(base);
//--- Set the button text offset along the X axis
   button1.SetTextShiftH(2);
//--- Set the element ID, draw the element
//--- and display its description to the journal.
   button1.SetID(3);
   button1.Draw(false);
   button1.Print();
   
   
//--- Create a text label inside the base object
//--- and specify the base element as a container for the label
   text="Triggered button:";
   x=label1.X();
   y=label1.Bottom()+shift_y;
   w=base.GetForeground().TextWidth(text);
   h=DEF_LABEL_H;
   list.Add(label2=new CLabel("Label 2",0,wnd,text,x,y,w,h));
   label2.SetContainerObj(base);
//--- Set the hover and click color to red
//--- (this is a change to the standard parameters of a text label after its creation).
   label2.InitForeColorFocused(clrRed);
   label2.InitForeColorPressed(clrRed);
//--- Set the element ID, draw the element
//--- and display its description to the journal.
   label2.SetID(4);
   label2.Draw(false);
   label2.Print();
   
   
//--- Create the toggle button inside the base object
//--- and specify the base element as a button container
   x=button1.X();
   y=button1.Bottom()+shift_y;
   w=DEF_BUTTON_W;
   h=DEF_BUTTON_H;
   list.Add(button_t1=new CButtonTriggered("Triggered Button 1",0,wnd,"Button 2",x,y,w,h));
   button_t1.SetContainerObj(base);

//--- Set the button text offset along the X axis
   button_t1.SetTextShiftH(2);
//--- Set the ID and activated state of the element,
//--- draw the element and display its description to the journal.
   button_t1.SetID(5);
   button_t1.SetState(true);
   button_t1.Draw(false);
   button_t1.Print();
   
   
//--- Create the toggle button inside the base object
//--- and specify the base element as a button container
   x=button_t1.Right()+4;
   y=button_t1.Y();
   w=DEF_BUTTON_W;
   h=DEF_BUTTON_H;
   list.Add(button_t2=new CButtonTriggered("Triggered Button 2",0,wnd,"Button 3",x,y,w,h));
   button_t2.SetContainerObj(base);

//--- Set the button text offset along the X axis
   button_t2.SetTextShiftH(2);
//--- Set the element ID, draw the element
//--- and display its description to the journal.
   button_t2.SetID(6);
   button_t2.Draw(false);
   button_t2.Print();
   
   
//--- Create a text label inside the base object
//--- and specify the base element as a container for the label
   text="Arrowed buttons:";
   x=label1.X();
   y=label2.Bottom()+shift_y;
   w=base.GetForeground().TextWidth(text);
   h=DEF_LABEL_H;
   list.Add(label3=new CLabel("Label 3",0,wnd,text,x,y,w,h));
   label3.SetContainerObj(base);
//--- Set the hover and click color to red
//--- (this is a change to the standard parameters of a text label after its creation).
   label3.InitForeColorFocused(clrRed);
   label3.InitForeColorPressed(clrRed);
//--- Set the element ID, draw the element
//--- and display its description to the journal.
   label3.SetID(7);
   label3.Draw(false);
   label3.Print();
   
   
//--- Create the up arrow button inside the base object
//--- and specify the base element as a button container
   x=button1.X();
   y=button_t1.Bottom()+shift_y;
   w=DEF_BUTTON_H-1;
   h=DEF_BUTTON_H-1;
   list.Add(button_up=new CButtonArrowUp("Arrow Up Button",0,wnd,x,y,w,h));
   button_up.SetContainerObj(base);
//--- Set the image size and offset along the X axis
   button_up.SetImageBound(1,1,w-4,h-3);
//--- Here we can customize the appearance of the button, for example, remove the border
   //button_up.InitBorderColors(button_up.BackColor(),button_up.BackColorFocused(),button_up.BackColorPressed(),button_up.BackColorBlocked());
   //button_up.ColorsToDefault();
//--- Set the element ID, draw the element
//--- and display its description to the journal.
   button_up.SetID(8);
   button_up.Draw(false);
   button_up.Print();
   
   
//--- Create the down arrow button inside the base object
//--- and specify the base element as a button container
   x=button_up.Right()+4;
   y=button_up.Y();
   w=DEF_BUTTON_H-1;
   h=DEF_BUTTON_H-1;
   list.Add(button_dn=new CButtonArrowDown("Arrow Down Button",0,wnd,x,y,w,h));
   button_dn.SetContainerObj(base);
//--- Set the image size and offset along the X axis
   button_dn.SetImageBound(1,1,w-4,h-3);
//--- Set the element ID, draw the element
//--- and display its description to the journal.
   button_dn.SetID(9);
   button_dn.Draw(false);
   button_dn.Print();
   
   
//--- Create the left arrow button inside the base object
//--- and specify the base element as a button container
   x=button_dn.Right()+4;
   y=button_up.Y();
   w=DEF_BUTTON_H-1;
   h=DEF_BUTTON_H-1;
   list.Add(button_lt=new CButtonArrowLeft("Arrow Left Button",0,wnd,x,y,w,h));
   button_lt.SetContainerObj(base);
//--- Set the image size and offset along the X axis
   button_lt.SetImageBound(1,1,w-3,h-4);
//--- Set the element ID, draw the element
//--- and display its description to the journal.
   button_lt.SetID(10);
   button_lt.Draw(false);
   button_lt.Print();
   
   
//--- Create the right arrow button inside the base object
//--- and specify the base element as a button container
   x=button_lt.Right()+4;
   y=button_up.Y();
   w=DEF_BUTTON_H-1;
   h=DEF_BUTTON_H-1;
   list.Add(button_rt=new CButtonArrowRight("Arrow Right Button",0,wnd,x,y,w,h));
   button_rt.SetContainerObj(base);
//--- Set the image size and offset along the X axis
   button_rt.SetImageBound(1,1,w-3,h-4);
//--- Set the element ID, draw the element
//--- and display its description to the journal.
   button_rt.SetID(11);
   button_rt.Draw(false);
   button_rt.Print();
   
   
//--- Inside the base object, create a checkbox with a header on the right (left checkbox)
//--- and specify the base element as a button container
   x=label1.X();
   y=label3.Bottom()+shift_y;
   w=DEF_BUTTON_W+30;
   h=DEF_BUTTON_H;
   list.Add(checkbox_lt=new CCheckBox("CheckBoxL",0,wnd,"CheckBox L",x,y,w,h));
   checkbox_lt.SetContainerObj(base);
//--- Set the area coordinates and image area dimensions
   checkbox_lt.SetImageBound(2,1,h-2,h-2);
//--- Set the button text offset along the X axis
   checkbox_lt.SetTextShiftH(checkbox_lt.ImageRight()+2);
//--- Set the element ID, draw the element
//--- and display its description to the journal.
   checkbox_lt.SetID(12);
   checkbox_lt.Draw(false);
   checkbox_lt.Print();
   
   
//--- Inside the base object, create a checkbox with a header on the left (right checkbox)
//--- and specify the base element as a button container
   x=checkbox_lt.Right()+4;
   y=checkbox_lt.Y();
   w=DEF_BUTTON_W+30;
   h=DEF_BUTTON_H;
   list.Add(checkbox_rt=new CCheckBox("CheckBoxR",0,wnd,"CheckBox R",x,y,w,h));
   checkbox_rt.SetContainerObj(base);
//--- Set the area coordinates and image area dimensions
   checkbox_rt.SetTextShiftH(2);
//--- Set the button text offset along the X axis
   checkbox_rt.SetImageBound(checkbox_rt.Width()-h+2,1,h-2,h-2);
//--- Set the ID and activated state of the element,
//--- draw the element and display its description to the journal.
   checkbox_rt.SetID(13);
   checkbox_rt.SetState(true);
   checkbox_rt.Draw(false);
   checkbox_rt.Print();
   
   
//--- Inside the base object, create a radio button with a header on the right (left RadioButton)
//--- and specify the base element as a button container
   x=checkbox_lt.X();
   y=checkbox_lt.Bottom()+shift_y;
   w=DEF_BUTTON_W+46;
   h=DEF_BUTTON_H;
   list.Add(radio_bt_lt=new CRadioButton("RadioButtonL",0,wnd,"RadioButton L",x,y,w,h));
   radio_bt_lt.SetContainerObj(base);
//--- Set the area coordinates and image area dimensions
   radio_bt_lt.SetImageBound(2,1,h-2,h-2);
//--- Set the button text offset along the X axis
   radio_bt_lt.SetTextShiftH(radio_bt_lt.ImageRight()+2);
//--- Set the ID and activated state of the element,
//--- draw the element and display its description to the journal.
   radio_bt_lt.SetID(14);
   radio_bt_lt.SetState(true);
   radio_bt_lt.Draw(false);
   radio_bt_lt.Print();
   
   
//--- Inside the base object, create a radio button with a header on the left (right RadioButton)
//--- and specify the base element as a button container
   x=radio_bt_lt.Right()+4;
   y=radio_bt_lt.Y();
   w=DEF_BUTTON_W+46;
   h=DEF_BUTTON_H;
   list.Add(radio_bt_rt=new CRadioButton("RadioButtonR",0,wnd,"RadioButton R",x,y,w,h));
   radio_bt_rt.SetContainerObj(base);
//--- Set the button text offset along the X axis
   radio_bt_rt.SetTextShiftH(2);
//--- Set the area coordinates and image area dimensions
   radio_bt_rt.SetImageBound(radio_bt_rt.Width()-h+2,1,h-2,h-2);
//--- Set the element ID, draw the element
//--- and display its description to the journal.
   radio_bt_rt.SetID(15);
   radio_bt_rt.Draw(true);
   radio_bt_rt.Print();

//--- Successful initialization
   return(INIT_SUCCEEDED);
  }

Carefully study all the comments to the code. Here all the steps of creating objects are described in sufficient detail.

In indicator’s handler OnDeinit() destroy all the objects in the list:

//+------------------------------------------------------------------+
//| Custom deindicator initialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   list.Clear();
  }

The OnCalculate() handler is empty — we are not calculating or displaying anything on the chart:

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//--- return value of prev_calculated for the next call
   return(rates_total);
  }

To animate the created graphical elements, go through the list of created objects in the OnChartEvent() handler and call the similar handler for each element. Since radio buttons are not connected in groups in any way yet (this will be in the following articles), emulate toggling radio buttons, as it should be in a group of elements:

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Call the event handler of each of the created objects
   for(int i=0;i<list.Total();i++)
     {
      CCanvasBase *obj=list.At(i);
      if(obj!=NULL)
         obj.OnChartEvent(id,lparam,dparam,sparam);
     }
     
//--- Emulate radio buttons in the group ---
//--- If a custom event is received
   if(id>=CHARTEVENT_CUSTOM)
     {
      //--- If the left radio button is clicked
      if(sparam==radio_bt_lt.NameBG())
        {
         //--- If the button state changed (was not selected) 
         if(radio_bt_lt.State())
           {
            //--- make the right radio button unselected and redraw it
            radio_bt_rt.SetState(false);
            radio_bt_rt.Draw(true);
           }
        }
      //--- If the right radio button is clicked
      if(sparam==radio_bt_rt.NameBG())
        {
         //--- If the button state changed (was not selected) 
         if(radio_bt_rt.State())
           {
            //--- make the left radio button unselected and redraw it
            radio_bt_lt.SetState(false);
            radio_bt_lt.Draw(true);
           }
        }
     }
  }

Let's compile the indicator and run it on the chart:

All controls respond to mouse interaction, and the radio buttons toggle as if they are grouped. Text labels were made so that they change color when the cursor is hovered over to visually represent that you can customize controls at your discretion. In the normal state, label texts are static.

But there is one omission here — when you hover the mouse cursor over a control, an unnecessary tooltip with the indicator name appears. To get rid of this behavior, it is necessary for each graphic object to enter "\n" value in its OBJPROP_TOOLTIP property. Fix it.

In the CCanvasBase class, in the Create method, enter two lines with the installation of tooltips for background and foreground graphic objects:

//+------------------------------------------------------------------+
//| CCanvasBase::Create background and foreground graphical objects  |
//+------------------------------------------------------------------+
bool CCanvasBase::Create(const long chart_id,const int wnd,const string object_name,const int x,const int y,const int w,const int h)
  {
//--- Get the adjusted chart ID
   long id=this.CorrectChartID(chart_id);
//--- Correct the passed object name
   string nm=object_name;
   ::StringReplace(nm," ","_");
//--- Create a graphical object name for the background and create a canvas
   string obj_name=nm+"_BG";
   if(!this.m_background.CreateBitmapLabel(id,(wnd<0 ? 0 : wnd),obj_name,x,y,(w>0 ? w : 1),(h>0 ? h : 1),COLOR_FORMAT_ARGB_NORMALIZE))
     {
      ::PrintFormat("%s: The CreateBitmapLabel() method of the CCanvas class returned an error creating a \"%s\" graphic object",__FUNCTION__,obj_name);
      return false;
     }
//--- Create a graphical object name for the foreground and create a canvas
   obj_name=nm+"_FG";
   if(!this.m_foreground.CreateBitmapLabel(id,(wnd<0 ? 0 : wnd),obj_name,x,y,(w>0 ? w : 1),(h>0 ? h : 1),COLOR_FORMAT_ARGB_NORMALIZE))
     {
      ::PrintFormat("%s: The CreateBitmapLabel() method of the CCanvas class returned an error creating a \"%s\" graphic object",__FUNCTION__,obj_name);
      return false;
     }
//--- If created successfully, enter the program name into the OBJPROP_TEXT property of the graphical object
   ::ObjectSetString(id,this.NameBG(),OBJPROP_TEXT,this.m_program_name);
   ::ObjectSetString(id,this.NameFG(),OBJPROP_TEXT,this.m_program_name);
   ::ObjectSetString(id,this.NameBG(),OBJPROP_TOOLTIP,"\n");
   ::ObjectSetString(id,this.NameFG(),OBJPROP_TOOLTIP,"\n");

//--- Set the dimensions of the rectangular area and return 'true'
   this.m_bound.SetXY(x,y);
   this.m_bound.Resize(w,h);
   return true;
  }

Recompile the indicator and check:

Now, everything is correct.


Conclusion

Today we have taken another step towards creating the Table Control. All complex controls will be assembled from such simple but highly functional objects.

Today, we have added the Controller component to all objects. This allows the user to interact with the controls, and the elements themselves can interact with each other.

In the next article, we will prepare the panel and container elements, which are the main components for placing other elements in them. At the same time, the container enables to scroll child elements inside itself.

Programs used in the article:

#
 Name Type
Description
 1  Base.mqh  Class Library  Classes for creating a base object of controls
 2  Controls.mqh  Class Library  Control classes
 3  iTestLabel.mq5  Test indicator  Indicator for testing manipulations with classes of controls
 4  MQL5.zip  Archive  An archive of the files above for unpacking into the MQL5 directory of the client terminal

Classes within the Base.mqh library:

#
Name
 Description
 1  CBaseObj  A base class for all the graphical objects
 2  CColor  Color management class
 3  CColorElement  A class for managing the colors of various states of a graphical element
 4  CBound  Rectangular area control class
 5  CCanvasBase  A base class for manipulating graphical elements on canvas

Classes within the Controls.mqh library:

#
Name
 Description
 1  CImagePainter  A class for drawing images in an area defined by coordinates and dimensions
 2  CLabel  The "Text Label" Control Class
 3  CButton  The “Simple Button" Control Class
 4  CButtonTriggered  The “Two-Position Button" Control Class
 5  CButtonArrowUp  “Up Arrow Button” Control Class
 6
 CButtonArrowDown  “Down Arrow Button” Control Class
 7  CButtonArrowLeft  “Left Arrow Button” Control Class
 8  CButtonArrowRight  “Right Arrow Button” Control Class
 9  CCheckBox  The “Checkbox" Control Class
 10  CRadioButton  The “Radio Button" Control Class

All created files are attached to the article for self-study. The archive file can be unzipped to the terminal folder, and all files will be located in the desired folder: \MQL5\Indicators\Tables\.

Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/18221

Attached files |
Base.mqh (190.42 KB)
Controls.mqh (162.11 KB)
iTestLabel.mq5 (32.08 KB)
MQL5.zip (37.07 KB)
Implementing Practical Modules from Other Languages in MQL5 (Part 05): The Logging module from Python, Log Like a Pro Implementing Practical Modules from Other Languages in MQL5 (Part 05): The Logging module from Python, Log Like a Pro
Integrating Python's logging module with MQL5 empowers traders with a systematic logging approach, simplifying the process of monitoring, debugging, and documenting trading activities. This article explains the adaptation process, offering traders a powerful tool for maintaining clarity and organization in trading software development.
From Basic to Intermediate: Structs (II) From Basic to Intermediate: Structs (II)
In this article, we will try to understand why programming languages like MQL5 have structures, and why in some cases structures are the ideal way to pass values between functions and procedures, while in other cases they may not be the best way to do it.
Currency pair strength indicator in pure MQL5 Currency pair strength indicator in pure MQL5
We are going to develop a professional indicator for currency strength analysis in MQL5. This step-by-step guide will show you how to develop a powerful trading tool with a visual dashboard for MetaTrader 5. You will learn how to calculate the strength of currency pairs across multiple timeframes (H1, H4, D1), implement dynamic data updates, and create a user-friendly interface.
Capital management in trading and the trader's home accounting program with a database Capital management in trading and the trader's home accounting program with a database
How can a trader manage capital? How can a trader and investor keep track of expenses, income, assets, and liabilities? I am not just going to introduce you to accounting software; I am going to show you a tool that might become your reliable financial navigator in the stormy sea of trading.