English Русский Español Deutsch 日本語
preview
Componentes View e Controller para tabelas no paradigma MVC em MQL5: Elementos de controle simples

Componentes View e Controller para tabelas no paradigma MVC em MQL5: Elementos de controle simples

MetaTrader 5Exemplos |
93 0
Artyom Trishkin
Artyom Trishkin

Conteúdo


Introdução

No âmbito do desenvolvimento do elemento de controle Table View no paradigma MVC (Model-View-Controller), criamos o modelo da tabela (componente Model) e iniciamos a criação do componente View. Na primeira etapa, foi criado um objeto base, que é o progenitor de todos os demais elementos gráficos.

Hoje iniciaremos o desenvolvimento de elementos de controle simples, a partir dos quais, posteriormente, criaremos elementos compostos. Cada elemento de controle será dotado de funcional para interação interativa com o usuário e com outros elementos. Ou seja, trata-se exatamente do funcional do componente Controller.

Como na linguagem MQL modelo de eventos está integrado aos objetos criados por meio dos eventos do gráfico, em todos os elementos de controle organizaremos o processamento de eventos para implementar a ligação do componente View com o componente Controller. Para isso, faremos ajustes na classe base dos elementos gráficos.

Em seguida, criaremos elementos de controle simples, o rótulo de texto e diferentes botões. Cada um desses elementos terá a possibilidade de desenhar um ícone, o que permitirá criar, a partir de botões simples, elementos de controle completamente diferentes. Se observarmos uma linha de uma lista em árvore, onde à esquerda há um ícone e à direita há um texto, isso parece ser um elemento de controle separado. Porém, teremos a possibilidade de criá-lo facilmente utilizando um botão comum. Ao mesmo tempo, será possível configurar os parâmetros da linha de modo que ela ou reaja com a mudança de cor ao passar o cursor do mouse e ao clicar, ou seja estática, mas responda a cliques.

Tudo isso poderá ser feito facilmente com a ajuda de algumas linhas de configuração do objeto após a sua criação. E, a partir desses elementos, posteriormente criaremos elementos de controle compostos complexos, totalmente interativos e prontos para uso.


Componente Controller. Aperfeiçoamento das classes base

Assim, para implementar o que foi planejado, precisamos aperfeiçoar um pouco as classes, macrossubstituições e enumerações já escritas. A maior parte do funcional necessário ficará localizada no objeto base dos elementos gráficos. Portanto, o principal aperfeiçoamento afetará justamente esse objeto.

Anteriormente, essa classe estava localizada no caminho MQL5\Scripts\Tables\Controls\Base.mqh.
Hoje vamos escrever um indicador de teste, portanto precisamos, no diretório de indicadores \MQL5\Indicators, criar uma nova pasta \Tables\Controls\ e colocar nela o arquivo Base.mqh, que é exatamente o que iremos aperfeiçoar hoje.

Posteriormente, nos objetos serão armazenadas listas de elementos de controle anexados. Tais objetos podem ser, por exemplo, contêineres. Para que essas listas possam funcionar corretamente com arquivos, isto é, criar objetos armazenados nas listas, é necessário declarar previamente todas as classes dos elementos que serão criados. Vamos inserir no arquivo Base.mqh declaração das classes, novas macrossubstituições, enumerações e constantes das enumerações:

//+------------------------------------------------------------------+
//|                                                         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                                                        |
//+------------------------------------------------------------------+

Ao criar métodos de salvamento e carregamento de objetos em/de arquivos, em cada método existem linhas constantes que se repetem sem alteração de um método para outro:

//--- 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;

E também existem métodos idênticos Description e Print. Portanto, é razoável transferir essas linhas para os métodos de carregamento/salvamento no objeto base. Assim, não será necessário escrevê-las em cada novo método de carregamento/salvamento em cada nova classe onde esteja prevista a работа com arquivos.

Declararemos esses métodos no objeto base:

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) {}
  };

E escrever a sua implementação:

//+------------------------------------------------------------------+
//| 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;
  }

Agora, para cada nova classe, os métodos Description e Print podem ser declarados e implementados apenas no caso em que a sua lógica seja diferente da lógica definida nessa classe.

E, nos métodos de trabalho com arquivos nas classes herdadas, em vez de escrever constantemente as mesmas linhas em cada método de cada classe, simplesmente chamaremos os métodos de trabalho com arquivos desse objeto base.

De todas as classes subsequentes deste arquivo (Base.mqh), removeremos todos os métodos Print, pois eles já existem no objeto base e o repetem completamente.

Em todos os métodos de trabalho com arquivos removeremos as seguintes linhas:

//+------------------------------------------------------------------+
//| 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;
  }

Agora em vez dessas linhas, temos simplesmente a chamada do método da classe base:

//+------------------------------------------------------------------+
//| 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;
  }

Essas alterações já foram feitas em todos os métodos de trabalho com arquivos deste arquivo. Ao escrever as classes subsequentes, levaremos essas alterações em consideração.

Na classe CColorElement, substituiremos nos construtores das classes linhas idênticas e repetidas

//+-----------------------------------------------------------------------------+
//| 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);
  }

por um único método 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

...

A sua implementação:

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

Se no método de inicialização da cor for passado um valor de cor transparente, então não é necessário alterá-lo de forma alguma para quaisquer estados.

Levaremos isso em conta na implementação do método:

//+----------------------------------------------------------------------+
//| 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);   
  }

Na classe CBound, é necessário adicionar um método que retorne o sinalizador de o cursor estar dentro da área retangular — pois isso será necessário na implementação do componente Controller:

//+------------------------------------------------------------------+
//| 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); }
  };

Agora é necessário adicionar tudo o que for preciso para a implementação do componente Controller na classe base do canvas dos elementos gráficos CCanvasBase.

Na interação dos elementos gráficos com o mouse, é necessário desativar algumas propriedades do gráfico, como a rolagem do gráfico com a rodinha do mouse, o menu do botão direito do mouse, etc. Isso será feito por cada objeto dos elementos gráficos. Porém, na primeira execução, é necessário memorizar o estado das propriedades do gráfico que existia antes da execução do programa. E, após o término do trabalho, restaurar tudo ao estado original.

Para isso, na seção privada da classe CCanvasBase, declararemos variáveis para armazenar os valores das propriedades do gráfico que devem ser memorizadas, bem como um método para definir as restrições dessas propriedades do gráfico.

//+------------------------------------------------------------------+
//| 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:

Os elementos gráficos podem ter dois estados (podem ser mais, mas por enquanto são dois). Por exemplo, para um botão, pressionado e solto. Isso significa que precisamos controlar os estados das cores do elemento em seus dois estados. Na seção protegida da classe, definiremos uma variável para armazenar o estado do elemento, mais um conjunto de objetos de gerenciamento de cores e separaremos o controle de transparência do canvas de fundo e do primeiro plano de forma independente:

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

Aqui também declararemos métodos para controle do cursor do mouse, gerenciamento de cores e manipuladores virtuais de eventos:

//--- 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:

Na seção pública da classe, adicionaremos métodos para obter os objetos de gerenciamento de cores do elemento no estado ativado e métodos para obter as cores nos diferentes estados do elemento:

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

Agora, em cada um dos métodos de obtenção de cor, o estado do elemento é verificado, ativado ou desativado, e a cor necessária é retornada de acordo com o estado do elemento.

Adicionaremos métodos para definir as cores do elemento ativado e aperfeiçoaremos os métodos para definir as cores dos estados do elemento em relação ao cursor do mouse, levando em conta o estado do elemento como ativado ou desativado:

//--- 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);      }

Adicionaremos métodos para definir e retornar o estado do elemento:

//--- 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)

Ao definir o estado do elemento, é necessário, após definir o sinalizador de estado, gravar todas as cores do elemento como atuais — se o elemento estiver ativado, por exemplo, um botão pressionado, todas as cores atuais são definidas como as cores do botão pressionado. Caso contrário, as cores atuais são obtidas da lista de cores do botão não pressionado.

Como agora separamos a definição e o retorno da transparência para o fundo e o primeiro plano, adicionaremos novos métodos para definir e retornar a transparência:

   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

Declararemos um manipulador de eventos que deverá ser chamado a partir do manipulador de eventos do programa controlador:

//--- 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);
  };

No construtor, faremos a indicação correta das propriedades da fonte desenhada no canvas e chamaremos o método Init() para memorizar as propriedades do gráfico e do 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();
     }
  }

No destrutor da classe, destruímos o objeto gráfico criado e restauramos as propriedades do gráfico e as permissões do mouse que foram memorizadas:

//+------------------------------------------------------------------+
//| 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);
  }

No método de criação do elemento gráfico, o nome do objeto gráfico criado não deve conter espaços. Corrigiremos isso, substituindo os espaços no nome por sublinhados:

//+------------------------------------------------------------------+
//| 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;
  }

O método que retorna o sinalizador de o cursor estar dentro do objeto:

//+-----------------------------------------------------------------------------------+
//| 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);
  }

Como o tamanho do objeto e o tamanho do canvas podem diferir (o método ObjectTrim altera o tamanho do canvas sem modificar o tamanho do objeto), então aqui, como limites dentro dos quais o cursor se encontra, é utilizado um dos valores: ou o limite do objeto, ou a borda correspondente do canvas. O método retorna o sinalizador de as coordenadas passadas estarem dentro dos limites obtidos.

No método de bloqueio do elemento, a definição do sinalizador de bloqueio deve estar antesda chamada do método de desenho do elemento, vamos corrigir isso:

//+------------------------------------------------------------------+
//| 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);
  }

Essa correção permite desenhar corretamente o elemento bloqueado. Até essa alteração, ao desenhar o elemento, as cores eram obtidas do estado padrão e não do estado bloqueado, pois o sinalizador era definido apenas após o bloqueio.

Método para definir as restrições do gráfico:

//+------------------------------------------------------------------+
//| 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);
  }

Método de inicialização da classe:

//+------------------------------------------------------------------+
//| 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();
  }

Método de inicialização das cores do objeto por padrão:

//+------------------------------------------------------------------+
//| 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);
  }

Método que verifica se a cor definida é igual à especificada:

//+------------------------------------------------------------------+
//| 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;
  }

Para que as cores do elemento sejam alteradas apenas no momento da troca do estado do elemento, esse método retorna um sinalizador indicando se as cores correspondentes ao estado do elemento já estão definidas. Se as cores atuais do elemento não forem iguais às cores estabelecidas para o estado verificado, o método permite a mudança de cor e o redesenho do elemento gráfico. Se as cores já estiverem definidas de acordo com o estado do elemento, não há necessidade de alterar as cores nem de redesenhar o objeto — o método proíbe a mudança de cor.

Método que altera as cores dos elementos do objeto por evento:

//+------------------------------------------------------------------+
//| 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;
     }
  }

Dependendo do evento pelo qual é necessário alterar a cor, são definidas as cores atuais de acordo com o evento, isto é, com o estado do elemento.

Manipulador de eventos:

//+------------------------------------------------------------------+
//| 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);
        }
     }
  }

No objeto base dos elementos gráficos foi organizada a lógica de processamento da interação do cursor do mouse com os elementos gráficos. Para os diversos eventos monitorados, são chamados manipuladores virtuais. Alguns manipuladores são implementados diretamente nesta classe, enquanto outros simplesmente não fazem nada e precisam ser implementados nos objetos que herdam desta classe.

Os manipuladores de eventos cujo nome termina em *Handler são destinados ao processamento da interação interna nos elementos de controle entre os componentes que os compõem. Já os manipuladores que possuem *Event em seu nome processam diretamente os eventos do gráfico e enviam ao gráfico eventos do usuário, pelos quais é possível determinar o tipo de evento e de qual elemento de controle ele foi enviado. Isso permitirá ao usuário tratar tais eventos em seu próprio programa.

Manipulador de perda de foco:

//+------------------------------------------------------------------+
//| 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);
     }
  }

Manipulador de passagem do cursor:

//+------------------------------------------------------------------+
//| 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);
     }
  }

Manipulador de clique no objeto:

//+------------------------------------------------------------------+
//| 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());
  }

Manipulador do evento de criação do objeto gráfico:

//+------------------------------------------------------------------+
//| 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);
  }

A lógica de todos os manipuladores está detalhadamente comentada no código. Essencialmente, aqui apenas é organizada a reação ao evento na forma de alteração das cores do elemento gráfico e, onde necessário, o envio de eventos do usuário para o gráfico. O último manipulador reage à criação do objeto gráfico no gráfico e move os elementos gráficos para o primeiro plano. Isso permitirá, por exemplo, que painéis permaneçam sempre no primeiro plano.

Todos esses manipuladores são virtuais e, se necessário, devem ser sobrescritos nas classes herdadas.

Com o aperfeiçoamento do objeto base de todos os elementos gráficos, finalizamos esta etapa. Agora, com base no componente Controller criado no objeto base e no componente View criado anteriormente, iniciaremos a criação dos elementos gráficos mais simples, que também fazem parte do componente View. Eles se tornarão os "blocos de construção" a partir dos quais, ao final, serão criados elementos de controle complexos e, em particular, o elemento de controle Table View, cuja criação estamos desenvolvendo ao longo de vários artigos.


Elementos de controle simples

Na mesma pasta \MQL5\Indicators\Tables\Controls\ criaremos um novo arquivo incluído Controls.mqh.

Ao arquivo criado incluiremos o arquivo do objeto base dos elementos gráficos Base.mqh e adicionaremos algumas macrossubstituições e enumerações:

//+------------------------------------------------------------------+
//|                                                     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                                                          |
//+------------------------------------------------------------------+

Nas macrossubstituições definimos os tamanhos padrão para rótulos de texto e botões. Na enumeração indicamos as propriedades existentes do elemento gráfico base. Com base nessas propriedades, será possível buscar objetos, classificá-los e compará-los. Ao adicionar novas propriedades a quaisquer objetos, incluiremos novas constantes nessa enumeração.


Classes auxiliares

Cada elemento gráfico pode ter em sua composição uma imagem. Isso permitirá desenhar ícones para botões, linhas de texto, etc.
Criaremos uma classe especial para o desenho de imagens, que será uma parte integrante dos elementos de controle simples.

Classe para desenho de imagens em uma área especificada

O objeto dessa classe será declarado na classe do elemento de controle e fornecerá a possibilidade de especificar as dimensões da área e suas coordenadas, dentro das quais a imagem será desenhada. A classe será dotada de métodos para desenhar setas para botões com setas, checkboxes e radiobuttons. Posteriormente, adicionaremos métodos para desenhar outros ícones e um método para desenhar imagens personalizadas. Na classe será passado um ponteiro para o canvas no qual o desenho é realizado, dentro das coordenadas e limites definidos na classe do objeto de desenho:
//+------------------------------------------------------------------+
//| 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) {}
  };

Analisemos os métodos da classe.

Método para comparar dois objetos de desenho:

//+------------------------------------------------------------------+
//| 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);
     }
  }

O método é necessário para a busca do objeto de desenho desejado. Por padrão, a busca é realizada pelo identificador do objeto. O método será necessário quando, nos objetos dos elementos de controle, houver listas nas quais são armazenados objetos de desenho. No momento, em cada elemento de controle será declarado um único objeto de desenho, destinado ao desenho do ícone principal do elemento.

Método que verifica a validade do canvas e a correção das dimensões da área da imagem:

//+------------------------------------------------------------------+
//|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;
  }

Se nenhum ponteiro para o canvas for passado ao objeto, ou se a largura e a altura da área da imagem não estiverem definidas, o método retorna false. Caso contrário, retorna true.

Método que limpa a área da imagem:

//+------------------------------------------------------------------+
//| 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;   
  }

O método limpa completamente toda a área da imagem, preenchendo-a com uma cor transparente.

Método que desenha uma seta preenchida para cima:

//+------------------------------------------------------------------+
//| 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;   
  }

Método que desenha uma seta preenchida para baixo:

//+------------------------------------------------------------------+
//| 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;   
  }

Método que desenha uma seta preenchida para a esquerda:

//+------------------------------------------------------------------+
//| 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;
  }

Método que desenha uma seta preenchida para a direita:

//+------------------------------------------------------------------+
//| 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;
  }

Dentro da área da imagem é definida uma área para a seta com um recuo de um pixel em cada um dos lados da área retangular, e dentro dela é desenhada a seta preenchida.

Método que desenha um CheckBox marcado:

//+------------------------------------------------------------------+
//| 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;
  }

Método que desenha um CheckBox desmarcado:

//+------------------------------------------------------------------+
//| 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;
  }

Método que desenha um RadioButton marcado:

//+------------------------------------------------------------------+
//| 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;
  }

Método que desenha um RadioButton desmarcado:

//+------------------------------------------------------------------+
//| 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;
  }

Esses são métodos simples, que apenas fornecem a possibilidade de desenhar as figuras necessárias sem a necessidade de escrevê-las manualmente. Mais adiante, adicionaremos aqui outros métodos que desenham diferentes ícones para a formatação dos elementos gráficos.

Métodos para salvar a área de desenho em arquivo e carregá-la a partir de arquivo:

//+------------------------------------------------------------------+
//| 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;
  }

Agora já podemos prosseguir para as classes dos elementos de controle simples. O objeto mínimo desse tipo será a classe de rótulo de texto. A partir desse elemento serão herdadas as classes de outros elementos de controle.
Neste mesmo arquivo Controls.mqh continuaremos a escrever o código das classes.

Classe do elemento de controle "Rótulo de texto"

Nesta classe haverá um conjunto de variáveis e métodos que permitem trabalhar com qualquer elemento de controle, definir e obter os parâmetros do objeto, salvar e carregar as suas propriedades. A interatividade de todos os elementos de controle, o componente Controller, nós adicionamos hoje à classe base de todos os elementos de controle. Analisemos a classe do rótulo de texto:

//+------------------------------------------------------------------+
//| 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) {}
  };

Na classe estão definidos dois arrays de símbolos ushort, para o texto atual e para o texto anterior do rótulo. Isso permite, durante o desenho, ter acesso às dimensões do texto anterior e apagar corretamente a área coberta pelo texto antes de exibir o novo texto no canvas.

Analisemos os métodos declarados.

A classe possui quatro construtores, que permitem criar o objeto com diferentes conjuntos de parâmetros:

//+------------------------------------------------------------------+
//| 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);
  }

A área de desenho, a ícone do elemento, é definida com dimensões zero, o que significa a ausência de ícone no elemento. O texto do elemento é definido e é atribuída transparência total ao fundo e opacidade total ao primeiro plano.

Método para comparação de dois objetos:

//+------------------------------------------------------------------+
//| 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);
     }
  }

A comparação é possível pelo nome do objeto, texto do rótulo, cor, transparência e identificador. Por padrão, os objetos são comparados pelo identificador do objeto, pois quando os objetos estão em uma mesma lista, é melhor distingui-los pelos identificadores para acesso rápido ao necessário.

Método que apaga o texto do rótulo:

//+------------------------------------------------------------------+
//| 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);
  }

Se anteriormente algum texto foi escrito, ele pode ser apagado preenchendo completamente a sua área com um retângulo totalmente transparente de acordo com o tamanho do texto. Se anteriormente não havia texto, toda a área do canvas do objeto é apagada.

Método que exibe o texto no 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);
  }

Aqui, primeiro o texto anterior é apagado do canvas e, em seguida, o novo texto é exibido. Se o novo texto ultrapassar os limites do objeto, no local em que ele extrapola os limites do elemento à direita é exibida uma reticência, indicando que o texto não coube na área do objeto, aproximadamente assim: "Este texto não cab...".

Método que desenha a aparência externa:

//+------------------------------------------------------------------+
//| 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);
  }

Aqui é simplesmente chamado o método de desenho do texto do rótulo.

Métodos de trabalho com arquivos:

//+------------------------------------------------------------------+
//| 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;
  }

Com base na classe analisada, criaremos a classe de botão simples.

Classe do elemento de controle "Botão simples"

//+------------------------------------------------------------------+
//| 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) {}
  };

A classe do botão simples difere da classe do rótulo de texto apenas pelo método de desenho da aparência externa.

A classe possui quatro construtores, que permitem criar um botão com os parâmetros especificados:

//+-------------------------------------------------------------------+
//| 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);
  }

Definimos o estado "botão não pressionado" e atribuímos opacidade total ao fundo e ao primeiro plano.

Método de comparação de dois objetos:

//+------------------------------------------------------------------+
//| 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);
     }
  }

O método é idêntico ao método da classe do rótulo de texto. Muito provavelmente, se outras propriedades não surgirem para os botões, esse método poderá ser removido da classe e será utilizado o método da classe pai.

Método que desenha a aparência externa do botão:

//+------------------------------------------------------------------+
//| 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);
  }

Primeiro, o fundo é preenchido com a cor definida, em seguida a borda é desenhada e o texto do botão é exibido.

Com base nessa classe, criaremos a classe do botão de duas posições.

Classe do elemento de controle "Botão de duas posições"

//+------------------------------------------------------------------+
//| 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) {}
  };

O objeto possui quatro construtores, que permitem criar um botão com os parâmetros especificados:

//+------------------------------------------------------------------+
//| 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();
  }

Em cada construtor é chamado o método de inicialização das cores padrão:

//+------------------------------------------------------------------+
//| 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);
  }

Estas são as cores padrão definidas para um botão recém-criado. Após a criação do objeto, todas as cores podem ser configuradas conforme desejado.

No método de comparaçãofoi adicionada a comparação pelo estado do botão:

//+------------------------------------------------------------------+
//| 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);
     }
  }

Método que desenha a aparência externa do botão:

//+------------------------------------------------------------------+
//| 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);
  }

O método é idêntico ao método da classe pai e, se nenhuma modificação adicional da classe for realizada posteriormente, esse método poderá ser removido, sendo utilizado o método de desenho da classe pai.

O botão de duas posições possui dois estados:

  1. Pressionado,
  2. Não pressionado.

Para o rastreamento e a alternância de seus estados, aqui foi sobrescrito o manipulador de pressionamento dos botões do mouse OnPressEvent da classe pai:

//+------------------------------------------------------------------+
//| 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);
  }

Com base na classe CButton criaremos quatro botões com setas: para cima, para baixo, para a esquerda e para a direita. Os objetos utilizarão a classe de desenho de imagens para desenhar as setas.

Classe do elemento de controle "Botão com seta para cima"

//+------------------------------------------------------------------+
//| 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) {}
  };

Quatro construtores permitem criar um objeto com os parâmetros especificados:

//+------------------------------------------------------------------+
//| 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);
  }

Nos construtores são inicializadas as cores padrão e são definidas as coordenadas e os tamanhos da área da imagem.

Método que desenha a aparência externa do botão:

//+------------------------------------------------------------------+
//| 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);
  }

O método é semelhante ao método de desenho do botão, porém adicionalmente é exibido o desenho da seta para cima por meio do método ArrowUp do objeto de desenho.

Todos os demais classes são idênticas à analisada, porém nos métodos de desenho são renderizados os ícones correspondentes à finalidade do botão.

Classe do elemento de controle "Botão com seta para baixo"

//+------------------------------------------------------------------+
//| 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);
  }

Classe do elemento de controle "Botão com seta para a esquerda"

//+------------------------------------------------------------------+
//| 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);
  }

Classe do elemento de controle "Botão com seta para a direita"

//+------------------------------------------------------------------+
//| 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);
  }

Classe do elemento de controle "Checkbox"

A classe do elemento de controle "checkbox" é semelhante às classes dos botões com setas, porém aqui o fundo será totalmente transparente. Ou seja, serão desenhados apenas o texto e o ícone do checkbox. O checkbox possui dois estados, marcado e desmarcado, portanto será herdado da classe de botão de duas posições:

//+------------------------------------------------------------------+
//| 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) {}
  };

Todas as classes dos elementos de controle possuem quatro construtores:

//+------------------------------------------------------------------+
//| 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);
  }

Aqui são inicializadas as cores padrão do objeto, é definido um fundo totalmente transparente e um primeiro plano opaco, e em seguida são configurados os tamanhos e as coordenadas da área da imagem.

O método de comparação retorna o resultado da chamada do método de comparação da classe pai:

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

Método de inicialização das cores padrão do objeto:

//+------------------------------------------------------------------+
//| 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);
  }

Método de desenho da aparência externa do checkbox:

//+------------------------------------------------------------------+
//| 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);
  }

Dependendo do estado do elemento, é desenhado um quadrado marcado com um visto ou apenas um quadrado vazio.

Agora, com base neste objeto, criaremos a classe do elemento de controle "radiobutton".

Classe do elemento de controle "Radiobutton"

Como o radiobutton sempre funciona em grupo, ele só pode ser desativado quando outro botão do grupo é ativado, portanto aqui também precisamos sobrescrever o manipulador de clique no objeto da classe pai.

//+------------------------------------------------------------------+
//| 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) {}
  };

Construtores:

//+------------------------------------------------------------------+
//| 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)
  {
  }

Nenhuma ação adicional após a chamada do construtor da classe pai é necessária aqui. Portanto, os construtores possuem corpo vazio.

O método de comparação retorna o resultado da chamada do método de comparação da classe pai:

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

Método de desenho da aparência externa do botão:

//+------------------------------------------------------------------+
//| 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);
  }

Esse método é idêntico ao método da classe pai, porém aqui são desenhados os ícones do radiobutton, selecionado e não selecionado.

Manipulador de eventos de pressionamento dos botões do mouse:

//+------------------------------------------------------------------+
//| 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);
  }

Aqui, se o botão já estiver em estado ligado, não é necessário executar nenhuma ação — saímos do manipulador. Se o botão estiver desligado, invertemos o seu estado e chamamos o manipulador da classe pai, o objeto base de todos os elementos de controle CCanvasBase.

Por hoje, estes são todos os elementos de controle que minimamente era necessário implementar para a realização de elementos de controle complexos.
Vamos testar o que obtivemos.


Testando o resultado

Na pasta \MQL5\Indicators\Tables\ criaremos um novo indicador com o nome iTestLabel.mq5.

Definiremos o número de buffers de cálculo e de séries gráficas do indicador como zero — não é necessário desenhar nenhum gráfico. Conectaremos a biblioteca criada de elementos gráficos. O indicador irá desenhar elementos gráficos, que, no momento da criação, salvaremos em uma lista, cujo arquivo de classe está conectado ao arquivo do indicador:

//+------------------------------------------------------------------+
//|                                                   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                         |
//+------------------------------------------------------------------+

Aqui, para simplificação, já são criados ponteiros para os elementos gráficos que serão criados, aos quais iremos nos referir após a criação do elemento para trabalhar com os objetos.

Todos os objetos serão criados no manipulador OnInit() do indicador. Faremos o seguinte: criaremos um objeto base e o coloriremos de modo que lembre algum tipo de painel.

Dentro dessa "base" criaremos todos os elementos gráficos e indicaremos esse objeto base como contêiner para eles.

Escreveremos em OnInit() o seguinte código:

//+------------------------------------------------------------------+
//| 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);
  }

Estude atentamente todos os comentários do código — aqui todos os passos de criação dos objetos estão descritos de forma bastante detalhada.

No manipulador OnDeinit() do indicador destruímos todos os objetos da lista:

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

O manipulador OnCalculate() está vazio — não calculamos nem exibimos nada no gráfico:

//+------------------------------------------------------------------+
//| 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);
  }

Para dar vida aos elementos gráficos criados, é necessário, no manipulador OnChartEvent()percorrer a lista de objetos criados e chamar o manipulador homônimo para cada elemento. Como, por enquanto, as radiobotões não estão de nenhuma forma ligadas em grupos — isso será feito nos próximos artigos — emularemos a comutação das radiobotões como deveria ocorrer em um grupo de elementos:

//+------------------------------------------------------------------+
//| 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);
           }
        }
     }
  }

Vamos compilar o indicador e executá-lo no gráfico:

Todos os elementos de controle reagem à interação com o mouse, as radiobotões alternam como se estivessem agrupadas. As rótulos de texto foram feitas para mudar de cor ao passar o cursor, para uma apresentação visual clara de que os elementos de controle podem ser configurados conforme desejado. No estado normal, os textos dos rótulos são estáticos.

Mas aqui há uma omissão — ao passar o cursor do mouse sobre um elemento de controle, aparece um tooltip desnecessário com o nome do indicador. Para eliminar esse comportamento, é necessário escrever o valor "\n" na propriedade OBJPROP_TOOLTIP de cada objeto gráfico. Vamos corrigir isso.

Na classe CCanvasBase, no método Create adicionaremos duas linhas configurando os tooltips para os objetos gráficos de fundo e de primeiro plano:

//+------------------------------------------------------------------+
//| 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;
  }

Recompilaremos o indicador e verificaremos:

Agora tudo está correto.


Considerações finais

Hoje demos mais um passo na criação do elemento de controle Table Control. Todos os elementos de controle complexos serão montados a partir de objetos simples como estes, porém bastante funcionais.

Hoje adicionamos a todos os objetos o componente Controller, que permite ao usuário interagir de forma interativa com os elementos de controle e aos próprios elementos interagirem entre si.

No próximo artigo, prepararemos os elementos painel e contêiner, que são os principais componentes para a colocação de outros elementos dentro deles. Ao mesmo tempo, o contêiner oferece a possibilidade de rolagem dos elementos filhos em seu interior.

Programas utilizados no artigo:

#
 Nome Tipo
Descrição
 1  Base.mqh  Biblioteca de classes  Classes para a criação do objeto base dos elementos de controle
 2  Controls.mqh  Biblioteca de classes  Classes dos elementos de controle
 3  iTestLabel.mq5  Indicador de teste  Indicador para testar o trabalho com as classes de elementos de controle
 4  MQL5.zip  Arquivo compactado  Arquivo com os arquivos apresentados acima, para extração no diretório MQL5 do terminal cliente

Classes que compõem a biblioteca Base.mqh:

#
Nome
 Descrição
 1  CBaseObj  Classe base para todos os objetos gráficos
 2  CColor  Classe para gerenciamento de cores
 3  CColorElement  Classe para gerenciamento das cores dos diferentes estados do elemento gráfico
 4  CBound  Classe para gerenciamento de área retangular
 5  CCanvasBase  Classe base para trabalhar com elementos gráficos no canvas

Classes que compõem a biblioteca Controls.mqh:

#
Nome
 Descrição
 1  CImagePainter  Classe para desenho de imagens em uma área definida por coordenadas e dimensões
 2  CLabel  Classe do elemento de controle "rótulo de texto"
 3  CButton  Classe do elemento de controle "botão simples"
 4  CButtonTriggered  Classe do elemento de controle "botão de duas posições"
 5  CButtonArrowUp  Classe do elemento de controle "botão com seta para cima"
 6
 CButtonArrowDown  Classe do elemento de controle "botão com seta para baixo"
 7  CButtonArrowLeft  Classe do elemento de controle "botão com seta para a esquerda"
 8  CButtonArrowRight  Classe do elemento de controle "botão com seta para a direita"
 9  CCheckBox  Classe do elemento de controle "checkbox"
 10  CRadioButton  Classe do elemento de controle "radiobutton"

Todos os arquivos criados são anexados ao artigo para estudo individual. O arquivo compactado pode ser extraído para a pasta do terminal, e todos os arquivos estarão localizados na pasta correta: \MQL5\Indicators\Tables.

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/18221

Arquivos anexados |
Base.mqh (190.42 KB)
Controls.mqh (162.11 KB)
iTestLabel.mq5 (32.08 KB)
MQL5.zip (37.07 KB)
Redes neurais em trading: Extração eficiente de características para classificação precisa (Construção de objetos) Redes neurais em trading: Extração eficiente de características para classificação precisa (Construção de objetos)
Mantis é uma ferramenta universal para análise profunda de séries temporais, escalável de forma flexível para quaisquer cenários financeiros. Saiba como a combinação de patching, convoluções locais e atenção cruzada permite obter uma interpretação de alta precisão dos padrões de mercado.
Negociamos opções sem opções (Parte 1): Fundamentos da teoria e emulação por meio de ativos subjacentes Negociamos opções sem opções (Parte 1): Fundamentos da teoria e emulação por meio de ativos subjacentes
O artigo descreve uma variante de emulação de opções por meio do ativo subjacente, implementada na linguagem de programação MQL5. São comparadas as vantagens e desvantagens da abordagem escolhida em relação às opções reais negociadas em bolsa, usando como exemplo o mercado futuro FORTS da bolsa de Moscou MOEX e a corretora de criptomoedas Bybit.
Indicador do modelo CAPM no mercado Forex Indicador do modelo CAPM no mercado Forex
Adaptação do modelo clássico CAPM para o mercado cambial Forex em MQL5. O indicador calcula a rentabilidade esperada e o prêmio de risco com base na volatilidade histórica. Os indicadores aumentam nos picos e nas depressões, refletindo os princípios fundamentais de precificação. Aplicação prática para estratégias contra a tendência e de seguimento de tendência, levando em conta a dinâmica da relação entre risco e rentabilidade em tempo real. Inclui o aparato matemático e a implementação técnica.
Redes neurais em trading: Extração eficiente de características para classificação precisa (Conclusão) Redes neurais em trading: Extração eficiente de características para classificação precisa (Conclusão)
O framework Mantis transforma séries temporais complexas em tokens informativos e serve como uma base confiável para um Agente de trading inteligente, pronto para operar em tempo real.