preview
Компоненты View и Controller для таблиц в парадигме MVC на MQL5: Простые элементы управления

Компоненты View и Controller для таблиц в парадигме MVC на MQL5: Простые элементы управления

MetaTrader 5Примеры |
481 0
Artyom Trishkin
Artyom Trishkin

Содержание


Введение

В рамках разработки элемента управления Table View в парадигме MVC (Model-View-Controller) мы создали модель таблицы (компонент Model) и приступили к созданию компонента View. На первом этапе был создан базовый объект, являющийся прародителем всех остальных графических элементов.

Сегодня начнём разработку простых элементов управления, из которых далее будем создавать составные элементы. Каждый элемент управления будет наделён функционалом для интерактивного взаимодействия с пользователем и с другими элементами. Т.е. это не что иное, как функционал компонента Controller.

Так как в языке MQL событийная модель интегрирована в создаваемые объекты при помощи событий чарта, то во всех элементах управления организуем обработку событий для реализации связи компонента View с компонентом Controller. Для этого доработаем базовый класс графических элементов.

Далее создадим простые элементы управления — текстовую метку и различные кнопки. У каждого из этих элементов будет возможность рисования иконки, что даст возможность создавать из простых кнопок совершенно разные элементы управления. Если посмотреть на строку древовидного списка, где слева есть иконка, а справа — текст, то это вроде бы отдельный элемент управления. Но у нас будет возможность легко создать его, используя обычную кнопку. При этом будет возможность настроить параметры строки так, чтобы она либо реагировала изменением цвета при наведении курсора мышки и щелчке, либо была статичной, но откликалась на нажатия.

Всё это будет легко сделать при помощи нескольких строк настроек объекта после его создания. И вот из таких элементов далее будем создавать сложные составные элементы управления — полностью интерактивные и готовые к использованию.


Компонент Controller. Доработка базовых классов

Итак, чтобы реализовать задуманное, нам необходимо немного доработать уже написанные классы, макроподстановки  и перечисления. Большинство необходимого функционала будет расположено в базовом объекте графических элементов. Поэтому основная доработка коснётся именно его.

Ранее этот класс был расположен по адресу MQL5\Scripts\Tables\Controls\Base.mqh.
Сегодня будем писать тестовый индикатор, поэтому нам нужно в каталоге индикаторов \MQL5\Indicators\ создать новую папку \Tables\Controls\ и разместить в ней файл Base.mqh — именно его и будем дорабатывать сегодня.

В последующем в объектах будут храниться списки прикреплённых элементов управления. Такими объектами могут быть, например, контейнеры. Чтобы эти списки могли правильно работать с файлами — создавать объекты, хранимые в списках, необходимо все классы создаваемых элементов заранее продекларировать. Впишем в файл Base.mqh декларацию классов, новые макроподстановки, перечисления и константы перечислений:

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

//+------------------------------------------------------------------+
//| Включаемые библиотеки                                            |
//+------------------------------------------------------------------+
#include <Canvas\Canvas.mqh>              // Класс СБ CCanvas
#include <Arrays\List.mqh>                // Класс СБ CList

//--- Форвард-декларация классов элементов управления
class    CImagePainter;                   // Класс рисования изображений
class    CLabel;                          // Класс текстовой метки
class    CButton;                         // Класс простой кнопки
class    CButtonTriggered;                // Класс двухпозиционной кнопки
class    CButtonArrowUp;                  // Класс кнопки со стрелкой вверх
class    CButtonArrowDown;                // Класс кнопки со стрелкой вниз
class    CButtonArrowLeft;                // Класс кнопки со стрелкой влево
class    CButtonArrowRight;               // Класс кнопки со стрелкой вправо
class    CCheckBox;                       // Класс элемента управления CheckBox
class    CRadioButton;                    // Класс элемента управления RadioButton

//+------------------------------------------------------------------+
//| Макроподстановки                                                 |
//+------------------------------------------------------------------+
#define  clrNULL              0x00FFFFFF  // Прозрачный цвет для CCanvas
#define  MARKER_START_DATA    -1          // Маркер начала данных в файле
#define  DEF_FONTNAME         "Calibri"   // Шрифт по умолчанию
#define  DEF_FONTSIZE         10          // Размер шрифта по умолчанию

//+------------------------------------------------------------------+
//| Перечисления                                                     |
//+------------------------------------------------------------------+
enum ENUM_ELEMENT_TYPE                    // Перечисление типов графических элементов
  {
   ELEMENT_TYPE_BASE = 0x10000,           // Базовый объект графических элементов
   ELEMENT_TYPE_COLOR,                    // Объект цвета
   ELEMENT_TYPE_COLORS_ELEMENT,           // Объект цветов элемента графического объекта
   ELEMENT_TYPE_RECTANGLE_AREA,           // Прямоугольная область элемента
   ELEMENT_TYPE_IMAGE_PAINTER,            // Объект для рисования изображений
   ELEMENT_TYPE_CANVAS_BASE,              // Базовый объект холста графических элементов
   ELEMENT_TYPE_LABEL,                    // Текстовая метка
   ELEMENT_TYPE_BUTTON,                   // Простая кнопка
   ELEMENT_TYPE_BUTTON_TRIGGERED,         // Двухпозиционная кнопка
   ELEMENT_TYPE_BUTTON_ARROW_UP,          // Кнопка со стрелкой вверх
   ELEMENT_TYPE_BUTTON_ARROW_DOWN,        // Кнопка со стрелкой вниз
   ELEMENT_TYPE_BUTTON_ARROW_LEFT,        // Кнопка со стрелкой влево
   ELEMENT_TYPE_BUTTON_ARROW_RIGHT,       // Кнопка со стрелкой вправо
   ELEMENT_TYPE_CHECKBOX,                 // Элемент управления CheckBox
   ELEMENT_TYPE_RADIOBUTTON,              // Элемент управления RadioButton
  };
  
enum ENUM_ELEMENT_STATE                   // Состояние элемента
  {
   ELEMENT_STATE_DEF,                     // По умолчанию (напр. кнопка отжата, и т.п.)
   ELEMENT_STATE_ACT,                     // Активирован (напр. кнопка нажата, и т.п.)
  };

enum ENUM_COLOR_STATE                     // Перечисление цветов состояний элемента
  {
   COLOR_STATE_DEFAULT,                   // Цвет обычного состояния
   COLOR_STATE_FOCUSED,                   // Цвет при наведении курсора на элемент
   COLOR_STATE_PRESSED,                   // Цвет при нажатии на элемент
   COLOR_STATE_BLOCKED,                   // Цвет заблокированного элемента
  };
//+------------------------------------------------------------------+ 
//| Функции                                                          |
//+------------------------------------------------------------------+

При создании методов сохранения и загрузки объектов в/из файлов, в каждом методе есть постоянные повторяющиеся строки без изменения от метода к методу:

//--- Проверяем хэндл
   if(file_handle==INVALID_HANDLE)
      return false;
//--- Сохраняем маркер начала данных - 0xFFFFFFFFFFFFFFFF
   if(::FileWriteLong(file_handle,-1)!=sizeof(long))
      return false;
//--- Сохраняем тип объекта
   if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE)
      return false;

//--- Сохраняем идентификатор
   if(::FileWriteInteger(file_handle,this.m_id,INT_VALUE)!=INT_VALUE)
      return false;
//--- Сохраняем наименование
   if(::FileWriteArray(file_handle,this.m_name)!=sizeof(this.m_name))
      return false;

И есть одинаковые методы Description и Print. Значит, целесообразно перенести эти строки в методы загрузки/сохранения в базовый объект. Тогда их не придётся писать в каждом новом методе загрузки/сохранения в каждом новом классе, где предусмотрена работа с файлами.

Объявим эти методы в базовом объекте:

public:
//--- Устанавливает (1) наименование, (2) идентификатор
   void              SetName(const string name)                { ::StringToShortArray(name,this.m_name);    }
   void              SetID(const int id)                       { this.m_id=id;                              }
//--- Возвращает (1) наименование, (2) идентификатор
   string            Name(void)                          const { return ::ShortArrayToString(this.m_name);  }
   int               ID(void)                            const { return this.m_id;                          }

//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   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) Возвращает, (2) выводит в журнал описание объекта
   virtual string    Description(void);
   virtual void      Print(void);
   
//--- Конструктор/деструктор
                     CBaseObj (void) : m_id(-1) { this.SetName(""); }
                    ~CBaseObj (void) {}
  };

И напишем их реализацию:

//+------------------------------------------------------------------+
//| CBaseObj::Возвращает описание объекта                            |
//+------------------------------------------------------------------+
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::Выводит в журнал описание объекта                      |
//+------------------------------------------------------------------+
void CBaseObj::Print(void)
  {
   ::Print(this.Description());
  }
//+------------------------------------------------------------------+
//| CBaseObj::Сохранение в файл                                      |
//+------------------------------------------------------------------+
bool CBaseObj::Save(const int file_handle)
  {
//--- Проверяем хэндл
   if(file_handle==INVALID_HANDLE)
      return false;
//--- Сохраняем маркер начала данных - 0xFFFFFFFFFFFFFFFF
   if(::FileWriteLong(file_handle,-1)!=sizeof(long))
      return false;
//--- Сохраняем тип объекта
   if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE)
      return false;

//--- Сохраняем идентификатор
   if(::FileWriteInteger(file_handle,this.m_id,INT_VALUE)!=INT_VALUE)
      return false;
//--- Сохраняем наименование
   if(::FileWriteArray(file_handle,this.m_name)!=sizeof(this.m_name))
      return false;
   
//--- Всё успешно
   return true;
  }
//+------------------------------------------------------------------+
//| CBaseObj::Загрузка из файла                                      |
//+------------------------------------------------------------------+
bool CBaseObj::Load(const int file_handle)
  {
//--- Проверяем хэндл
   if(file_handle==INVALID_HANDLE)
      return false;
//--- Загружаем и проверяем маркер начала данных - 0xFFFFFFFFFFFFFFFF
   if(::FileReadLong(file_handle)!=-1)
      return false;
//--- Загружаем тип объекта
   if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type())
      return false;

//--- Загружаем идентификатор
   this.m_id=::FileReadInteger(file_handle,INT_VALUE);
//--- Загружаем наименование
   if(::FileReadArray(file_handle,this.m_name)!=sizeof(this.m_name))
      return false;
   
//--- Всё успешно
   return true;
  }

Теперь для каждого нового класса методы Description и Print можно объявлять и реализовывать только в случае, если их логика отличается от логики, прописанной в этом классе.

А в методах работы с файлами в наследуемых классах, вместо постоянного прописывания одних и тех же строк в каждом методе каждого класса, мы просто будем обращаться к методам работы с файлами этого базового объекта.

Из всех последующих классов этого файла (Base.mqh) удалим все методы Print — они уже есть в базовом объекте, и полностью его повторяют.

Во всех методах работы с файлами удалим такие строки:

//+------------------------------------------------------------------+
//| CColor::Сохранение в файл                                        |
//+------------------------------------------------------------------+
bool CColor::Save(const int file_handle)
  {
//--- Проверяем хэндл
   if(file_handle==INVALID_HANDLE)
      return false;
//--- Сохраняем маркер начала данных - 0xFFFFFFFFFFFFFFFF
   if(::FileWriteLong(file_handle,-1)!=sizeof(long))
      return false;
//--- Сохраняем тип объекта
   if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE)
      return false;

//--- Сохраняем цвет
   if(::FileWriteInteger(file_handle,this.m_color,INT_VALUE)!=INT_VALUE)
      return false;
//--- Сохраняем идентификатор
   if(::FileWriteInteger(file_handle,this.m_id,INT_VALUE)!=INT_VALUE)
      return false;
//--- Сохраняем наименование
   if(::FileWriteArray(file_handle,this.m_name)!=sizeof(this.m_name))
      return false;
   
//--- Всё успешно
   return true;
  }

Теперь вместо этих строк у нас просто прописан вызов метода базового класса:

//+------------------------------------------------------------------+
//| CColor::Сохранение в файл                                        |
//+------------------------------------------------------------------+
bool CColor::Save(const int file_handle)
  {
//--- Сохраняем данные родительского объекта
   if(!CBaseObj::Save(file_handle))
      return false;

//--- Сохраняем цвет
   if(::FileWriteInteger(file_handle,this.m_color,INT_VALUE)!=INT_VALUE)
      return false;
   
//--- Всё успешно
   return true;
  }

Такие изменения уже сделаны во всех методах работы с файлами в этом файле. При написании последующих классов будем учитывать эти изменения.

В классе CColorElement заменим в констркторах классов одинаковые повторяющиеся строки

//+------------------------------------------------------------------+
//| CColorControl::Конструктор с установкой прозрачных цветов объекта|
//+------------------------------------------------------------------+
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);
  }

одним методом Init():

public:
//--- Возвращает новый цвет
   color             NewColor(color base_color, int shift_red, int shift_green, int shift_blue);

//--- Инициализация класса
   void              Init(void);

//--- Инициализация цветов различных состояний

...

Его реализация:

//+------------------------------------------------------------------+
//| CColorControl::Инициализация класса                              |
//+------------------------------------------------------------------+
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);
  }
//+------------------------------------------------------------------+

Если в метод инициализации цвета передаётся прозрачный цвет, то его никак изменять не нужно для любых состояний.

Учтём это в реализации метода:

//+------------------------------------------------------------------+
//| CColorControl::Устанавливает цвета для всех состояний по текущему|
//+------------------------------------------------------------------+
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);   
  }

В классе CBound необходимо добавить метод, возвращающий флаг нахождения курсора внутри прямоугольной области — это потребуется при реализации компонента Controller:

//+------------------------------------------------------------------+
//| Класс прямоугольной области                                      |
//+------------------------------------------------------------------+
class CBound : public CBaseObj
  {
protected:
   CRect             m_bound;                                  // Структура прямоугольной области

public:
//--- Изменяет (1) ширину, (2) высоту, (3) размер ограничивающего прямоугольника
   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);               }
   
//--- Устанавливает координату (1) X, (2) Y, (3) обе координаты ограничивающего прямоугольника
   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) Устанавливает, (2) смещает ограничивающий прямоугольник на указанные координаты/размер смещения
   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);                                   }
   
//--- Возвращает координаты, размеры и границы объекта
   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);}

//--- Возвращает флаг нахождения курсора внутри области
   bool              Contains(const int x,const int y)   const { return this.m_bound.Contains(x,y);                           }
   
//--- Возвращает описание объекта
   virtual string    Description(void);

   
//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   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);                         }
   
//--- Конструкторы/деструктор
                     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); }
  };

Теперь необходимо добавить всё для реализации компонента Controller в базовый класс холста графических элементов CCanvasBase.

При взаимодействии графических элементов с мышкой необходимо отключать некоторые свойства графика, такие как промотку графика колёсиком, меню правой кнопки мышки, и т.п. Это будет делать каждый объект графических элементов. Но при первом запуске необходимо запомнить то состояние свойств графика, каким оно было до запуска программы. А после завершения работы — вернуть всё на место.

Для этого в приватной секции класса CCanvasBase обявим переменные для хранения значений запоминаемых свойств графика и метод для установки запретов свойств графика:

//+------------------------------------------------------------------+
//| Базовый класс холста графических элементов                       |
//+------------------------------------------------------------------+
class CCanvasBase : public CBaseObj
  {
private:
   bool              m_chart_mouse_wheel_flag;                 // Флаг отправки сообщений о прокрутке колёсика мышки
   bool              m_chart_mouse_move_flag;                  // Флаг отправки сообщений о перемещениях курсора мышки
   bool              m_chart_object_create_flag;               // Флаг отправки сообщений о событии создания графического объекта
   bool              m_chart_mouse_scroll_flag;                // Флаг прокрутки графика левой кнопкой и колёсиком мышки
   bool              m_chart_context_menu_flag;                // Флаг доступа к контекстному меню по нажатию правой клавиши мышки
   bool              m_chart_crosshair_tool_flag;              // Флаг доступа к инструменту "Перекрестие" по нажатию средней клавиши мышки
   bool              m_flags_state;                            // Состояние флагов прокрутки графика колёсиком, контекстного меню и перекрестия
   
//--- Установка запретов для графика (прокрутка колёсиком, контекстное меню и перекрестие)
   void              SetFlags(const bool flag);
   
protected:

У графических элементов может быть два состояния (может и больше, но пока — два). Например, для кнопки — нажата, отжата. Значит, нам необходимо контролировать состояния цветов элемента в двух его состояниях. В защищённой секции класса определим переменную для хранения состояния элемента, ещё один набор объектов управления цветом и разделим контроль прозрачности для канваса фона и переднего плана по раздельности:

protected:
   CCanvas           m_background;                             // Канвас для рисования фона
   CCanvas           m_foreground;                             // Канвас для рисования переднего плана
   CBound            m_bound;                                  // Границы объекта
   CCanvasBase      *m_container;                              // Родительский объект-контейнер
   CColorElement     m_color_background;                       // Объект управления цветом фона
   CColorElement     m_color_foreground;                       // Объект управления цветом переднего плана
   CColorElement     m_color_border;                           // Объект управления цветом рамки
   
   CColorElement     m_color_background_act;                   // Объект управления цветом фона активированного элемента
   CColorElement     m_color_foreground_act;                   // Объект управления цветом переднего плана активированного элемента
   CColorElement     m_color_border_act;                       // Объект управления цветом рамки активированного элемента
   
   ENUM_ELEMENT_STATE m_state;                                 // Состояние элемента (напр., кнопки (вкл/выкл))
   long              m_chart_id;                               // Идентификатор графика
   int               m_wnd;                                    // Номер подокна графика
   int               m_wnd_y;                                  // Смещение координаты Y курсора в подокне
   int               m_obj_x;                                  // Координата X графического объекта
   int               m_obj_y;                                  // Координата Y графического объекта
   uchar             m_alpha_bg;                               // Прозрачность фона
   uchar             m_alpha_fg;                               // Прозрачность переднего плана
   uint              m_border_width;                           // Ширина рамки
   string            m_program_name;                           // Имя программы
   bool              m_hidden;                                 // Флаг скрытого объекта
   bool              m_blocked;                                // Флаг заблокированного элемента
   bool              m_focused;                                // Флаг элемента в фокусе

Здесь же объявим методы для контроля курсора мышки, управления цветами и виртуальные обработчики событий:

//--- Ограничивает графический объект по размерам контейнера
   virtual void      ObjectTrim(void);
//--- Возвращает флаг нахождения курсора внутри объекта
   bool              Contains(const int x,const int y);

   
//--- Проверяет установленный цвет на равенство указанному
   bool              CheckColor(const ENUM_COLOR_STATE state) const;
//--- Изменяет цвета фона, текста и рамки в зависимости от условия
   void              ColorChange(const ENUM_COLOR_STATE state);
   
//--- Инициализация (1) объекта класса, (2) цветов объекта по умолчанию
   void              Init(void);
   virtual void      InitColors(void);

//--- Обработчики событий (1) наведения курсора (Focus), (2) нажатий кнопок мышки (Press), (3) прокрутки колёсика (Wheel),
//--- (4) ухода из фокуса (Release), (5) создания графического объекта (Create). Должны определяться в наследниках
   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;   }  // обработчик здесь отключен
//--- Обработчики пользовательских событий элемента при наведении курсора, щелчке и прокрутке колёсика в области объекта
   virtual void      MouseMoveHandler(const int id, const long lparam, const double dparam, const string sparam)  { return;   }  // обработчик здесь отключен
   virtual void      MousePressHandler(const int id, const long lparam, const double dparam, const string sparam) { return;   }  // обработчик здесь отключен
   virtual void      MouseWheelHandler(const int id, const long lparam, const double dparam, const string sparam) { return;   }  // обработчик здесь отключен
   
public:

В публичной секции класса добавим методы для получения объектов управления цветом элемента в активированном состоянии и методы для получения цветов в различных состояниях элемента:

public:
//--- Возвращает указатель на канвас (1) фона, (2) переднего плана
   CCanvas          *GetBackground(void)                       { return &this.m_background;                                                        }
   CCanvas          *GetForeground(void)                       { return &this.m_foreground;                                                        }
   
//--- Возвращает указатель на объект управления цветом (1) фона, (2) переднего плана, (3) рамки
   CColorElement    *GetBackColorControl(void)                 { return &this.m_color_background;                                                  }
   CColorElement    *GetForeColorControl(void)                 { return &this.m_color_foreground;                                                  }
   CColorElement    *GetBorderColorControl(void)               { return &this.m_color_border;                                                      }
   
//--- Возвращает указатель на объект управления цветом (1) фона, (2) переднего плана, (3) рамки активированного элемента
   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;                                                  }
   
//--- Возврат текущего цвета (1) фона, (2) переднего плана, (3) рамки
   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());      }
   
//--- Возврат предустановленного цвета DEFAULT (1) фона, (2) переднего плана, (3) рамки
   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());      }
   
//--- Возврат предустановленного цвета FOCUSED (1) фона, (2) переднего плана, (3) рамки
   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());      }
   
//--- Возврат предустановленного цвета PRESSED (1) фона, (2) переднего плана, (3) рамки
   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());      }
   
//--- Возврат предустановленного цвета BLOCKED (1) фона, (2) переднего плана, (3) рамки
   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();                                          }
   
//--- Установка цветов фона для всех состояний

Теперь в каждом из методов получения цвета проверяется состояние элемента (активирован/деактивирован) и возвращается требуемый цвет в соответствии с состоянием элемента.

Добавим методы для установки цветов активированного элемента и доработаем методы для установки цветов состояний элемента относительно курсора мышки с учётом состояния элемента как активированного/неактивированного:

//--- Установка цветов фона для всех состояний
   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);                                      }

//--- Установка цветов переднего плана для всех состояний
   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);                                      }

//--- Установка цветов рамки для всех состояний
   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);                                          }

//--- Инициализация цвета (1) фона, (2) переднего плана, (3) рамки начальными значениями
   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);                                         }
   
//--- Инициализация цвета (1) фона, (2) переднего плана, (3) рамки при наведении курсора начальными значениями
   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);                                         }
   
//--- Инициализация цвета (1) фона, (2) переднего плана, (3) рамки при щелчке по объекту начальными значениями
   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);                                         }
   
//--- Установка текущего цвета фона в различные состояния
   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);  }
   
//--- Установка текущего цвета переднего плана в различные состояния
   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);  }
   
//--- Установка текущего цвета рамки в различные состояния
   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);      }

Добавим методы для установки и возврата состояния элемента:

//--- Создаёт 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) Устанавливает, (2) возвращает состояние
   void              SetState(ENUM_ELEMENT_STATE state)        { this.m_state=state; this.ColorsToDefault();                                       }
   ENUM_ELEMENT_STATE State(void)                        const { return this.m_state;                                                              }

//--- Возвращает (1) принадлежность объекта программе, флаг (2) скрытого, (3) заблокированного элемента (4) в фокусе, (5) имя графического объекта (фон, текст)

При установке состояния элемента, необходимо после установки флага состояния все цвета элемента записать как текущие — если элемент активирован, например, кнопка нажата, то все текущие цвета устанавливаются как цвета для нажатой кнопки. В ином случае — текущие цвета берутся из списка цветов не нажатой кнопки.

Так как теперь мы разделили установку и возврат прозрачности для фона и переднего плана, то добавим новые методы для установки и возврата прозрачности:

   string            NameBG(void)                        const { return this.m_background.ChartObjectName();                                       }
   string            NameFG(void)                        const { return this.m_foreground.ChartObjectName();                                       }
   
//--- (1) Возвращает, (2) устанавливает прозрачность фона
   uchar             AlphaBG(void)                       const { return this.m_alpha_bg;                                                           }
   void              SetAlphaBG(const uchar value)             { this.m_alpha_bg=value;                                                            }
//--- (1) Возвращает, (2) устанавливает прозрачность переднего плана
   uchar             AlphaFG(void)                       const { return this.m_alpha_fg;                                                           }
   void              SetAlphaFG(const uchar value)             { this.m_alpha_fg=value;                                                            }

//--- Устанавливает прозрачность для фона и переднего плана
   void              SetAlpha(const uchar value)               { this.m_alpha_fg=this.m_alpha_bg=value;                                            }
   
//--- (1) Возвращает, (2) устанавливает ширину рамки

Объявим обработчик событий, который должен будет вызываться из обработчика событий управляющей программы:

//--- Обработчик событий                                               |
   void              OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam);
   
//--- Конструкторы/деструктор
                     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);
  };

В конструкторе сделаем правильное указание свойств шрифта, рисуемого на канвасе и вызовем метод Init() для запоминания свойств графика и мышки:

//+------------------------------------------------------------------+
//| CCanvasBase::Конструктор                                         |
//+------------------------------------------------------------------+
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)
  {
//--- Получаем скорректированный идентификатор графика и дистанцию в пикселях по вертикальной оси Y
//--- между верхней рамкой подокна индикатора и верхней рамкой главного окна графика
   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(this.Create(this.m_chart_id,this.m_wnd,object_name,x,y,w,h))
     {
      //--- Очищаем канвасы фона и переднего плана и устанавливаем начальные значения координат,
      //--- наименования графических объектов и свойства текста, рисуемого на переднем плане
      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");
      
      //--- Запоминаем разрешения для мышки и инструментов графика
      this.Init();
     }
  }

В деструкторе класса уничтожаем созданный графический объект и восстанавливаем запомненные свойства графика и разрешений для мышки:

//+------------------------------------------------------------------+
//| CCanvasBase::Деструктор                                          |
//+------------------------------------------------------------------+
CCanvasBase::~CCanvasBase(void)
  {
//--- Уничтожаем объект
   this.Destroy();
//--- Возвращаем разрешения для мышки и инструментов графика
   ::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);
  }

В методе создания графического элемента имя создаваемого графического объекта не далжно иметь пробелов. Исправим это, заменив пробелы в имени знаками подчёркивания:

//+------------------------------------------------------------------+
//| CCanvasBase::Создаёт графические объекты фона и переднего плана  |
//+------------------------------------------------------------------+
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)
  {
//--- Получаем скорректированный идентификатор графика
   long id=this.CorrectChartID(chart_id);
//--- Корректируем переданное имя для объекта
   string nm=object_name;
   ::StringReplace(nm," ","_");
//--- Создаём имя графического объекта для фона и создаём канвас
   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;
     }
//--- Создаём имя графического объекта для переднего плана и создаём канвас
   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;
     }
//--- При успешном создании в свойство графического объекта OBJPROP_TEXT вписываем наименование программы
   ::ObjectSetString(id,this.NameBG(),OBJPROP_TEXT,this.m_program_name);
   ::ObjectSetString(id,this.NameFG(),OBJPROP_TEXT,this.m_program_name);
   
//--- Устанавливаем размеры прямоугольной области и возвращаем true
   this.m_bound.SetXY(x,y);
   this.m_bound.Resize(w,h);
   return true;
  }

Метод, возвращающий флаг нахождения курсора внутри объекта:

//+------------------------------------------------------------------+
//| CCanvasBase::Возвращает флаг нахождения курсора внутри объекта   |
//+------------------------------------------------------------------+
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);
  }

Так как размер объекта и размер канваса могут различаться (метод ObjectTrim меняет размеры канваса, не изменяя размеров объекта), то здесь в качестве границ, внутри которых находится курсор, берётся одно из значений: либо граница объекта, либо соответствующий край канваса. Из метода возвращается флаг нахождения переданных в метод координат внутри полученных границ.

В методе блокировки элемента установка флага блокировки должна находиться передвызовом метода рисования элемента, исправим:

//+------------------------------------------------------------------+
//| CCanvasBase::Блокирует элемент                                   |
//+------------------------------------------------------------------+
void CCanvasBase::Block(const bool chart_redraw)
  {
//--- Если элемент уже заблокирован - уходим
   if(this.m_blocked)
      return;
//--- Устанавливаем текущие цвета как цвета заблокированного элемента, 
//--- устанавливаем флаг блокировки и перерисовываем объект
   this.ColorsToBlocked();
   this.m_blocked=true;
   this.Draw(chart_redraw);
  }

Это исправление позволяет правильно нарисовать заблокированный элемент. До этой правки при рисовании элемента цвета брались от стандартного состояния, а не заблокированного, так как флаг устанавливался уже после блокировки.

Метод для установки запретов для графика:

//+------------------------------------------------------------------+
//| CCanvasBase::Установка запретов для графика                      |
//| (прокрутка колёсиком, контекстное меню и перекрестие)            |
//+------------------------------------------------------------------+
void CCanvasBase::SetFlags(const bool flag)
  {
//--- Если нужно установить флаги, и они уже были установлены ранее - уходим
   if(flag && this.m_flags_state)
      return;
//--- Если нужно сбросить флаги, и они уже были сброшены ранее - уходим
   if(!flag && !this.m_flags_state)
      return;
//--- Устанавливаем требуемый флаг для контекстного меню,
//--- инструмента "перекрестие" и прокрутки графика колёсиком мышки.
//--- После установки запоминаем значение установленного флага
   ::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;
//--- Делаем обновление графика для немедленного применения установленных флагов
   ::ChartRedraw(this.m_chart_id);
  }

Метод инициализация класса:

//+------------------------------------------------------------------+
//| CCanvasBase::Инициализация класса                                |
//+------------------------------------------------------------------+
void CCanvasBase::Init(void)
  {
//--- Запоминаем разрешения для мышки и инструментов графика
   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);
//--- Устанавливаем разрешения для мышки и графика
   ::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);
   
//--- Инициализируем цвета объекта по умолчанию
   this.InitColors();
  }

Метод инициализация цветов объекта по умолчанию:

//+------------------------------------------------------------------+
//| CCanvasBase::Инициализация цветов объекта по умолчанию           |
//+------------------------------------------------------------------+
void CCanvasBase::InitColors(void)
  {
//--- Инициализируем цвета заднего плана для обычного и активированного состояний и делаем его текущим цветом фона
   this.InitBackColors(clrWhiteSmoke);
   this.InitBackColorsAct(clrWhiteSmoke);
   this.BackColorToDefault();
   
//--- Инициализируем цвета переднего плана для обычного и активированного состояний и делаем его текущим цветом текста
   this.InitForeColors(clrBlack);
   this.InitForeColorsAct(clrBlack);
   this.ForeColorToDefault();
   
//--- Инициализируем цвета рамки для обычного и активированного состояний и делаем его текущим цветом рамки
   this.InitBorderColors(clrDarkGray);
   this.InitBorderColorsAct(clrDarkGray);
   this.BorderColorToDefault();
   
//--- Инициализируем цвет рамки и цвет переднего плана для заблокированного элемента
   this.InitBorderColorBlocked(clrLightGray);
   this.InitForeColorBlocked(clrSilver);
  }

Метод, проверяющий установленный цвет на равенство указанному:

//+------------------------------------------------------------------+
//| CCanvasBase::Проверяет установленный цвет на равенство указанному|
//+------------------------------------------------------------------+
bool CCanvasBase::CheckColor(const ENUM_COLOR_STATE state) const
  {
   bool res=true;
 //--- В зависимости от проверяемого события
   switch(state)
     {
//--- проверяем равенство всех STANDARD цветов фона, текста и рамки предустановленным значениям
      case COLOR_STATE_DEFAULT :
        res &=this.BackColor()==this.BackColorDefault();
        res &=this.ForeColor()==this.ForeColorDefault();
        res &=this.BorderColor()==this.BorderColorDefault();
        break;

//--- проверяем равенство всех FOCUSED цветов фона, текста и рамки предустановленным значениям
      case COLOR_STATE_FOCUSED :
        res &=this.BackColor()==this.BackColorFocused();
        res &=this.ForeColor()==this.ForeColorFocused();
        res &=this.BorderColor()==this.BorderColorFocused();
        break;
     
//--- проверяем равенство всех PRESSED цветов фона, текста и рамки предустановленным значениям
      case COLOR_STATE_PRESSED :
        res &=this.BackColor()==this.BackColorPressed();
        res &=this.ForeColor()==this.ForeColorPressed();
        res &=this.BorderColor()==this.BorderColorPressed();
        break;
     
//--- проверяем равенство всех BLOCKED цветов фона, текста и рамки предустановленным значениям
      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;
  }

Для того, чтобы цвета элемента менять только в момент переключения состояния элемента, этот метод возвращает флаг уже установленных цветов, соответствующих состоянию элемента. Если текущие цвета элемента не равны цветам, установленным для проверяемого состояния, то метод разрешает смену цвета и перерисовку графического элемента. Если цвета уже установлены в соответствии с состоянием элемента, то менять цвета и перерисовывать объект нет необходимости — метод запрещает смену цвета.

Метод, меняющий цвета элементов объекта по событию:

//+------------------------------------------------------------------+
//| CCanvasBase::Смена цвета элементов объекта по событию            |
//+------------------------------------------------------------------+
void CCanvasBase::ColorChange(const ENUM_COLOR_STATE state)
  {
//--- В зависимости от события устанавливаем цвета события как основные
   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;
     }
  }

В зависимости от события, по которому необходимо сменить цвет, устанавливаются текущие цвета, в соответствии с событием (состоянием элемента).

Обработчик событий:

//+------------------------------------------------------------------+
//| CCanvasBase::Обработчик событий                                  |
//+------------------------------------------------------------------+
void CCanvasBase::OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
//--- Если изменение графика
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      //--- скорректируем дистанцию между верхней рамкой подокна индикатора и верхней рамкой главного окна графика
      this.m_wnd_y=(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOW_YDISTANCE,this.m_wnd);
     }

//--- Если элемент заблокирован или скрыт - уходим
   if(this.IsBlocked() || this.IsHidden())
      return;
      
//--- Координаты курсора мышки
   int x=(int)lparam;
   int y=(int)dparam-this.m_wnd_y;  // Корректируем Y по высоте окна индикатора

//--- Событие перемещения курсора, либо щелчка кнопкой мышки
   if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Если курсор в пределах объекта
      if(this.Contains(x, y))
        {
         //--- Если объект не в составе контейнера - запрещаем прокрутку графика, контекстное меню и инструмент "Перекрестие"
         if(this.m_container==NULL)
            this.SetFlags(false);
         //--- Получаем состояние кнопок мышки, если нажаты - вызываем обработчик нажатий
         if(sparam=="1" || sparam=="2" || sparam=="16")
            this.OnPressEvent(id, lparam, dparam, sparam);
         //--- кнопки не нажаты - обрабатываем перемещение курсора
         else
            this.OnFocusEvent(id, lparam, dparam, sparam);
        }
      //--- Курсор за пределами объекта
      else
        {
         //--- Обрабатываем увод курсора за границы объекта
         this.OnReleaseEvent(id,lparam,dparam,sparam);
         //--- Если объект не в составе контейнера - разрешаем прокрутку графика, контекстное меню и инструмент "Перекрестие"
         if(this.m_container==NULL)
            this.SetFlags(true);
        }
     }
     
//--- Событие прокрутки колёсика мышки
   if(id==CHARTEVENT_MOUSE_WHEEL)
     {
      this.OnWheelEvent(id,lparam,dparam,sparam);
     }

//--- Событие создания графического объекта
   if(id==CHARTEVENT_OBJECT_CREATE)
     {
      this.OnCreateEvent(id,lparam,dparam,sparam);
     }
     
//--- Если пришло пользовательское событие графика
   if(id>CHARTEVENT_CUSTOM)
     {
      //--- собственные события не обрабатываем
      if(sparam==this.NameBG())
         return;

      //--- приводим пользовательское событие в соответствие со стандартными
      ENUM_CHART_EVENT chart_event=ENUM_CHART_EVENT(id-CHARTEVENT_CUSTOM);
      //--- Если щелчок мышки по объекту
      if(chart_event==CHARTEVENT_OBJECT_CLICK)
        {
         this.MousePressHandler(chart_event, lparam, dparam, sparam);
        }
      //--- Если перемещение курсора мышки
      if(chart_event==CHARTEVENT_MOUSE_MOVE)
        {
         this.MouseMoveHandler(chart_event, lparam, dparam, sparam);
        }
      //--- Если прокрутка колёсика мышки
      if(chart_event==CHARTEVENT_MOUSE_WHEEL)
        {
         this.MouseWheelHandler(chart_event, lparam, dparam, sparam);
        }
     }
  }

В базовом объекте графических элементов организована логика обработки взаимодействия курсора мышки с графическими элементами. На различные отслеживаемые события вызываются виртуальные обработчики. Некоторые обработчики реализованы прямо в этом классе, а некоторые просто ничего не делают, и их необходимо реализовывать в объектах-наследниках данного класса.

Обработчики событий, чьё имя заканчивается на *Handler, предназначены для обработки взаимодействия внутри элементов управления между составляющими их компонентами. Тогда, как обработчики, имеющие в своём названии *Event, напрямую обрабатывают события графика и отправляют на график пользовательские события, по которым можно определить тип события, и с какого элемента управления оно отправлено. Это даст возможность обрабатывать такие события пользователю в своей программе.

Обработчик ухода из фокуса:

//+------------------------------------------------------------------+
//| CCanvasBase::Обработчик ухода из фокуса                          |
//+------------------------------------------------------------------+
void CCanvasBase::OnReleaseEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Элемент не в фокусе при уводе курсора
   this.m_focused=false;
//--- восстанавливаем исходные цвета, сбрасываем флаг Focused и перерисовываем объект
   if(!this.CheckColor(COLOR_STATE_DEFAULT))
     {
      this.ColorChange(COLOR_STATE_DEFAULT);
      this.Draw(true);
     }
  }

Обработчик наведения курсора:

//+------------------------------------------------------------------+
//| CCanvasBase::Обработчик наведения курсора                        |
//+------------------------------------------------------------------+
void CCanvasBase::OnFocusEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Элемент в фокусе
   this.m_focused=true;
//--- Если цвета объекта не для режима Focused
   if(!this.CheckColor(COLOR_STATE_FOCUSED))
     {
      //--- устанавливаем цвета и флаг Focused и перерисовываем объект
      this.ColorChange(COLOR_STATE_FOCUSED);
      this.Draw(true);
     }
  }

Обработчик нажатия на объект:

//+------------------------------------------------------------------+
//| CCanvasBase::Обработчик нажатия на объект                        |
//+------------------------------------------------------------------+
void CCanvasBase::OnPressEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Элемент в фокусе при щелчке по нему
   this.m_focused=true;
//--- Если цвета объекта не для режима Pressed
   if(!this.CheckColor(COLOR_STATE_PRESSED))
     {
      //--- устанавливаем цвета Pressed и перерисовываем объект
      this.ColorChange(COLOR_STATE_PRESSED);
      this.Draw(true);
     }

   //--- отправляем пользовательское событие на график с передаенными значениями в lparam, dparam, и именем объекта в sparam
   ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_OBJECT_CLICK, lparam, dparam, this.NameBG());
  }

Обработчик события создания графического объекта:

//+------------------------------------------------------------------+
//| CCanvasBase::Обработчик события создания графического объекта    |
//+------------------------------------------------------------------+
void CCanvasBase::OnCreateEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- если это объект, принадлежащий этой программе - уходим
   if(this.IsBelongsToThis(sparam))
      return;
//--- переносим объект на передний план
   this.BringToTop(true);
  }

Логика всех обработчиков подробно прокомментирована в коде. По сути, здесь лишь организована реакция на событие в виде изменения цветов графического элемента и, где необходимо — отправка пользовательских событий на график. Последний обработчик реагирует на создание графического объекта на графике и переносит графические элементы на передний план. Это позволит, например, панели всегда оставаться на переднем плане.

Все эти обработчики — виртуальные, и при необходимости должны переопределяться в наследуемых классах.

С доработкой базового объекта всех графических элементов мы закончили. Теперь, на основе созданного компонента Controller в базовом объекте и ранее созданного компонента View, начнём создавать простейшие графические элементы (тоже являются частью компонента View). И они станут теми "кирпичиками", из которых в итоге будут созданы сложные элементы управления, и в частности — элемент управления Table View, над созданием которого мы работаем на протяжении нескольких статей.


Простые элементы управления

В той же папке \MQL5\Indicators\Tables\Controls\ создадим новый включаемый файл Controls.mqh.

К созданному файлу подключим файл базового объекта графических элементов Base.mqh и добавим некоторые макроподстановки и перечисления:

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

//+------------------------------------------------------------------+
//| Включаемые библиотеки                                            |
//+------------------------------------------------------------------+
#include "Base.mqh"

//+------------------------------------------------------------------+
//| Макроподстановки                                                 |
//+------------------------------------------------------------------+
#define  DEF_LABEL_W          40          // Ширина текстовой метки по умолчанию
#define  DEF_LABEL_H          16          // Высота текстовой метки по умолчанию
#define  DEF_BUTTON_W         50          // Ширина кнопки по умолчанию
#define  DEF_BUTTON_H         16          // Высота кнопки по умолчанию

//+------------------------------------------------------------------+
//| Перечисления                                                     |
//+------------------------------------------------------------------+
enum ENUM_ELEMENT_COMPARE_BY              // Сравниваемые свойства
  {
   ELEMENT_SORT_BY_ID   =  0,             // Сравнение по идентификатору элемента
   ELEMENT_SORT_BY_NAME,                  // Сравнение по наименованию элемента
   ELEMENT_SORT_BY_TEXT,                  // Сравнение по тексту элемента
   ELEMENT_SORT_BY_COLOR,                 // Сравнение по цвету элемента
   ELEMENT_SORT_BY_ALPHA,                 // Сравнение по прозрачности элемента
   ELEMENT_SORT_BY_STATE,                 // Сравнение по состоянию элемента
  };
//+------------------------------------------------------------------+ 
//| Функции                                                          |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Классы                                                           |
//+------------------------------------------------------------------+

В макроподстановках мы определили размеры по умолчанию для текстовых меток и кнопок. В перечислении мы указали имеющиеся свойства базового графического элемента. По этим свойствам можно будет искать объекты, сортировать и сравнивать. При добавлении новых свойств к каким-либо объектам, в это перечисление будем добавлять новые константы.


Вспомогательные классы

Каждый графический элемент может иметь в своём составе изображение. Это даст возможность рисовать иконки для кнопок, строк текста, и т.п.
Создадим специальный класс для рисования изображений, который будет нетъемлемой частью простых элементов управления.

Класс для рисования изображений в указанной области

Объект этого класса будет объявлен в классе элемента управления и будет предоставлять возможность указать размеры области и её координаты, внутри которой будет рисоваться изображение. Класс наделим методами для рисования стрелок для кнопок со стрелками, чекбоксов и радиокнопок. В последствии добавим методы для рисования других иконок и метод для рисования собственных изображений. В класс будем передавать указатель на канвас, на котором производится рисование в координатах и границах, установленных в классе объекта рисования:
//+------------------------------------------------------------------+
//| Классы                                                           |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Класс рисования изображений                                      |
//+------------------------------------------------------------------+
class CImagePainter : public CBaseObj
  {
protected:
   CCanvas          *m_canvas;                                 // Указатель на канвас, где рисуем
   CBound            m_bound;                                  // Координаты и границы изображения
   uchar             m_alpha;                                  // Прозрачность
   
//--- Проверяет валидность холста и корректность размеров
   bool              CheckBound(void);

public:
//--- (1) Назначает канвас для рисования, (2) устанавливает, (3) возвращает прозрачность
   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) Устанавливает координаты, (2) изменяет размеры области
   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);            }
//--- Устанавливает координаты и размеры области
   void              SetBound(const int x,const int y,const int w,const int h)
                       {
                        this.SetXY(x,y);
                        this.SetSize(w,h);
                       }

//--- Возвращает границы и размеры рисунка
   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();        }
   
//--- Очищает область
   bool              Clear(const int x,const int y,const int w,const int h,const bool update=true);
//--- Рисует закрашенную стрелку (1) вверх, (2) вниз, (3) влево, (4) вправо
   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);
   
//--- Рисует (1) отмеченный, (2) неотмеченный 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);
   
//--- Рисует (1) отмеченный, (2) неотмеченный 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);
   
//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   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);  }
   
//--- Конструкторы/деструктор
                     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) {}
  };

Рассмотрим методы класса.

Метод для сравнения двух объектов рисования:

//+------------------------------------------------------------------+
//| CImagePainter::Сравнение двух объектов                           |
//+------------------------------------------------------------------+
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);
     }
  }

Метод необходим для поиска требуемого объекта рисования. По умолчанию поиск ведётся по идентификатору объекта. Метод потребуется, когда в объектах элементов управления будут списки, в которых хранятся объекты рисования. На данный момент в каждом элементе управления будет объявлен один объект рисования, предназначенный для рисования главной иконки элемента.

Метод, проверяющий валидность холста и корректность размеров области изображения:

//+------------------------------------------------------------------+
//|CImagePainter::Проверяет валидность холста и корректность размеров|
//+------------------------------------------------------------------+
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;
  }

Если в объект не передан указатель на канвас, либо не установлены ширина и высота области изображения — метод возвращает false. Иначе — true.

Метод, очищающий область изображения:

//+------------------------------------------------------------------+
//| CImagePainter::Очищает область                                   |
//+------------------------------------------------------------------+
bool CImagePainter::Clear(const int x,const int y,const int w,const int h,const bool update=true)
  {
//--- Если область изображения не валидна - возвращаем false
   if(!this.CheckBound())
      return false;
//--- Очищаем прозрачным цветом всю область изображения
   this.m_canvas.FillRectangle(x,y,x+w-1,y+h-1,clrNULL);
//--- Если указано - обновляем канвас
   if(update)
      this.m_canvas.Update(false);
//--- Всё успешно
   return true;   
  }

Метод полностью очищает всю область изображения, заливая её прозрачным цветом.

Метод, рисующий закрашенную стрелку вверх:

//+------------------------------------------------------------------+
//| CImagePainter::Рисует закрашенную стрелку вверх                  |
//+------------------------------------------------------------------+
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)
  {
//--- Если область изображения не валидна - возвращаем false
   if(!this.CheckBound())
      return false;

//--- Рассчитываем координаты углов стрелки внутри области изображения
   int hw=(int)::floor(w/2);  // Половина ширины
   if(hw==0)
      hw=1;

   int x1 = x + 1;            // X. Основание (левая точка)
   int y1 = y + h - 4;        // Y. Левая точка основания
   int x2 = x1 + hw;          // X. Вершина (центральная верхняя точка)
   int y2 = y + 3;            // Y. Вершина (верхняя точка)
   int x3 = x1 + w - 1;       // X. Основание (правая точка)
   int y3 = y1;               // Y. Основание (правая точка)

//--- Рисуем треугольник
   this.m_canvas.FillTriangle(x1, y1, x2, y2, x3, y3, ::ColorToARGB(clr, alpha));
   if(update)
      this.m_canvas.Update(false);
   return true;   
  }

Метод, рисующий закрашенную стрелку вниз:

//+------------------------------------------------------------------+
//| CImagePainter::Рисует закрашенную стрелку вниз                   |
//+------------------------------------------------------------------+
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)
  {
//--- Если область изображения не валидна - возвращаем false
   if(!this.CheckBound())
      return false;

//--- Рассчитываем координаты углов стрелки внутри области изображения
   int hw=(int)::floor(w/2);  // Половина ширины
   if(hw==0)
      hw=1;

   int x1=x+1;                // X. Основание (левая точка)
   int y1=y+4;                // Y. Левая точка основания
   int x2=x1+hw;              // X. Вершина (центральная нижняя точка)
   int y2=y+h-3;              // Y. Вершина (нижняя точка)
   int x3=x1+w-1;             // X. Основание (правая точка)
   int y3=y1;                 // Y. Основание (правая точка)

//--- Рисуем треугольник
   this.m_canvas.FillTriangle(x1, y1, x2, y2, x3, y3, ::ColorToARGB(clr, alpha));
   if(update)
      this.m_canvas.Update(false);
   return true;   
  }

Метод, рисующий закрашенную стрелку влево:

//+------------------------------------------------------------------+
//| CImagePainter::Рисует закрашенную стрелку влево                  |
//+------------------------------------------------------------------+
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)
  {
//--- Если область изображения не валидна - возвращаем false
   if(!this.CheckBound())
      return false;

//--- Рассчитываем координаты углов стрелки внутри области изображения
   int hh=(int)::floor(h/2);  // Половина высоты
   if(hh==0)
      hh=1;

   int x1=x+w-4;              // X. Основание (правая сторона)
   int y1=y+1;                // Y. Верхний угол основания
   int x2=x+3;                // X. Вершина (левая центральная точка)
   int y2=y1+hh;              // Y. Центральная точка (вершина)
   int x3=x1;                 // X. Нижний угол основания
   int y3=y1+h-1;             // Y. Нижний угол основания

//--- Рисуем треугольник
   this.m_canvas.FillTriangle(x1, y1, x2, y2, x3, y3, ::ColorToARGB(clr, alpha));

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

Метод, рисующий закрашенную стрелку вправо:

//+------------------------------------------------------------------+
//| CImagePainter::Рисует закрашенную стрелку вправо                 |
//+------------------------------------------------------------------+
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)
  {
//--- Если область изображения не валидна - возвращаем false
   if(!this.CheckBound())
      return false;

//--- Рассчитываем координаты углов стрелки внутри области изображения
   int hh=(int)::floor(h/2);  // Половина высоты
   if(hh==0)
      hh=1;

   int x1=x+4;                // X. Основание треугольника (левая сторона)
   int y1=y+1;                // Y. Верхний угол основания
   int x2=x+w-3;              // X. Вершина (правая центральная точка)
   int y2=y1+hh;              // Y. Центральная точка (вершина)
   int x3=x1;                 // X. Нижний угол основания
   int y3=y1+h-1;             // Y. Нижний угол основания

//--- Рисуем треугольник
   this.m_canvas.FillTriangle(x1, y1, x2, y2, x3, y3, ::ColorToARGB(clr, alpha));
   if(update)
      this.m_canvas.Update(false);
   return true;
  }

Внутри области изображения определяется область для стрелки с отступом по одному пикселю с каждой из сторон прямоугольной области, и внутри рисуется закрашенная стрелка.

Метод, рисующий отмеченный CheckBox:

//+------------------------------------------------------------------+
//| CImagePainter::Рисует отмеченный 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)
  {
//--- Если область изображения не валидна - возвращаем false
   if(!this.CheckBound())
      return false;

//--- Координаты прямоугольника
   int x1=x+1;                // Левый верхний угол, X
   int y1=y+1;                // Левый верхний угол, Y
   int x2=x+w-2;              // Правый нижний угол, X
   int y2=y+h-2;              // Правый нижний угол, Y

//--- Рисуем прямоугольник
   this.m_canvas.Rectangle(x1, y1, x2, y2, ::ColorToARGB(clr, alpha));
   
//--- Координаты "галочки"
   int arrx[3], arry[3];
   
   arrx[0]=x1+(x2-x1)/4;      // X. Левая точка
   arrx[1]=x1+w/3;            // X. Центральная точка
   arrx[2]=x2-(x2-x1)/4;      // X. Правая точка
   
   arry[0]=y1+1+(y2-y1)/2;    // Y. Левая точка
   arry[1]=y2-(y2-y1)/3;      // Y. Центральная точка
   arry[2]=y1+(y2-y1)/3;      // Y. Правая точка
   
//--- Рисуем "галочку" линией двойной толщины
   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;
  }

Метод, рисующий неотмеченный CheckBox:

//+------------------------------------------------------------------+
//| CImagePainter::Рисует неотмеченный 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)
  {
//--- Если область изображения не валидна - возвращаем false
   if(!this.CheckBound())
      return false;

//--- Координаты прямоугольника
   int x1=x+1;                // Левый верхний угол, X
   int y1=y+1;                // Левый верхний угол, Y
   int x2=x+w-2;              // Правый нижний угол, X
   int y2=y+h-2;              // Правый нижний угол, Y

//--- Рисуем прямоугольник
   this.m_canvas.Rectangle(x1, y1, x2, y2, ::ColorToARGB(clr, alpha));
   
   if(update)
      this.m_canvas.Update(false);
   return true;
  }

Метод, рисующий отмеченный RadioButton:

//+------------------------------------------------------------------+
//| CImagePainter::Рисует отмеченный 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)
  {
//--- Если область изображения не валидна - возвращаем false
   if(!this.CheckBound())
      return false;

//--- Координаты и радиус окружности
   int x1=x+1;                // Левый верхний угол области окружности, X
   int y1=y+1;                // Левый верхний угол области окружности, Y
   int x2=x+w-2;              // Правый нижний угол области окружности, X
   int y2=y+h-2;              // Правый нижний угол области окружности, Y
   
//--- Координаты и радиус окружности
   int d=::fmin(x2-x1,y2-y1); // Диаметр по меньшей стороне (ширина или высота)
   int r=d/2;                 // Радиус
   int cx=x1+r;               // Координата X центра
   int cy=y1+r;               // Координата Y центра

//--- Рисуем окружность
   this.m_canvas.CircleWu(cx, cy, r, ::ColorToARGB(clr, alpha));
   
//--- Радиус "метки"
   r/=2;
   if(r<1)
      r=1;
//--- Рисуем метку
   this.m_canvas.FillCircle(cx, cy, r, ::ColorToARGB(clr, alpha));
   
   if(update)
      this.m_canvas.Update(false);
   return true;
  }

Метод, рисующий неотмеченный RadioButton:

//+------------------------------------------------------------------+
//| CImagePainter::Рисует неотмеченный 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)
  {
//--- Если область изображения не валидна - возвращаем false
   if(!this.CheckBound())
      return false;

//--- Координаты и радиус окружности
   int x1=x+1;                // Левый верхний угол области окружности, X
   int y1=y+1;                // Левый верхний угол области окружности, Y
   int x2=x+w-2;              // Правый нижний угол области окружности, X
   int y2=y+h-2;              // Правый нижний угол области окружности, Y
   
//--- Координаты и радиус окружности
   int d=::fmin(x2-x1,y2-y1); // Диаметр по меньшей стороне (ширина или высота)
   int r=d/2;                 // Радиус
   int cx=x1+r;               // Координата X центра
   int cy=y1+r;               // Координата Y центра

//--- Рисуем окружность
   this.m_canvas.CircleWu(cx, cy, r, ::ColorToARGB(clr, alpha));
   
   if(update)
      this.m_canvas.Update(false);
   return true;
  }

Это простые методы, просто дающие возможность рисовать нужные фигуры без необходимости писать их самостоятельно. Далее добавим сюда и другие методы, рисующие иные иконки для оформления графических элементов.

Методы для сохранения области рисунка в файл и загрузки её из файла:

//+------------------------------------------------------------------+
//| CImagePainter::Сохранение в файл                                 |
//+------------------------------------------------------------------+
bool CImagePainter::Save(const int file_handle)
  {
//--- Сохраняем данные родительского объекта
   if(!CBaseObj::Save(file_handle))
      return false;
  
//--- Сохраняем прозрачность
   if(::FileWriteInteger(file_handle,this.m_alpha,INT_VALUE)!=INT_VALUE)
      return false;
//--- Сохраняем данные области
   if(!this.m_bound.Save(file_handle))
      return false;
      
//--- Всё успешно
   return true;
  }
//+------------------------------------------------------------------+
//| CImagePainter::Загрузка из файла                                 |
//+------------------------------------------------------------------+
bool CImagePainter::Load(const int file_handle)
  {
//--- Загружаем данные родительского объекта
   if(!CBaseObj::Load(file_handle))
      return false;
      
//--- Загружаем прозрачность
   this.m_alpha=(uchar)::FileReadInteger(file_handle,INT_VALUE);
//--- Загружаем данные области
   if(!this.m_bound.Load(file_handle))
      return false;
   
//--- Всё успешно
   return true;
  }

Теперь можно приступить к классам простых элементов управления. Минимальным таким объектом будет класс текстовой метки. От этого элемента будут наследоваться классы других элементов управления.
В этом же файле Controls.mqh продолжим писать коды классов.

Класс элемента управления "Текстовая метка"

В этом классе будет набор переменных и методов, позволяющих работать с любым элементом управления, устанавливать и получать параметры объекта, сохранять и загружать его свойства. Интерактивность всех элементов управления (компонент Controller) мы сегодня добавили в базовый класс всех элементов управления. Рассмтрим класс текстовой метки:

//+------------------------------------------------------------------+
//| Класс текстовой метки                                            |
//+------------------------------------------------------------------+
class CLabel : public CCanvasBase
  {
protected:
   CImagePainter     m_painter;                                // Класс рисования
   ushort            m_text[];                                 // Текст
   ushort            m_text_prev[];                            // Прошлый текст
   int               m_text_x;                                 // Координата X текста (смещение относительно  левой границы объекта)
   int               m_text_y;                                 // Координата Y текста (смещение относительно  верхней границы объекта)
   
//--- (1) Устанавливает, (2) возвращает прошлый текст
   void              SetTextPrev(const string text)            { ::StringToShortArray(text,this.m_text_prev);  }
   string            TextPrev(void)                      const { return ::ShortArrayToString(this.m_text_prev);}
      
//--- Стирает текст
   void              ClearText(void);

public:
//--- Возвращает указатель на класс рисования
   CImagePainter    *Painter(void)                             { return &this.m_painter;           }
   
//--- (1) Устанавливает, (2) возвращает текст
   void              SetText(const string text)                { ::StringToShortArray(text,this.m_text);       }
   string            Text(void)                          const { return ::ShortArrayToString(this.m_text);     }
   
//--- Возвращает координату (1) X, (2) Y текста
   int               TextX(void)                         const { return this.m_text_x;                         }
   int               TextY(void)                         const { return this.m_text_y;                         }

//--- Устанавливает координату (1) X, (2) Y текста
   void              SetTextShiftH(const int x)                { this.m_text_x=x;                              }
   void              SetTextShiftV(const int y)                { this.m_text_y=y;                              }
   
//--- (1) Устанавливает координаты, (2) изменяет размеры области изображения
   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);                  }
//--- Устанавливает координаты и размеры области изображения
   void              SetImageBound(const int x,const int y,const int w,const int h)
                       {
                        this.SetImageXY(x,y);
                        this.SetImageSize(w,h);
                       }
//--- Возвращает координату (1) X, (2) Y, (3) ширину, (4) высоту, (5) правую, (6) нижнюю границу области изображения
   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();               }

//--- Выводит текст
   void              DrawText(const int dx, const int dy, const string text, const bool chart_redraw);
   
//--- Рисует внешний вид
   virtual void      Draw(const bool chart_redraw);

//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   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);                   }
   
//--- Конструкторы/деструктор
                     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) {}
  };

В классе определены два ushort-массива символов — для текущего и прошлого текстов метки. Это позволяет при рисовании иметь доступ к размерам прошлого текста, и правильно стирать область, перекрытую текстом, перед тем как вывести новый текст на канвас.

Рассмотрим объявленные методы.

Класс имеет четыре конструктора, позволяющие создать объект по разным наборам параметров:

//+------------------------------------------------------------------+
//| CLabel::Конструктор по умолчанию. Строит метку в главном окне    |
//| текущего графика в координатах 0,0 с размерами по умолчанию      |
//+------------------------------------------------------------------+
CLabel::CLabel(void) : CCanvasBase("Label",::ChartID(),0,0,0,DEF_LABEL_W,DEF_LABEL_H), m_text_x(0), m_text_y(0)
  {
//--- Объекту рисования назначаем канвас переднего плана и
//--- обнуляем координаты и размеры, что делает его неактивным
   this.m_painter.CanvasAssign(this.GetForeground());
   this.m_painter.SetXY(0,0);
   this.m_painter.SetSize(0,0);
//--- Устанавливаем текущий и предыдущий текст
   this.SetText("Label");
   this.SetTextPrev("");
//--- Фон - прозрачный, передний план - нет
   this.SetAlphaBG(0);
   this.SetAlphaFG(255);
  }
//+------------------------------------------------------------------+
//| CLabel::Конструктор параметрический. Строит метку в главном окне |
//| текущего графика с указанными текстом, координами и размерами    |
//+------------------------------------------------------------------+
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)
  {
//--- Объекту рисования назначаем канвас переднего плана и
//--- обнуляем координаты и размеры, что делает его неактивным
   this.m_painter.CanvasAssign(this.GetForeground());
   this.m_painter.SetXY(0,0);
   this.m_painter.SetSize(0,0);
//--- Устанавливаем текущий и предыдущий текст
   this.SetText(text);
   this.SetTextPrev("");
//--- Фон - прозрачный, передний план - нет
   this.SetAlphaBG(0);
   this.SetAlphaFG(255);
  }
//+-------------------------------------------------------------------+
//| CLabel::Конструктор параметрический. Строит метку в указанном окне|
//| текущего графика с указанными текстом, координами и размерами     |
//+-------------------------------------------------------------------+
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)
  {
//--- Объекту рисования назначаем канвас переднего плана и
//--- обнуляем координаты и размеры, что делает его неактивным
   this.m_painter.CanvasAssign(this.GetForeground());
   this.m_painter.SetXY(0,0);
   this.m_painter.SetSize(0,0);
//--- Устанавливаем текущий и предыдущий текст
   this.SetText(text);
   this.SetTextPrev("");
//--- Фон - прозрачный, передний план - нет
   this.SetAlphaBG(0);
   this.SetAlphaFG(255);
  }
//+-------------------------------------------------------------------+
//| CLabel::Конструктор параметрический. Строит метку в указанном окне|
//| указанного графика с указанными текстом, координами и размерами   |
//+-------------------------------------------------------------------+
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)
  {
//--- Объекту рисования назначаем канвас переднего плана и
//--- обнуляем координаты и размеры, что делает его неактивным
   this.m_painter.CanvasAssign(this.GetForeground());
   this.m_painter.SetXY(0,0);
   this.m_painter.SetSize(0,0);
//--- Устанавливаем текущий и предыдущий текст
   this.SetText(text);
   this.SetTextPrev("");
//--- Фон - прозрачный, передний план - нет
   this.SetAlphaBG(0);
   this.SetAlphaFG(255);
  }

Область рисунка (иконки элемента) устанавливается с нулевыми размерами, что означает отсутствие иконки у элемента. Устанавливается текст элемента и задаётся полная прозрачность для фона и полная непрозрачность для переднего плана.

Метод для сравнения двух объектов:

//+------------------------------------------------------------------+
//| CLabel::Сравнение двух объектов                                  |
//+------------------------------------------------------------------+
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);
     }
  }

Сравнение возможно по имени объекта, тексту метки, цвету, прозрачности и идентификатору. По умолчанию объекты сравниваются по идентификатору объекта, так как при нахождении объектов в одном списке, их лучше различать по идентификаторам для быстрого доступа к нужному.

Метод, стирающий текст метки:

//+------------------------------------------------------------------+
//| CLabel::Стирает текст                                            |
//+------------------------------------------------------------------+
void CLabel::ClearText(void)
  {
   int w=0, h=0;
   string text=this.TextPrev();
//--- Получаем размеры прошлого текста
   if(text!="")
      this.m_foreground.TextSize(text,w,h);
//--- Если размеры получены - рисуем на месте текста прозрачный прямоугольник, стирая текст
   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);
//--- Иначе - очищаем полностью весь передний план
   else
      this.m_foreground.Erase(clrNULL);
  }

Если ранее был написан текст, то его можно стереть, закрасив его полностью прозрачным прямоугольником по размерам текста. Если ранее текста не было — стирается вся площадь канваса объекта.

Метод, выводящий текст на канвас:

//+------------------------------------------------------------------+
//| CLabel::Выводит текст                                            |
//+------------------------------------------------------------------+
void CLabel::DrawText(const int dx,const int dy,const string text,const bool chart_redraw)
  {
//--- Очищаем прошлый текст и устанавливаем новый
   this.ClearText();
   this.SetText(text);
//--- Выводим установленный текст
   this.m_foreground.TextOut(this.AdjX(dx),this.AdjY(dy),this.Text(),::ColorToARGB(this.ForeColor(),this.AlphaFG()));
   
//--- Если текст выходит за правую границу объекта
   if(this.Width()-dx<this.m_foreground.TextWidth(text))
     {
      //--- Получаем размеры текста "троеточие"
      int w=0,h=0;
      this.m_foreground.TextSize("... ",w,h);
      if(w>0 && h>0)
        {
         //--- Стираем текст у правой границы объекта по размеру текста "троеточие" и заменяем троеточием окончание текста метки
         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()));
        }
     }
//--- Обновляем канвас переднего плана и запоминаем новые координаты текста
   this.m_foreground.Update(chart_redraw);
   this.m_text_x=dx;
   this.m_text_y=dy;
//--- Запоминаем нарисованный текст как прошлый
   this.SetTextPrev(text);
  }

Здесь сначала стирается прошлый текст на канвасе, а затем выводится новый. Если новый текст выходит за границы объекта, то в месте его выхода за пределы элемента справа выводится троеточие, указывающее, что текст не вместился в область объекта, примерно так: "Этот текст не вмест...".

Метод, рисующий внешний вид:

//+------------------------------------------------------------------+
//| CLabel::Рисует внешний вид                                       |
//+------------------------------------------------------------------+
void CLabel::Draw(const bool chart_redraw)
  {
   this.DrawText(this.m_text_x,this.m_text_y,this.Text(),chart_redraw);
  }

Здесь просто вызывается метод рисования текста метки.

Методы работы с файлами:

//+------------------------------------------------------------------+
//| CLabel::Сохранение в файл                                        |
//+------------------------------------------------------------------+
bool CLabel::Save(const int file_handle)
  {
//--- Сохраняем данные родительского объекта
   if(!CCanvasBase::Save(file_handle))
      return false;
  
//--- Сохраняем текст
   if(::FileWriteArray(file_handle,this.m_text)!=sizeof(this.m_text))
      return false;
//--- Сохраняем предыдущий текст
   if(::FileWriteArray(file_handle,this.m_text_prev)!=sizeof(this.m_text_prev))
      return false;
//--- Сохраняем координату X текста
   if(::FileWriteInteger(file_handle,this.m_text_x,INT_VALUE)!=INT_VALUE)
      return false;
//--- Сохраняем координату Y текста
   if(::FileWriteInteger(file_handle,this.m_text_y,INT_VALUE)!=INT_VALUE)
      return false;
   
//--- Всё успешно
   return true;
  }
//+------------------------------------------------------------------+
//| CLabel::Загрузка из файла                                        |
//+------------------------------------------------------------------+
bool CLabel::Load(const int file_handle)
  {
//--- Загружаем данные родительского объекта
   if(!CCanvasBase::Load(file_handle))
      return false;
      
//--- Загружаем текст
   if(::FileReadArray(file_handle,this.m_text)!=sizeof(this.m_text))
      return false;
//--- Загружаем предыдущий текст
   if(::FileReadArray(file_handle,this.m_text_prev)!=sizeof(this.m_text_prev))
      return false;
//--- Загружаем координату X текста
   this.m_text_x=::FileReadInteger(file_handle,INT_VALUE);
//--- Загружаем координату Y текста
   this.m_text_y=::FileReadInteger(file_handle,INT_VALUE);
   
//--- Всё успешно
   return true;
  }

На основе рассмотренного класса создадим класс простой кнопки.

Класс элемента управления "Простая кнопка"

//+------------------------------------------------------------------+
//| Класс простой кнопки                                             |
//+------------------------------------------------------------------+
class CButton : public CLabel
  {
public:
//--- Рисует внешний вид
   virtual void      Draw(const bool chart_redraw);

//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   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);      }
   
//--- Конструкторы/деструктор
                     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) {}
  };

Класс простой кнопки отличается от класса текстовой метки только методом рисования внешнего вида.

Класс имеет четыре конструктора, позволяющие создать кнопку с указанными параметрами:

//+------------------------------------------------------------------+
//| CButton::Конструктор по умолчанию. Строит кнопку в главном окне  |
//| текущего графика в координатах 0,0 с размерами по умолчанию      |
//+------------------------------------------------------------------+
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::Конструктор параметрический. Строит кнопку в главном окне|
//| текущего графика с указанными текстом, координами и размерами     |
//+-------------------------------------------------------------------+
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::Конструктор параметрический. Строит кнопку в указанном окне|
//| текущего графика с указанными текстом, координами и размерами       |
//+---------------------------------------------------------------------+
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::Конструктор параметрический. Строит кнопку в указанном окне|
//| указанного графика с указанными текстом, координами и размерами     |
//+---------------------------------------------------------------------+
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);
  }

Устанавливаем состояние "кнопка не нажата" и задаём полную непрозрачность для фона и переднего плана.

Метод сравнения двух объектов:

//+------------------------------------------------------------------+
//| CButton::Сравнение двух объектов                                 |
//+------------------------------------------------------------------+
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);
     }
  }

Метод идентичен методу класса текстовой метки. Скорее всего, если иных свойств у кнопок не появится, то этот метод можно будет удалить из класса, и будет использоваться метод родительского класса.

Метод, рисующий внешний вид кнопки:

//+------------------------------------------------------------------+
//| CButton::Рисует внешний вид                                      |
//+------------------------------------------------------------------+
void CButton::Draw(const bool chart_redraw)
  {
//--- Заливаем кнопку цветом фона, рисуем рамку и обновляем канвас фона
   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);
//--- Выводим текст кнопки
   CLabel::Draw(false);
      
//--- Если указано - обновляем график
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Сначала заливаем фон установленным цветом, затем рисуем рамку и выводим текст кнопки.

На основании данного класса создадим класс двухпозиционной кнопки.

Класс элемента управления "Двухпозиционная кнопка"

//+------------------------------------------------------------------+
//| Класс двухпозиционной кнопки                                     |
//+------------------------------------------------------------------+
class CButtonTriggered : public CButton
  {
public:
//--- Рисует внешний вид
   virtual void      Draw(const bool chart_redraw);

//--- Обработчик событий нажатий кнопок мышки (Press)
   virtual void      OnPressEvent(const int id, const long lparam, const double dparam, const string sparam);

//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   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);  }
  
//--- Инициализация цветов объекта по умолчанию
   virtual void      InitColors(void);
   
//--- Конструкторы/деструктор
                     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) {}
  };

Объект имеет четыре конструктора, позволяющих создать кнопку с указанными параметрами:

//+------------------------------------------------------------------+
//| CButtonTriggered::Конструктор по умолчанию.                      |
//| Строит кнопку в главном окне текущего графика                    |
//| в координатах 0,0 с размерами по умолчанию                       |
//+------------------------------------------------------------------+
CButtonTriggered::CButtonTriggered(void) : CButton("Button",::ChartID(),0,"Button",0,0,DEF_BUTTON_W,DEF_BUTTON_H)
  {
   this.InitColors();
  }
//+------------------------------------------------------------------+
//| CButtonTriggered::Конструктор параметрический.                   |
//| Строит кнопку в главном окне текущего графика                    |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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::Конструктор параметрический.                   |
//| Строит кнопку в указанном окне текущего графика                  |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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::Конструктор параметрический.                   |
//| Строит кнопку в указанном окне указанного графика                |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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();
  }

В каждом конструкторе вызывается метод инициализации цветов по умолчанию:

//+------------------------------------------------------------------+
//| CButtonTriggered::Инициализация цветов объекта по умолчанию      |
//+------------------------------------------------------------------+
void CButtonTriggered::InitColors(void)
  {
//--- Инициализируем цвета заднего плана для обычного и активированного состояний и делаем его текущим цветом фона
   this.InitBackColors(clrWhiteSmoke);
   this.InitBackColorsAct(clrLightBlue);
   this.BackColorToDefault();
   
//--- Инициализируем цвета переднего плана для обычного и активированного состояний и делаем его текущим цветом текста
   this.InitForeColors(clrBlack);
   this.InitForeColorsAct(clrBlack);
   this.ForeColorToDefault();
   
//--- Инициализируем цвета рамки для обычного и активированного состояний и делаем его текущим цветом рамки
   this.InitBorderColors(clrDarkGray);
   this.InitBorderColorsAct(clrGreen);
   this.BorderColorToDefault();
   
//--- Инициализируем цвет рамки и цвет переднего плана для заблокированного элемента
   this.InitBorderColorBlocked(clrLightGray);
   this.InitForeColorBlocked(clrSilver);
  }

Это цвета по умолчанию, устанавливаемые для новой созданной кнопки. После создания объекта все цвета можно настроить по своему усмотрению.

В методе сравнениядобавлено сравнение по состоянию кнопки:

//+------------------------------------------------------------------+
//| CButtonTriggered::Сравнение двух объектов                        |
//+------------------------------------------------------------------+
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);
     }
  }

Метод, рисующий внешний вид кнопки:

//+------------------------------------------------------------------+
//| CButtonTriggered::Рисует внешний вид                             |
//+------------------------------------------------------------------+
void CButtonTriggered::Draw(const bool chart_redraw)
  {
//--- Заливаем кнопку цветом фона, рисуем рамку и обновляем канвас фона
   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);
//--- Выводим текст кнопки
   CLabel::Draw(false);
      
//--- Если указано - обновляем график
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Метод идентичен методу родительского класса и, если никаких доработок класса в дальнейшем не будет, то метод можно будет удалить — будет использоваться метод рисования от родительского класса.

Двухпозиционная кнопка имеет два состояния:

  1. Нажата,
  2. Отжата.

Для отслеживания и переключения её состояний здесь переопределён обработчик нажатий кнопок мышки OnPressEvent родительского класса:

//+------------------------------------------------------------------+
//| CButtonTriggered::Обработчик событий нажатий кнопок мышки (Press)|
//+------------------------------------------------------------------+
void CButtonTriggered::OnPressEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Устанавливаем состояние кнопки, обратное уже установленному
   ENUM_ELEMENT_STATE state=(this.State()==ELEMENT_STATE_DEF ? ELEMENT_STATE_ACT : ELEMENT_STATE_DEF);
   this.SetState(state);
   
//--- Вызываем обработчик родительского объекта с указанием идентификатора в lparam и состояния в dparam
   CCanvasBase::OnPressEvent(id,this.m_id,this.m_state,sparam);
  }

На основе класса CButton создадим четыре кнопки со стрелками: вверх, вниз, влево и вправо. Объекты будут использовать класс рисования изображений для рисования стрелок.

Класс элемента управления "Кнопка со стрелкой вверх"

//+------------------------------------------------------------------+
//| Класс кнопки со стрелкой вверх                                   |
//+------------------------------------------------------------------+
class CButtonArrowUp : public CButton
  {
public:
//--- Рисует внешний вид
   virtual void      Draw(const bool chart_redraw);

//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   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);}
   
//--- Конструкторы/деструктор
                     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) {}
  };

Четыре конструктора позволяют создать объект с указанными параметрами:

//+------------------------------------------------------------------+
//| CButtonArrowUp::Конструктор по умолчанию.                        |
//| Строит кнопку в главном окне текущего графика                    |
//| в координатах 0,0 с размерами по умолчанию                       |
//+------------------------------------------------------------------+
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::Конструктор параметрический.                     |
//| Строит кнопку в главном окне текущего графика                    |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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::Конструктор параметрический.                     |
//| Строит кнопку в указанном окне текущего графика                  |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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::Конструктор параметрический.                     |
//| Строит кнопку в указанном окне указанного графика                |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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);
  }

В конструкторах инициализируются цвета по умолчанию и устанавливаются координаты и размеры области изображения.

Метод, рисующий внешний вид кнопки:

//+------------------------------------------------------------------+
//| CButtonArrowUp::Рисует внешний вид                               |
//+------------------------------------------------------------------+
void CButtonArrowUp::Draw(const bool chart_redraw)
  {
//--- Заливаем кнопку цветом фона, рисуем рамку и обновляем канвас фона
   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);
//--- Выводим текст кнопки
   CLabel::Draw(false);
//--- Очищаем область рисунка
   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);
//--- Задаём цвет стрелки для обычного и заблокированного состояний кнопки и рисуем стрелку вверх 
   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(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Метод похож на метод рисования кнопки, но дополнительно выводится рисунок стрелки вверх методом ArrowUp объекта рисования.

Все остальные классы идентичны рассмотренному, но в методах рисования рисуются значки, соответствующие назначению кнопки.

Класс элемента управления "Кнопка со стрелкой вниз"

//+------------------------------------------------------------------+
//| Класс кнопки со стрелкой вниз                                    |
//+------------------------------------------------------------------+
class CButtonArrowDown : public CButton
  {
public:
//--- Рисует внешний вид
   virtual void      Draw(const bool chart_redraw);

//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   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); }
   
//--- Конструкторы/деструктор
                     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::Конструктор по умолчанию.                      |
//| Строит кнопку в главном окне текущего графика                    |
//| в координатах 0,0 с размерами по умолчанию                       |
//+------------------------------------------------------------------+
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::Конструктор параметрический.                   |
//| Строит кнопку в главном окне текущего графика                    |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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::Конструктор параметрический.                   |
//| Строит кнопку в указанном окне текущего графика                  |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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::Конструктор параметрический.                   |
//| Строит кнопку в указанном окне указанного графика                |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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::Рисует внешний вид                             |
//+------------------------------------------------------------------+
void CButtonArrowDown::Draw(const bool chart_redraw)
  {
//--- Заливаем кнопку цветом фона, рисуем рамку и обновляем канвас фона
   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);
//--- Выводим текст кнопки
   CLabel::Draw(false);
//--- Очищаем область рисунка
   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);
//--- Задаём цвет стрелки для обычного и заблокированного состояний кнопки и рисуем стрелку вниз
   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(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Класс элемента управления "Кнопка со стрелкой влево"

//+------------------------------------------------------------------+
//| Класс кнопки со стрелкой влево                                   |
//+------------------------------------------------------------------+
class CButtonArrowLeft : public CButton
  {
public:
//--- Рисует внешний вид
   virtual void      Draw(const bool chart_redraw);

//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   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); }
   
//--- Конструкторы/деструктор
                     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::Конструктор по умолчанию.                      |
//| Строит кнопку в главном окне текущего графика                    |
//| в координатах 0,0 с размерами по умолчанию                       |
//+------------------------------------------------------------------+
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::Конструктор параметрический.                   |
//| Строит кнопку в главном окне текущего графика                    |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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::Конструктор параметрический.                   |
//| Строит кнопку в указанном окне текущего графика                  |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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::Конструктор параметрический.                   |
//| Строит кнопку в указанном окне указанного графика                |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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::Рисует внешний вид                             |
//+------------------------------------------------------------------+
void CButtonArrowLeft::Draw(const bool chart_redraw)
  {
//--- Заливаем кнопку цветом фона, рисуем рамку и обновляем канвас фона
   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);
//--- Выводим текст кнопки
   CLabel::Draw(false);
//--- Очищаем область рисунка
   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);
//--- Задаём цвет стрелки для обычного и заблокированного состояний кнопки и рисуем стрелку влево
   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(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Класс элемента управления "Кнопка со стрелкой вправо"

//+------------------------------------------------------------------+
//| Класс кнопки со стрелкой вправо                                  |
//+------------------------------------------------------------------+
class CButtonArrowRight : public CButton
  {
public:
//--- Рисует внешний вид
   virtual void      Draw(const bool chart_redraw);

//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   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); }
   
//--- Конструкторы/деструктор
                     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::Конструктор по умолчанию.                     |
//| Строит кнопку в главном окне текущего графика                    |
//| в координатах 0,0 с размерами по умолчанию                       |
//+------------------------------------------------------------------+
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::Конструктор параметрический.                  |
//| Строит кнопку в главном окне текущего графика                    |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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::Конструктор параметрический.                  |
//| Строит кнопку в указанном окне текущего графика                  |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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::Конструктор параметрический.                  |
//| Строит кнопку в указанном окне указанного графика                |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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::Рисует внешний вид                            |
//+------------------------------------------------------------------+
void CButtonArrowRight::Draw(const bool chart_redraw)
  {
//--- Заливаем кнопку цветом фона, рисуем рамку и обновляем канвас фона
   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);
//--- Выводим текст кнопки
   CLabel::Draw(false);
//--- Очищаем область рисунка
   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);
//--- Задаём цвет стрелки для обычного и заблокированного состояний кнопки и рисуем стрелку вправо
   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(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Класс элемента управления "Чекбокс"

Класс элемента управления "чекбокс" похож на классы кнопок со стрелками, то здесь фон будет полностью прозрачным. Т.е. рисоваться будет только текст и значок чекбокса. Чекбокс имеет два состояния — отмечен и не отмечен, значит, унаследован будет от класса двухпозиционной кнопки:

//+------------------------------------------------------------------+
//| Класс элемента управления Checkbox                               |
//+------------------------------------------------------------------+
class CCheckBox : public CButtonTriggered
  {
public:
//--- Рисует внешний вид
   virtual void      Draw(const bool chart_redraw);

//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   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);       }
  
//--- Инициализация цветов объекта по умолчанию
   virtual void      InitColors(void);
   
//--- Конструкторы/деструктор
                     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) {}
  };

Все классы элементов управления имеют по четыре конструктора:

//+------------------------------------------------------------------+
//| CCheckBox::Конструктор по умолчанию.                             |
//| Строит кнопку в главном окне текущего графика                    |
//| в координатах 0,0 с размерами по умолчанию                       |
//+------------------------------------------------------------------+
CCheckBox::CCheckBox(void) : CButtonTriggered("CheckBox",::ChartID(),0,"CheckBox",0,0,DEF_BUTTON_W,DEF_BUTTON_H)
  {
//--- Устанавливаем цвета по умолчанию, прозрачность для фона и переднего плана,
//--- и координаты и границы области рисунка значка кнопки
   this.InitColors();
   this.SetAlphaBG(0);
   this.SetAlphaFG(255);
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CCheckBox::Конструктор параметрический.                          |
//| Строит кнопку в главном окне текущего графика                    |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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)
  {
//--- Устанавливаем цвета по умолчанию, прозрачность для фона и переднего плана,
//--- и координаты и границы области рисунка значка кнопки
   this.InitColors();
   this.SetAlphaBG(0);
   this.SetAlphaFG(255);
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CCheckBox::Конструктор параметрический.                          |
//| Строит кнопку в указанном окне текущего графика                  |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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)
  {
//--- Устанавливаем цвета по умолчанию, прозрачность для фона и переднего плана,
//--- и координаты и границы области рисунка значка кнопки
   this.InitColors();
   this.SetAlphaBG(0);
   this.SetAlphaFG(255);
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }
//+------------------------------------------------------------------+
//| CCheckBox::Конструктор параметрический.                          |
//| Строит кнопку в указанном окне указанного графика                |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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)
  {
//--- Устанавливаем цвета по умолчанию, прозрачность для фона и переднего плана,
//--- и координаты и границы области рисунка значка кнопки
   this.InitColors();
   this.SetAlphaBG(0);
   this.SetAlphaFG(255);
   this.SetImageBound(1,1,this.Height()-2,this.Height()-2);
  }

Здесь инициализируются цвета объекта по умолчанию, устанавливается полностью прозрачный фон и непрозрачный передний план, а затем задаются размеры и координаты области изображения.

Метод сравнения возвращает результат вызова метода сравнения родительского класса:

//+------------------------------------------------------------------+
//| CCheckBox::Сравнение двух объектов                               |
//+------------------------------------------------------------------+
int CCheckBox::Compare(const CObject *node,const int mode=0) const
  {
   return CButtonTriggered::Compare(node,mode);
  }

Метод инициализации цветов объекта по умолчанию:

//+------------------------------------------------------------------+
//| CCheckBox::Инициализация цветов объекта по умолчанию             |
//+------------------------------------------------------------------+
void CCheckBox::InitColors(void)
  {
//--- Инициализируем цвета заднего плана для обычного и активированного состояний и делаем его текущим цветом фона
   this.InitBackColors(clrNULL);
   this.InitBackColorsAct(clrNULL);
   this.BackColorToDefault();
   
//--- Инициализируем цвета переднего плана для обычного и активированного состояний и делаем его текущим цветом текста
   this.InitForeColors(clrBlack);
   this.InitForeColorsAct(clrBlack);
   this.InitForeColorFocused(clrNavy);
   this.InitForeColorActFocused(clrNavy);
   this.ForeColorToDefault();
   
//--- Инициализируем цвета рамки для обычного и активированного состояний и делаем его текущим цветом рамки
   this.InitBorderColors(clrNULL);
   this.InitBorderColorsAct(clrNULL);
   this.BorderColorToDefault();

//--- Инициализируем цвет рамки и цвет переднего плана для заблокированного элемента
   this.InitBorderColorBlocked(clrNULL);
   this.InitForeColorBlocked(clrSilver);
  }

Метод рисования внешнего вида чекбокса:

//+------------------------------------------------------------------+
//| CCheckBox::Рисует внешний вид                                    |
//+------------------------------------------------------------------+
void CCheckBox::Draw(const bool chart_redraw)
  {
//--- Заливаем кнопку цветом фона, рисуем рамку и обновляем канвас фона
   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);
//--- Выводим текст кнопки
   CLabel::Draw(false);
   
//--- Очищаем область рисунка
   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);
//--- Рисуем отмеченный значок для активного состояния кнопки,
   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);
//--- и неотмеченный - для неактивного
   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(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

В зависимости от состояния элемента рисуется либо квадрат, отмеченный галочкой, либо просто пустой квадрат.

Теперь на основе данного объекта создадим класс элемента управления "радиокнопка".

Класс элемента управления "Радиокнопка"

Так как радиокнопка всегда работает в группе — может быть выключена только при включении другой кнопки группы, то здесь нам тоже нужно переопределить обработчик щелчка по объекту родительского класса.

//+------------------------------------------------------------------+
//| Класс элоемента управления Radio Button                          |
//+------------------------------------------------------------------+
class CRadioButton : public CCheckBox
  {
public:
//--- Рисует внешний вид
   virtual void      Draw(const bool chart_redraw);

//--- Обработчик событий нажатий кнопок мышки (Press)
   virtual void      OnPressEvent(const int id, const long lparam, const double dparam, const string sparam);

//--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта
   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);    }
  
//--- Конструкторы/деструктор
                     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) {}
  };

Конструкторы:

//+------------------------------------------------------------------+
//| CRadioButton::Конструктор по умолчанию.                          |
//| Строит кнопку в главном окне текущего графика                    |
//| в координатах 0,0 с размерами по умолчанию                       |
//+------------------------------------------------------------------+
CRadioButton::CRadioButton(void) : CCheckBox("RadioButton",::ChartID(),0,"",0,0,DEF_BUTTON_H,DEF_BUTTON_H)
  {
  }
//+------------------------------------------------------------------+
//| CRadioButton::Конструктор параметрический.                       |
//| Строит кнопку в главном окне текущего графика                    |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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::Конструктор параметрический.                       |
//| Строит кнопку в указанном окне текущего графика                  |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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::Конструктор параметрический.                       |
//| Строит кнопку в указанном окне указанного графика                |
//| с указанными текстом, координами и размерами                     |
//+------------------------------------------------------------------+
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)
  {
  }

Никаких дополнительных действий после вызова конструктора родительского класса здесь делать не нужно. Поэтому конструкторы имеют пустое тело.

Метод сравнения возвращает результат вызова метода сравнения родительского класса:

//+------------------------------------------------------------------+
//| CRadioButton::Сравнение двух объектов                            |
//+------------------------------------------------------------------+
int CRadioButton::Compare(const CObject *node,const int mode=0) const
  {
   return CCheckBox::Compare(node,mode);
  }

Метод рисования внешнего вида кнопки:

//+------------------------------------------------------------------+
//| CRadioButton::Рисует внешний вид                                 |
//+------------------------------------------------------------------+
void CRadioButton::Draw(const bool chart_redraw)
  {
//--- Заливаем кнопку цветом фона, рисуем рамку и обновляем канвас фона
   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);
//--- Выводим текст кнопки
   CLabel::Draw(false);
   
//--- Очищаем область рисунка
   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);
//--- Рисуем отмеченный значок для активного состояния кнопки,
   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);
//--- и неотмеченный - для неактивного
   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(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Этот метод идентичен методу родительского класса, но здесь рисуются значки радиокнопки — выбранный и не выбранный.

Обработчик событий нажатий кнопок мышки:

//+------------------------------------------------------------------+
//| CRadioButton::Обработчик событий нажатий кнопок мышки (Press)    |
//+------------------------------------------------------------------+
void CRadioButton::OnPressEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Если кнопка уже отмечена - уходим
   if(this.m_state)
      return;
//--- Устанавливаем состояние кнопки, обратное уже установленному
   ENUM_ELEMENT_STATE state=(this.State()==ELEMENT_STATE_DEF ? ELEMENT_STATE_ACT : ELEMENT_STATE_DEF);
   this.SetState(state);
   
//--- Вызываем обработчик родительского объекта с указанием идентификатора в lparam и состояния в dparam
   CCanvasBase::OnPressEvent(id,this.m_id,this.m_state,sparam);
  }

Здесь, если кнопка уже имеет включенное состояние, то никаких действий выполнять не нужно — уходим из обработчика. Если кнопка выключена — инвертируем её состояние и вызываем обработчик родительского класса (базового объекта всех элементов управления CCanvasBase).

На сегодня это все элементы управления, которые минимально необходимо было сделать для реализации сложных элементов управления.
Протестируем что получилось.


Тестируем результат

В папке \MQL5\Indicators\Tables\ создадим новый индикатор с именем iTestLabel.mq5.

Установим количество расчётных буферов и графических серий индикатора в ноль — никаких графиков рисовать не нужно. Подключим созданную библиотеку графических элементов. Индикатор будет в отдельном своём окне рисовать графические элементы, которые при создании будем сохранять в список, файл класса которого подключен к файлу индикатора:

//+------------------------------------------------------------------+
//|                                                   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 <Arrays\ArrayObj.mqh>
#include "Controls\Controls.mqh"
  
CArrayObj         list;             // Список для хранения тестируемых объектов
CCanvasBase      *base =NULL;       // Указатель на базовый графический элемент
CLabel           *label1=NULL;      // Указатель на графический элемент Label
CLabel           *label2=NULL;      // Указатель на графический элемент Label
CLabel           *label3=NULL;      // Указатель на графический элемент Label
CButton          *button1=NULL;     // Указатель на графический элемент Button
CButtonTriggered *button_t1=NULL;   // Указатель на графический элемент ButtonTriggered
CButtonTriggered *button_t2=NULL;   // Указатель на графический элемент ButtonTriggered
CButtonArrowUp   *button_up=NULL;   // Указатель на графический элемент CButtonArrowUp
CButtonArrowDown *button_dn=NULL;   // Указатель на графический элемент CButtonArrowDown
CButtonArrowLeft *button_lt=NULL;   // Указатель на графический элемент CButtonArrowLeft
CButtonArrowRight*button_rt=NULL;   // Указатель на графический элемент CButtonArrowRight
CCheckBox        *checkbox_lt=NULL; // Указатель на графический элемент CCheckBox
CCheckBox        *checkbox_rt=NULL; // Указатель на графический элемент CCheckBox
CRadioButton     *radio_bt_lt=NULL; // Указатель на графический элемент CRadioButton
CRadioButton     *radio_bt_rt=NULL; // Указатель на графический элемент CRadioButton

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

Здесь для упрощения сразу созданы указатели на создаваемые графические элементы, к которым после создания элемента и будем обращаться для работы с объектами.

Все объекты создадим в обработчике OnInit() индикатора. Сделаем так: создадим один базовый объект и раскрасим его так, чтобы напоминал некую панель.

Внутри этой "подложки" создадим все графические элементы и укажем для них этот базовый объект как контейнер.

Напишем в OnInit() такой код:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Ищем подокно графика
   int wnd=ChartWindowFind();
//--- Создаём базовый графический элемент
   list.Add(base=new CCanvasBase("Rectangle",0,wnd,100,40,260,160));
   base.SetAlphaBG(250);      // Прозрачность
   base.SetBorderWidth(6);    // Ширина рамки
   
//--- Инициализируем цвет фона, указываем цвет для заблокированного элемента
//--- и делаем текущим цветом фона элемента цвет фона, заданный по умолчанию 
   base.InitBackColors(clrWhiteSmoke);
   base.InitBackColorBlocked(clrLightGray);
   base.BackColorToDefault();
   
//--- Заливаем цветом фон и рисуем рамку с отступом в один пиксель от установленной ширины рамки
   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);
//--- Устанавливаем наименование и идентификатор элемента и выводим в журнал его описание
   base.SetName("Rectangle 1");
   base.SetID(1);
   base.Print();
   

//--- Внутри базового объекта создаём текстовую метку
//--- и указываем для метки в качестве контейнера базовый элемент
   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);
//--- Устанавливаем цвет при наведении курсора и щелчке по элементу как красный
//--- (это изменение стандартных параметров текстовой метки после её создания).
   label1.InitForeColorFocused(clrRed);   
   label1.InitForeColorPressed(clrRed);
//--- Устанавливаем идентификатор элемента, рисуем элемент
//--- и выводим в журнал его описание.
   label1.SetID(2);
   label1.Draw(false);
   label1.Print();
   
   
//--- Внутри базового объекта создаём простую кнопку
//--- и указываем для кнопки в качестве контейнера базовый элемент
   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);
//--- Задаём смещение текста кнопки по оси X
   button1.SetTextShiftH(2);
//--- Устанавливаем идентификатор элемента, рисуем элемент
//--- и выводим в журнал его описание.
   button1.SetID(3);
   button1.Draw(false);
   button1.Print();
   
   
//--- Внутри базового объекта создаём текстовую метку
//--- и указываем для метки в качестве контейнера базовый элемент
   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);
//--- Устанавливаем цвет при наведении курсора и щелчке по элементу как красный
//--- (это изменение стандартных параметров текстовой метки после её создания).
   label2.InitForeColorFocused(clrRed);
   label2.InitForeColorPressed(clrRed);
//--- Устанавливаем идентификатор элемента, рисуем элемент
//--- и выводим в журнал его описание.
   label2.SetID(4);
   label2.Draw(false);
   label2.Print();
   
   
//--- Внутри базового объекта создаём двухпозиционную кнопку
//--- и указываем для кнопки в качестве контейнера базовый элемент
   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);

//--- Задаём смещение текста кнопки по оси X
   button_t1.SetTextShiftH(2);
//--- Устанавливаем идентификатор и активированное состояние элемента,
//--- рисуем элемент и выводим в журнал его описание.
   button_t1.SetID(5);
   button_t1.SetState(true);
   button_t1.Draw(false);
   button_t1.Print();
   
   
//--- Внутри базового объекта создаём двухпозиционную кнопку
//--- и указываем для кнопки в качестве контейнера базовый элемент
   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);

//--- Задаём смещение текста кнопки по оси X
   button_t2.SetTextShiftH(2);
//--- Устанавливаем идентификатор элемента, рисуем элемент
//--- и выводим в журнал его описание.
   button_t2.SetID(6);
   button_t2.Draw(false);
   button_t2.Print();
   
   
//--- Внутри базового объекта создаём текстовую метку
//--- и указываем для метки в качестве контейнера базовый элемент
   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);
//--- Устанавливаем цвет при наведении курсора и щелчке по элементу как красный
//--- (это изменение стандартных параметров текстовой метки после её создания).
   label3.InitForeColorFocused(clrRed);
   label3.InitForeColorPressed(clrRed);
//--- Устанавливаем идентификатор элемента, рисуем элемент
//--- и выводим в журнал его описание.
   label3.SetID(7);
   label3.Draw(false);
   label3.Print();
   
   
//--- Внутри базового объекта создаём кнопку со стрелкой вверх
//--- и указываем для кнопки в качестве контейнера базовый элемент
   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);
//--- Задаём размеры и смещение изображения по оси X
   button_up.SetImageBound(1,1,w-4,h-3);
//--- Здесь можно настроить внешний вид кнопки, например, убрать рамку
   //button_up.InitBorderColors(button_up.BackColor(),button_up.BackColorFocused(),button_up.BackColorPressed(),button_up.BackColorBlocked());
   //button_up.ColorsToDefault();
//--- Устанавливаем идентификатор элемента, рисуем элемент
//--- и выводим в журнал его описание.
   button_up.SetID(8);
   button_up.Draw(false);
   button_up.Print();
   
   
//--- Внутри базового объекта создаём кнопку со стрелкой вниз
//--- и указываем для кнопки в качестве контейнера базовый элемент
   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);
//--- Задаём размеры и смещение изображения по оси X
   button_dn.SetImageBound(1,1,w-4,h-3);
//--- Устанавливаем идентификатор элемента, рисуем элемент
//--- и выводим в журнал его описание.
   button_dn.SetID(9);
   button_dn.Draw(false);
   button_dn.Print();
   
   
//--- Внутри базового объекта создаём кнопку со стрелкой влево
//--- и указываем для кнопки в качестве контейнера базовый элемент
   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);
//--- Задаём размеры и смещение изображения по оси X
   button_lt.SetImageBound(1,1,w-3,h-4);
//--- Устанавливаем идентификатор элемента, рисуем элемент
//--- и выводим в журнал его описание.
   button_lt.SetID(10);
   button_lt.Draw(false);
   button_lt.Print();
   
   
//--- Внутри базового объекта создаём кнопку со стрелкой вправо
//--- и указываем для кнопки в качестве контейнера базовый элемент
   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);
//--- Задаём размеры и смещение изображения по оси X
   button_rt.SetImageBound(1,1,w-3,h-4);
//--- Устанавливаем идентификатор элемента, рисуем элемент
//--- и выводим в журнал его описание.
   button_rt.SetID(11);
   button_rt.Draw(false);
   button_rt.Print();
   
   
//--- Внутри базового объекта создаём чекбокс с заголовком справа (левый чекбокс)
//--- и указываем для кнопки в качестве контейнера базовый элемент
   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);
//--- Задаём координаты и размеры области изображения
   checkbox_lt.SetImageBound(2,1,h-2,h-2);
//--- Задаём смещение текста кнопки по оси X
   checkbox_lt.SetTextShiftH(checkbox_lt.ImageRight()+2);
//--- Устанавливаем идентификатор элемента, рисуем элемент
//--- и выводим в журнал его описание.
   checkbox_lt.SetID(12);
   checkbox_lt.Draw(false);
   checkbox_lt.Print();
   
   
//--- Внутри базового объекта создаём чекбокс с заголовком слева (правый чекбокс)
//--- и указываем для кнопки в качестве контейнера базовый элемент
   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);
//--- Задаём координаты и размеры области изображения
   checkbox_rt.SetTextShiftH(2);
//--- Задаём смещение текста кнопки по оси X
   checkbox_rt.SetImageBound(checkbox_rt.Width()-h+2,1,h-2,h-2);
//--- Устанавливаем идентификатор и активированное состояние элемента,
//--- рисуем элемент и выводим в журнал его описание.
   checkbox_rt.SetID(13);
   checkbox_rt.SetState(true);
   checkbox_rt.Draw(false);
   checkbox_rt.Print();
   
   
//--- Внутри базового объекта создаём радиокнопку с заголовком справа (левый RadioButton)
//--- и указываем для кнопки в качестве контейнера базовый элемент
   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);
//--- Задаём координаты и размеры области изображения
   radio_bt_lt.SetImageBound(2,1,h-2,h-2);
//--- Задаём смещение текста кнопки по оси X
   radio_bt_lt.SetTextShiftH(radio_bt_lt.ImageRight()+2);
//--- Устанавливаем идентификатор и активированное состояние элемента,
//--- рисуем элемент и выводим в журнал его описание.
   radio_bt_lt.SetID(14);
   radio_bt_lt.SetState(true);
   radio_bt_lt.Draw(false);
   radio_bt_lt.Print();
   
   
//--- Внутри базового объекта создаём радиокнопку с заголовком слева (правый RadioButton)
//--- и указываем для кнопки в качестве контейнера базовый элемент
   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);
//--- Задаём смещение текста кнопки по оси X
   radio_bt_rt.SetTextShiftH(2);
//--- Задаём координаты и размеры области изображения
   radio_bt_rt.SetImageBound(radio_bt_rt.Width()-h+2,1,h-2,h-2);
//--- Устанавливаем идентификатор элемента, рисуем элемент
//--- и выводим в журнал его описание.
   radio_bt_rt.SetID(15);
   radio_bt_rt.Draw(true);
   radio_bt_rt.Print();

//--- Успешная инициализация
   return(INIT_SUCCEEDED);
  }

Изучите внимательно все комментарии к коду — здесь все шаги создания объектов описаны достаточно подробно.

В обработчике OnDeinit() индикатора уничтожаем все объекты в списке:

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

Обработчик OnCalculate() пустой — мы ничего не рассчитываем и не выводим на график:

//+------------------------------------------------------------------+
//| 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 next call
   return(rates_total);
  }

Чтобы оживить созданные графические элементы, необходимо в обработчике OnChartEvent()пройтись по списку созданных объектов и вызвать одноимённый обработчик для каждого элемента. Так как пока радиокнопки никак не связаны в группы (это будет в последующих статьях), эмулируем переключение радиокнопок, как это должно быть в группе элементов:

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Вызываем обработчик событий каждого из созданных объектов
   for(int i=0;i<list.Total();i++)
     {
      CCanvasBase *obj=list.At(i);
      if(obj!=NULL)
         obj.OnChartEvent(id,lparam,dparam,sparam);
     }
     
//--- Эмулируем работу радиокнопок в группе ---
//--- Если получено пользовательское событие
   if(id>=CHARTEVENT_CUSTOM)
     {
      //--- Если нажата левая радиокнопка
      if(sparam==radio_bt_lt.NameBG())
        {
         //--- Если состояние кнопки изменено (было не выбрано)
         if(radio_bt_lt.State())
           {
            //--- делаем правую радиокнопку невыбранной и перерисовываем её
            radio_bt_rt.SetState(false);
            radio_bt_rt.Draw(true);
           }
        }
      //--- Если нажата правая радиокнопка
      if(sparam==radio_bt_rt.NameBG())
        {
         //--- Если состояние кнопки изменено (было не выбрано)
         if(radio_bt_rt.State())
           {
            //--- делаем левую радиокнопку невыбранной и перерисовываем её
            radio_bt_lt.SetState(false);
            radio_bt_lt.Draw(true);
           }
        }
     }
  }

Давайте скомпилируем индикатор и запустим его на графике:

Все элементы управления реагируют на взаимодействие с мышкой, радиокнопки переключаются так, как будто они сгруппированы. Текстовые метки были сделаны меняющими цвет при наведении курсора для наглядного представления того, что настроить можно элементы управления по своему усмотрению. В обычном состоянии тексты меток статичны.

Но здесь есть одно упущение — при наведении курсора мышки на элемент управления, появляется ненужный тултип с именем индикатора. Чтобы избавиться от этого поведения, необходимо каждому графическому объекту в его свойстве OBJPROP_TOOLTIP вписать значение "\n". Исправим.

В классе CCanvasBase, в методе Create впишем две строки с установкой тултипов для графических объектов фона и переднего плана:

//+------------------------------------------------------------------+
//| CCanvasBase::Создаёт графические объекты фона и переднего плана  |
//+------------------------------------------------------------------+
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)
  {
//--- Получаем скорректированный идентификатор графика
   long id=this.CorrectChartID(chart_id);
//--- Корректируем переданное имя для объекта
   string nm=object_name;
   ::StringReplace(nm," ","_");
//--- Создаём имя графического объекта для фона и создаём канвас
   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;
     }
//--- Создаём имя графического объекта для переднего плана и создаём канвас
   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;
     }
//--- При успешном создании в свойство графического объекта OBJPROP_TEXT вписываем наименование программы
   ::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");

//--- Устанавливаем размеры прямоугольной области и возвращаем true
   this.m_bound.SetXY(x,y);
   this.m_bound.Resize(w,h);
   return true;
  }

Перекомпилируем индикатор и проверим:

Теперь всё правильно.


Заключение

Сегодня мы сделали ещё один шаг к созданию элемента управления Table Control. Все сложные элементы управления будут собираться из таких вот простых, но весьма функциональных объектов.

Сегодня мы добавили ко всем объектам компонент Controller, позвояющий интерактивно взаимодействовать пользователю с элементами управления и самим элементам — друг с другом.

В следующей статье подготовим элементы панель и контейнер, которые являются основными компонентами для размещения в них других элементов. При этом контейнер предоставляет возможность прокрутки дочерних элементов внутри себя.

Программы, используемые в статье:

#
 Имя Тип
Описание
 1  Base.mqh  Библиотека классов  Классы для создания базового объекта элементов управления
 2  Controls.mqh  Библиотека классов  Классы элементов управления
 3  iTestLabel.mq5  Тестовый индикатор  Индикатор для тестирования работы с классами элементов управления
 4  MQL5.zip  Архив  Архив файлов, представленных выше, для распаковки в каталог MQL5 клиентского терминала

Классы в составе библиотеки Base.mqh:

#
Наименование
 Описание
 1  CBaseObj  Базовый класс для всех графических объектов
 2  CColor  Класс для управления цветом
 3  CColorElement  Класс для управления цветами различных состояний графического элемента
 4  CBound  Класс для управления прямоугольной областью
 5  CCanvasBase  Базовый класс для работы с графическими элементами на холсте

Классы в составе библиотеки Controls.mqh:

#
Наименование
 Описание
 1  CImagePainter  Класс для рисования изображений в области, определённой координатами и размерами
 2  CLabel  Класс элемента управления "текстовая метка"
 3  CButton  Класс элемента управления "простая кнопка"
 4  CButtonTriggered  Класс элемента управления "двухпозиционная кнопка"
 5  CButtonArrowUp  Класс элемента управления "кнопка со стрелкой вверх"
 6
 CButtonArrowDown  Класс элемента управления "кнопка со стрелкой вниз"
 7  CButtonArrowLeft  Класс элемента управления "кнопка со стрелкой влево"
 8  CButtonArrowRight  Класс элемента управления "кнопка со стрелкой вправо"
 9  CCheckBox  Класс элемента управления "чекбокс"
 10  CRadioButton  Класс элемента управления "радиокнопка"

Все созданные файлы прилагаются к статье для самостоятельного изучения. Файл архива можно распаковать в папку терминала, и все файлы будут расположены в нужной папке: \MQL5\Indicators\Tables\.

Прикрепленные файлы |
Base.mqh (190.42 KB)
Controls.mqh (162.11 KB)
iTestLabel.mq5 (32.08 KB)
MQL5.zip (37.07 KB)
Переосмысливаем классические стратегии (Часть X): Может ли ИИ управлять MACD? Переосмысливаем классические стратегии (Часть X): Может ли ИИ управлять MACD?
Присоединяйтесь к нам, поскольку мы провели эмпирический анализ индикатора MACD, чтобы проверить, поможет ли применение искусственного интеллекта к стратегии, включая индикатор, повысить точность прогнозирования пары EURUSD. Мы одновременно оценивали, легче ли прогнозировать сам индикатор, чем цену, а также позволяет ли значение индикатора прогнозировать будущие уровни цен. Мы предоставим вам информацию, необходимую для принятия решения о том, стоит ли вам инвестировать свое время в интеграцию MACD в ваши торговые стратегии с использованием искусственного интеллекта.
Нейросети в трейдинге: Эффективное извлечение признаков для точной классификации (Построение объектов) Нейросети в трейдинге: Эффективное извлечение признаков для точной классификации (Построение объектов)
Mantis — универсальный инструмент для глубокого анализа временных рядов, гибко масштабируемый под любые финансовые сценарии. Узнайте, как сочетание патчинга, локальных свёрток и кросс-внимания позволяет получить высокоточную интерпретацию рыночных паттернов.
Нейросети в трейдинге: Эффективное извлечение признаков для точной классификации (Окончание) Нейросети в трейдинге: Эффективное извлечение признаков для точной классификации (Окончание)
Фреймворк Mantis превращает сложные временные ряды в информативные токены и служит надёжным фундаментом для интеллектуального торгового Агента, готового работать в реальном времени.
Индикатор прогнозирования ARIMA на MQL5 Индикатор прогнозирования ARIMA на MQL5
В данной статье мы создаем индикатор прогнозирования ARIMA на MQL5. Рассматривается, как модель ARIMA формирует прогнозы, её применимость к рынку Форекс и фондовому рынку в целом. Также объясняется, что такое авторегрессия AR, каким образом авторегрессионные модели используются для прогнозирования, и как работает механизм авторегрессии.