English 中文 Español Deutsch 日本語 Português
preview
DoEasy. Элементы управления (Часть 2): Продолжаем работу над классом CPanel

DoEasy. Элементы управления (Часть 2): Продолжаем работу над классом CPanel

MetaTrader 5Примеры | 15 апреля 2022, 16:12
1 470 0
Artyom Trishkin
Artyom Trishkin

Содержание


Концепция

С прошлой статьи мы начали обширную тему по созданию элементов управления в стиле Windows Forms. Но у нас остались ещё не исправленными некоторые ошибки и недоработки, которые "тянутся" с момента начала работы над графическими объектами, которые внимательный читатель наверняка мог уже заметить. Например, мы никогда не тестировали как ведут себя графические объекты при переключении таймфрейма графика. А они просто не выводятся на график, при этом в журнале появляются сообщения о том, что такие объекты уже созданы. Соответственно, их создание и прорисовка не выполняются. Так же, например, с мышкой у нас могут взаимодействовать только объекты-формы. Объект-графический элемент, являющийся родительским по отношению к форме, не имеет функционала для работы с мышкой, что в принципе правильно — это минимальный графический объект библиотеки, который может быть использован для каких-либо графических построений. Но если нужно интерактивное взаимодействие, то минимальным графическим объектом должен выступать объект-форма. Но его наследник — элемент управления "Панель", который мы начали разрабатывать в прошлой статье, он тоже не реагирует на мышку. Но должен. И это, скажем так, издержки последовательного добавления объектов и их функционала в библиотеку.

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

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


Доработка классов библиотеки

Для возможности быстро устанавливать различные стили отображения графических элементов мы создавали файл \MQL5\Include\DoEasy\GraphINI.mqh.
В нём находятся (и можно будет впоследствии добавлять собственные по примеру уже имеющихся) параметры разных цветовых схем, типы и стили отображения различных графических элементов.

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

Просто переместим параметры, находящиеся первыми в списке, в самый низ и подпишем их принадлежность:

//+------------------------------------------------------------------+
//| Список индексов параметров стилей форм                           |
//+------------------------------------------------------------------+
enum ENUM_FORM_STYLE_PARAMS
  {
   //--- CForm
   FORM_STYLE_FRAME_SHADOW_OPACITY,             // Непрозрачность тени
   FORM_STYLE_FRAME_SHADOW_BLUR,                // Размытие тени
   FORM_STYLE_DARKENING_COLOR_FOR_SHADOW,       // Затемнённость цвета тени формы
   FORM_STYLE_FRAME_SHADOW_X_SHIFT,             // Смещение тени по оси X
   FORM_STYLE_FRAME_SHADOW_Y_SHIFT,             // Смещение тени по оси Y
   //--- CPanel
   FORM_STYLE_FRAME_WIDTH_LEFT,                 // Ширина рамки панели слева
   FORM_STYLE_FRAME_WIDTH_RIGHT,                // Ширина рамки панели справа
   FORM_STYLE_FRAME_WIDTH_TOP,                  // Ширина рамки панели сверху
   FORM_STYLE_FRAME_WIDTH_BOTTOM,               // Ширина рамки панели снизу
  };
#define TOTAL_FORM_STYLE_PARAMS        (9)      // Количество параметров стиля формы
//+------------------------------------------------------------------+
//| Массив, содержащий параметры стилей форм                         |
//+------------------------------------------------------------------+
int array_form_style[TOTAL_FORM_STYLES][TOTAL_FORM_STYLE_PARAMS]=
  {
//--- Параметры стиля формы "Плоская форма"
   {
      //--- CForm
      80,                                       // Непрозрачность тени
      4,                                        // Размытие тени
      80,                                       // Затемнённость цвета тени формы
      2,                                        // Смещение тени по оси X
      2,                                        // Смещение тени по оси Y
      //--- CPanel
      3,                                        // Ширина рамки панели слева
      3,                                        // Ширина рамки панели справа
      3,                                        // Ширина рамки панели сверху
      3,                                        // Ширина рамки панели снизу
   },
//--- Параметры стиля формы "Рельефная форма"
   {
      //--- CForm
      80,                                       // Непрозрачность тени
      4,                                        // Размытие тени
      80,                                       // Затемнённость цвета тени формы
      2,                                        // Смещение тени по оси X
      2,                                        // Смещение тени по оси Y
      //--- CPanel
      3,                                        // Ширина рамки панели слева
      3,                                        // Ширина рамки панели справа
      3,                                        // Ширина рамки панели сверху
      3,                                        // Ширина рамки панели снизу
   },
  };
//+------------------------------------------------------------------+

Естественно, это совершенно не критичная доработка, но правильное структурирование параметров впоследствии облегчит их читаемость.

В файле \MQL5\Include\DoEasy\Data.mqh впишем индексы новых сообщений:

//--- CGraphElementsCollection
   MSG_GRAPH_ELM_COLLECTION_ERR_OBJ_ALREADY_EXISTS,   // Ошибка. Уже существует объект управления чартами с идентификатором чарта 
   MSG_GRAPH_ELM_COLLECTION_ERR_FAILED_CREATE_CTRL_OBJ,// Не удалось создать объект управления чартами с идентификатором чарта 
   MSG_GRAPH_ELM_COLLECTION_ERR_FAILED_GET_CTRL_OBJ,  // Не удалось получить объект управления чартами с идентификатором чарта 
   MSG_GRAPH_ELM_COLLECTION_ERR_GR_OBJ_ALREADY_EXISTS,// Такой графический объект уже существует: 
   MSG_GRAPH_ELM_COLLECTION_ERR_EMPTY_OBJECT,         // Ошибка! Пустой объект
   MSG_GRAPH_ELM_COLLECTION_ERR_FAILED_GET_ELEMENT,   // Не удалось получить графический элемент из списка

и тексты сообщений, соответствующие вновь добавленным индексам:

//--- CGraphElementsCollection
   {"Ошибка. Уже существует объект управления чартами с идентификатором чарта ","Error. A chart control object already exists with chart id "},
   {"Не удалось создать объект управления чартами с идентификатором чарта ","Failed to create chart control object with chart id "},
   {"Не удалось получить объект управления чартами с идентификатором чарта ","Failed to get chart control object with chart id "},
   {"Такой графический объект уже существует: ","Such a graphic object already exists: "},
   {"Ошибка! Пустой объект","Error! Empty object"},
   {"Не удалось получить графический элемент из списка","Failed to get graphic element from list"},


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

В файле \MQL5\Include\DoEasy\Defines.mqh создадим макроподстановку, указывающую ширину всех сторон рамки панели по умолчанию:

//--- Параметры канваса
#define PAUSE_FOR_CANV_UPDATE          (16)                       // Частота обновления канваса
#define CLR_CANV_NULL                  (0x00FFFFFF)               // Ноль для канваса с альфа-каналом
#define CLR_FORE_COLOR                 (C'0x2D,0x43,0x48')        // Цвет по умолчанию для текстов объектов на канвасе
#define DEF_FONT                       ("Calibri")                // Шрифт по умолчанию
#define DEF_FONT_SIZE                  (8)                        // Размер шрифта по умолчанию
#define OUTER_AREA_SIZE                (16)                       // Размер одной стороны внешней области вокруг рабочего пространства формы
#define DEF_FRAME_WIDTH_SIZE           (3)                        // Ширина рамки формы/панели/окна по умолчанию

и перечисления стилей начертания шрифта и типов его толщины в самом конце листинга файла:

//+------------------------------------------------------------------+
//| Список стилей начертания шрифта                                  |
//+------------------------------------------------------------------+
enum ENUM_FONT_STYLE
  {
   FONT_STYLE_NORMAL=0,                               // Обычный
   FONT_STYLE_ITALIC=FONT_ITALIC,                     // Курсив
   FONT_STYLE_UNDERLINE=FONT_UNDERLINE,               // Подчёркивание
   FONT_STYLE_STRIKEOUT=FONT_STRIKEOUT                // Перечёркивание
  };
//+------------------------------------------------------------------+
//| Список типов толщины шрифта                                      |
//+------------------------------------------------------------------+
enum ENUM_FW_TYPE
  {
   FW_TYPE_DONTCARE=FW_DONTCARE,
   FW_TYPE_THIN=FW_THIN,
   FW_TYPE_EXTRALIGHT=FW_EXTRALIGHT,
   FW_TYPE_ULTRALIGHT=FW_ULTRALIGHT,
   FW_TYPE_LIGHT=FW_LIGHT,
   FW_TYPE_NORMAL=FW_NORMAL,
   FW_TYPE_REGULAR=FW_REGULAR,
   FW_TYPE_MEDIUM=FW_MEDIUM,
   FW_TYPE_SEMIBOLD=FW_SEMIBOLD,
   FW_TYPE_DEMIBOLD=FW_DEMIBOLD,
   FW_TYPE_BOLD=FW_BOLD,
   FW_TYPE_EXTRABOLD=FW_EXTRABOLD,
   FW_TYPE_ULTRABOLD=FW_ULTRABOLD,
   FW_TYPE_HEAVY=FW_HEAVY,
   FW_TYPE_BLACK=FW_BLACK
  };
//+------------------------------------------------------------------+

Как видим, здесь мы просто каждой константе перечисления установили значение соответствующего по наименованию флага. Но для стиля начертания мы ввели новую константу для "нормального" шрифта — не курсивного, не подчёркнутого и не перечёркнутого. И это значение равно нулю, что будет сбрасывать для шрифта установленные ранее для него дополнительные флаги стиля его начертания.


В классе объекта-формы у нас есть класс анимации графических элементов на канвасе и объект тени формы. Оба эти объекта создаются посредством оператора new, а по завершении работы, удаляются в деструкторе класса. Таким образом эти объекты всегда отслеживаются и вовремя удаляются.
Но проблема появилась при наследовании от класса объекта-формы. Класс объекта-панели является наследником объекта-формы, и я столкнулся с тем, что вышеупомянутые объекты не удаляются, что вызывает утечку памяти. В этом можно убедиться, запустив советник из прошлой статьи. При удалении его с графика в журнале появятся сообщения о потере 4 объектов и утечки 512 байт памяти:

 4 undeleted objects left
 1 object of type CAnimations left
 3 objects of type CArrayObj left
 512 bytes of leaked memory

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

Для этого нам достаточно создать объект класса CArrayObj и в него складывать создаваемые в классе CForm объекты. При завершении работы терминал сам очистит память от всех объектов, находящихся в этом списке.

Откроем файл \MQL5\Include\DoEasy\Objects\Graph\Form.mqh и объявим такой список.
Также перенесём переменные для хранения ширины каждой стороны рамки формы в защищённую секцию класса:

//+------------------------------------------------------------------+
//| Класс объекта "форма"                                            |
//+------------------------------------------------------------------+
class CForm : public CGCnvElement
  {
private:
   CArrayObj         m_list_tmp;
   CArrayObj         m_list_elements;                          // Список присоединённых элементов
   CAnimations      *m_animations;                             // Указатель на объект анимаций
   CShadowObj       *m_shadow_obj;                             // Указатель на объект тени
   CMouseState       m_mouse;                                  // Объект класса "Состояния мышки"
   ENUM_MOUSE_FORM_STATE m_mouse_form_state;                   // Состояние мышки относительно формы
   ushort            m_mouse_state_flags;                      // Флаги состояния мышки
   color             m_color_frame;                            // Цвет рамки формы
   int               m_offset_x;                               // Смещение координаты X относительно курсора
   int               m_offset_y;                               // Смещение координаты Y относительно курсора
   
//--- Сбрасывает размер массива (1) текстовых, (2) прямоугольных, (3) геометрических кадров анимаций
   void              ResetArrayFrameT(void);
   void              ResetArrayFrameQ(void);
   void              ResetArrayFrameG(void);
   
//--- Возвращает имя зависимого объекта
   string            CreateNameDependentObject(const string base_name)  const
                       { return ::StringSubstr(this.NameObj(),::StringLen(::MQLInfoString(MQL_PROGRAM_NAME))+1)+"_"+base_name;   }
  
//--- Создаёт новый графический объект
   CGCnvElement     *CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type,
                                      const int element_num,
                                      const string name,
                                      const int x,
                                      const int y,
                                      const int w,
                                      const int h,
                                      const color colour,
                                      const uchar opacity,
                                      const bool movable,
                                      const bool activity);

//--- Создаёт объект для тени
   void              CreateShadowObj(const color colour,const uchar opacity);
   
protected:
   int               m_frame_width_left;                       // Ширина рамки формы слева
   int               m_frame_width_right;                      // Ширина рамки формы справа
   int               m_frame_width_top;                        // Ширина рамки формы сверху
   int               m_frame_width_bottom;                     // Ширина рамки формы снизу
//--- Инициализирует переменные
   void              Initialize(void);
   void              Deinitialize(void);
   
public:

Доступ к этим переменным нам нужен будет из классов-наследников, поэтому переменные должны быть в защищённой секции — видимы в этом классе и классах-наследниках.

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

//+------------------------------------------------------------------+
//| Инициализирует переменные                                        |
//+------------------------------------------------------------------+
void CForm::Initialize(void)
  {
   this.m_list_elements.Clear();
   this.m_list_elements.Sort();
   this.m_list_tmp.Clear();
   this.m_list_tmp.Sort();
   this.m_shadow_obj=NULL;
   this.m_shadow=false;
   this.m_frame_width_right=DEF_FRAME_WIDTH_SIZE;
   this.m_frame_width_left=DEF_FRAME_WIDTH_SIZE;
   this.m_frame_width_top=DEF_FRAME_WIDTH_SIZE;
   this.m_frame_width_bottom=DEF_FRAME_WIDTH_SIZE;
   this.m_mouse_state_flags=0;
   this.m_offset_x=0;
   this.m_offset_y=0;
   CGCnvElement::SetInteraction(false);
   this.m_animations=new CAnimations(CGCnvElement::GetObject());
   this.m_list_tmp.Add(m_animations);
  }
//+------------------------------------------------------------------+


В методе, создающем объект тени, после успешного создания объекта добавим его в новый список:

//+------------------------------------------------------------------+
//| Создаёт объект тени                                              |
//+------------------------------------------------------------------+
void CForm::CreateShadowObj(const color colour,const uchar opacity)
  {
//--- ...

//--- ...
   
//--- Создаём новый объект тени и записываем указатель на него в переменную
   this.m_shadow_obj=new CShadowObj(this.ChartID(),this.SubWindow(),this.CreateNameDependentObject("Shadow"),x,y,w,h);
   if(this.m_shadow_obj==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_FAILED_CREATE_SHADOW_OBJ));
      return;
     }
   this.m_list_tmp.Add(m_shadow_obj);
//--- ...

//--- Объект-форму перемещаем на передний план
   this.BringToTop();
  }
//+------------------------------------------------------------------+

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


Продолжим работу над классом объекта элемента управления WinForms CPanel.

Реализуем работу с параметрами шрифта панели и её рамкой.

В файле \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\Panel.mqh в приватной секции класса объявим переменную для хранения типа толщины шрифта и метод, возвращающий флаги, установленные для шрифта:

//+------------------------------------------------------------------+
//| Класс объекта Panel элементов управления WForms                  |
//+------------------------------------------------------------------+
class CPanel : public CForm
  {
private:
   color             m_fore_color;                                   // Цвет текста по умолчанию для всех объектов на панели
   ENUM_FW_TYPE      m_bold_type;                                    // Тип толщины шрифта
   ENUM_FRAME_STYLE  m_border_style;                                 // Стиль рамки панели
   bool              m_autoscroll;                                   // Флаг автоматического появления полосы прокрутки
   int               m_autoscroll_margin[2];                         // Массив полей вокруг элемента управления при автоматической прокрутке
   bool              m_autosize;                                     // Флаг автоматического изменения размера элемента под содержимое
   ENUM_CANV_ELEMENT_AUTO_SIZE_MODE m_autosize_mode;                 // Режим автоматического изменения размера элемента под содержимое
   ENUM_CANV_ELEMENT_DOCK_MODE m_dock_mode;                          // Режим привязки границ элемента к контейнеру
   int               m_margin[4];                                    // Массив промежутков всех сторон между полями данного и другого элемента управления
   int               m_padding[4];                                   // Массив промежутков всех сторон внутри элемента управления
//--- Возвращает флаги шрифта
   uint              GetFontFlags(void);
public:

При установке значения типа ширины шрифта в класс CCanvas, в переменную m_bold_type будем записывать это значение.
Метод, возвращающий флаги шрифта, возвращает все параметры, которые установлены для него: имя, размер, флаги и угол наклона. Так как нам нужно оперировать только с флагами, то, чтобы не объявлять в каждом методе локальные переменные, в которые будут записываться все эти значения, установленные для шрифта, мы будем вызывать этот метод, который вернёт только флаги, полученные из свойств шрифта класса CCanvas.

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

public:
//--- (1) Устанавливает, (2) возвращает цвет текста по умолчанию всех объектов на панели
   void              ForeColor(const color clr)                      { this.m_fore_color=clr;               }
   color             ForeColor(void)                           const { return this.m_fore_color;            }

//--- (1) Устанавливает, (2) возвращает флаг шрифта Bold
   void              Bold(const bool flag);
   bool              Bold(void);
//--- (1) Устанавливает, (2) возвращает флаг шрифта Italic
   void              Italic(const bool flag);
   bool              Italic(void);
//--- (1) Устанавливает, (2) возвращает флаг шрифта Strikeout
   void              Strikeout(const bool flag);
   bool              Strikeout(void);
//--- (1) Устанавливает, (2) возвращает флаг шрифта Underline
   void              Underline(const bool flag);
   bool              Underline(void);
//--- (1) Устанавливает, (2) возвращает стиль начертания шрифта
   void              FontDrawStyle(ENUM_FONT_STYLE style);
   ENUM_FONT_STYLE   FontDrawStyle(void);
//--- (1) Устанавливает, (2) возвращает тип толщины шрифта
   void              FontBoldType(ENUM_FW_TYPE type);
   ENUM_FW_TYPE      FontBoldType(void)                        const { return this.m_bold_type;             }

//--- (1) Устанавливает, (2) возвращает стиль рамки

...

и напишем методы для установки и возврата свойств рамки панели:

//--- Возвращает промежуток (1) слева, (2) сверху, (3) справа, (4) снизу между полями внутри элемента управления
   int               PaddingLeft(void)                         const { return this.m_padding[0];            }
   int               PaddingTop(void)                          const { return this.m_padding[1];            }
   int               PaddingRight(void)                        const { return this.m_padding[2];            }
   int               PaddingBottom(void)                       const { return this.m_padding[3];            }
   
//--- Устанавливает ширину рамки формы (1) слева, (2) сверху, (3) справа, (4) снизу, (5) всех сторон элемента управления
   void              FrameWidthLeft(const int value)                 { this.m_frame_width_left=value;       }
   void              FrameWidthTop(const int value)                  { this.m_frame_width_top=value;        }
   void              FrameWidthRight(const int value)                { this.m_frame_width_right=value;      }
   void              FrameWidthBottom(const int value)               { this.m_frame_width_bottom=value;     }
   void              FrameWidthAll(const int value)
                       {
                        this.FrameWidthLeft(value); this.FrameWidthTop(value); this.FrameWidthRight(value); this.FrameWidthBottom(value);
                       }
//--- Возвращает ширину рамки формы (1) слева, (2) сверху, (3) справа, (4) снизу
   int               FrameWidthLeft(void)                      const { return this.m_frame_width_left;      }
   int               FrameWidthTop(void)                       const { return this.m_frame_width_top;       }
   int               FrameWidthRight(void)                     const { return this.m_frame_width_right;     }
   int               FrameWidthBottom(void)                    const { return this.m_frame_width_bottom;    }
   
//--- Конструкторы


В каждом конструкторе пропишем установку типа толщины шрифта по умолчанию:

                     CPanel(const string name) : CForm(::ChartID(),0,name,0,0,0,0)
                       {
                        CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_PANEL);
                        this.m_type=OBJECT_DE_TYPE_GWF_PANEL; 
                        this.m_fore_color=CLR_FORE_COLOR;
                        this.m_bold_type=FW_TYPE_NORMAL;
                        this.MarginAll(3);
                        this.PaddingAll(0);
                        this.Initialize();
                       }
//--- Деструктор
                    ~CPanel();
  };
//+------------------------------------------------------------------+
//| Конструктор с указанием идентификатора чарта и подокна           |
//+------------------------------------------------------------------+
CPanel::CPanel(const long chart_id,
               const int subwindow,
               const string name,
               const int x,
               const int y,
               const int w,
               const int h) : CForm(chart_id,subwindow,name,x,y,w,h)
  {
   CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_PANEL);
   this.m_type=OBJECT_DE_TYPE_GWF_PANEL;
   this.m_fore_color=CLR_FORE_COLOR;
   this.m_bold_type=FW_TYPE_NORMAL;
   this.MarginAll(3);
   this.PaddingAll(0);
   this.Initialize();
  }
//+------------------------------------------------------------------+
//| Конструктор на текущем чарте с указанием подокна                 |
//+------------------------------------------------------------------+
CPanel::CPanel(const int subwindow,
               const string name,
               const int x,
               const int y,
               const int w,
               const int h) : CForm(::ChartID(),subwindow,name,x,y,w,h)
  {
   CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_PANEL);
   this.m_type=OBJECT_DE_TYPE_GWF_PANEL; 
   this.m_fore_color=CLR_FORE_COLOR;
   this.m_bold_type=FW_TYPE_NORMAL;
   this.MarginAll(3);
   this.PaddingAll(0);
   this.Initialize();
  }
//+------------------------------------------------------------------+
//| Конструктор на текущем чарте в главном окне графика              |
//+------------------------------------------------------------------+
CPanel::CPanel(const string name,
               const int x,
               const int y,
               const int w,
               const int h) : CForm(::ChartID(),0,name,x,y,w,h)
  {
   CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_PANEL);
   this.m_type=OBJECT_DE_TYPE_GWF_PANEL; 
   this.m_fore_color=CLR_FORE_COLOR;
   this.m_bold_type=FW_TYPE_NORMAL;
   this.MarginAll(3);
   this.PaddingAll(0);
   this.Initialize();
  }
//+------------------------------------------------------------------+

Такой тип будет выставлен по умолчанию для шрифтов панели.

Приватный метод, возвращающий флаги шрифта:

//+------------------------------------------------------------------+
//| Возвращает флаги шрифта                                          |
//+------------------------------------------------------------------+
uint CPanel::GetFontFlags(void)
  {
   string name;
   int size;
   uint flags;
   uint angle;
   CGCnvElement::GetFont(name,size,flags,angle);
   return flags;
  }
//+------------------------------------------------------------------+

Так как в метод GetFont() класса графического элемента необходимо передать переменные по ссылке, в которые будут записаны значения, полученные из параметров шрифта в классе CCanvas, то здесь мы объявляем все нужные переменные, получаем их значения вызовом метода GetFont() и возвращаем только полученные флаги.

Метод, устанавливающий флаг шрифта Bold:

//+------------------------------------------------------------------+
//| Устанавливает флаг шрифта Bold                                   |
//+------------------------------------------------------------------+
void CPanel::Bold(const bool flag)
  {
   uint flags=this.GetFontFlags();
   if(flag)
     {
      this.m_bold_type=FW_TYPE_BOLD;
      CGCnvElement::SetFontFlags(flags | FW_BOLD);
     }
   else
      this.m_bold_type=FW_TYPE_NORMAL;
  }
//+------------------------------------------------------------------+

Здесь: получаем флаги при помощи метода GetFontFlags(), рассмотренного выше, если флаг, переданный в аргументах метода, установлензаписываем значение Bold в переменную m_bold_type, хранящую тип толщины шрифта, и устанавливаем для флагов шрифта ещё один флаг — FW_BOLD.
Если же флаг, переданный в аргументах метода, не установлен, то записываем в переменную m_bold_type умолчательное значение.

Метод, возвращающий флаг шрифта Bold:

//+------------------------------------------------------------------+
//| Возвращает флаг шрифта Bold                                      |
//+------------------------------------------------------------------+
bool CPanel::Bold(void)
  {
   uint flags=this.GetFontFlags();
   return(flags &FW_BOLD)==FW_BOLD;
  }
//+------------------------------------------------------------------+

Здесь: получаем флаги методом GetFontFlags() и возвращаем результат проверки, что флаг FW_BOLD присутствует в переменной.


Методы установки и возврата остальных флагов начертания шрифта немного отличаются — в них не требуется установка в переменную значения флага.
В остальном они идентичны вышерассмотренным:

//+------------------------------------------------------------------+
//| Устанавливает флаг шрифта Italic                                 |
//+------------------------------------------------------------------+
void CPanel::Italic(const bool flag)
  {
   uint flags=this.GetFontFlags();
   if(flag)
      CGCnvElement::SetFontFlags(flags | FONT_ITALIC);
  }
//+------------------------------------------------------------------+
//| Возвращает флаг шрифта Italic                                    |
//+------------------------------------------------------------------+
bool CPanel::Italic(void)
  {
   uint flags=this.GetFontFlags();
   return(flags &FONT_ITALIC)==FONT_ITALIC;
  }
//+------------------------------------------------------------------+
//| Устанавливает флаг шрифта Strikeout                              |
//+------------------------------------------------------------------+
void CPanel::Strikeout(const bool flag)
  {
   uint flags=this.GetFontFlags();
   if(flag)
      CGCnvElement::SetFontFlags(flags | FONT_STRIKEOUT);
  }
//+------------------------------------------------------------------+
//| Возвращает флаг шрифта Strikeout                                 |
//+------------------------------------------------------------------+
bool CPanel::Strikeout(void)
  {
   uint flags=this.GetFontFlags();
   return(flags &FONT_STRIKEOUT)==FONT_STRIKEOUT;
  }
//+------------------------------------------------------------------+
//| Устанавливает флаг шрифта Underline                              |
//+------------------------------------------------------------------+
void CPanel::Underline(const bool flag)
  {
   uint flags=this.GetFontFlags();
   if(flag)
      CGCnvElement::SetFontFlags(flags | FONT_UNDERLINE);
  }
//+------------------------------------------------------------------+
//| Возвращает флаг шрифта Underline                                 |
//+------------------------------------------------------------------+
bool CPanel::Underline(void)
  {
   uint flags=this.GetFontFlags();
   return(flags &FONT_UNDERLINE)==FONT_UNDERLINE;
  }
//+------------------------------------------------------------------+

Думаю, эти методы понятны и в пояснениях не нуждаются.


Метод, устанавливающий стиль начертания шрифта:

//+------------------------------------------------------------------+
//| Устанавливает стиль начертания шрифта                            |
//+------------------------------------------------------------------+
void CPanel::FontDrawStyle(ENUM_FONT_STYLE style)
  {
   switch(style)
     {
      case FONT_STYLE_ITALIC     :  this.Italic(true);      break;
      case FONT_STYLE_UNDERLINE  :  this.Underline(true);   break;
      case FONT_STYLE_STRIKEOUT  :  this.Strikeout(true);   break;
      default: break;
     }
  }
//+------------------------------------------------------------------+

В зависимости от стиля начертания шрифта (курсив, с подчёркиванием, перечёркнутый), переданного в метод, вызываем соответствующий стилю метод его установки.

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

//+------------------------------------------------------------------+
//| Возвращает стиль начертания шрифта                               |
//+------------------------------------------------------------------+
ENUM_FONT_STYLE CPanel::FontDrawStyle(void)
  {
   return
     (
      this.Italic()     ?  FONT_STYLE_ITALIC    :
      this.Underline()  ?  FONT_STYLE_UNDERLINE :
      this.Strikeout()  ?  FONT_STYLE_UNDERLINE :
      FONT_STYLE_NORMAL
     );
  }
//+------------------------------------------------------------------+

В зависимости от стиля, установленного для шрифта, и возвращаемого соответствующими методами, возвращается тот же стиль начертания шрифта.
Если ни один из трёх стилей не установлен, возвращается Normal.


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

//+------------------------------------------------------------------+
//| Устанавливает тип толщины шрифта                                 |
//+------------------------------------------------------------------+
void CPanel::FontBoldType(ENUM_FW_TYPE type)
  {
   this.m_bold_type=type;
   uint flags=this.GetFontFlags();
   switch(type)
     {
      case FW_TYPE_DONTCARE   : CGCnvElement::SetFontFlags(flags | FW_DONTCARE);    break;
      case FW_TYPE_THIN       : CGCnvElement::SetFontFlags(flags | FW_THIN);        break;
      case FW_TYPE_EXTRALIGHT : CGCnvElement::SetFontFlags(flags | FW_EXTRALIGHT);  break;
      case FW_TYPE_ULTRALIGHT : CGCnvElement::SetFontFlags(flags | FW_ULTRALIGHT);  break;
      case FW_TYPE_LIGHT      : CGCnvElement::SetFontFlags(flags | FW_LIGHT);       break;
      case FW_TYPE_REGULAR    : CGCnvElement::SetFontFlags(flags | FW_REGULAR);     break;
      case FW_TYPE_MEDIUM     : CGCnvElement::SetFontFlags(flags | FW_MEDIUM);      break;
      case FW_TYPE_SEMIBOLD   : CGCnvElement::SetFontFlags(flags | FW_SEMIBOLD);    break;
      case FW_TYPE_DEMIBOLD   : CGCnvElement::SetFontFlags(flags | FW_DEMIBOLD);    break;
      case FW_TYPE_BOLD       : CGCnvElement::SetFontFlags(flags | FW_BOLD);        break;
      case FW_TYPE_EXTRABOLD  : CGCnvElement::SetFontFlags(flags | FW_EXTRABOLD);   break;
      case FW_TYPE_ULTRABOLD  : CGCnvElement::SetFontFlags(flags | FW_ULTRABOLD);   break;
      case FW_TYPE_HEAVY      : CGCnvElement::SetFontFlags(flags | FW_HEAVY);       break;
      case FW_TYPE_BLACK      : CGCnvElement::SetFontFlags(flags | FW_BLACK);       break;
      default                 : CGCnvElement::SetFontFlags(flags | FW_NORMAL);      break;
     }
  }
//+------------------------------------------------------------------+

Здесь:  В переменную m_bold_type записываем переданное в метод значение. Получаем флаги шрифта при помощи метода GetFontFlags().
В зависимости от типа ширины шрифта, переданного в метод, дописываем в полученную переменную с флагами шрифта ещё один флаг, соответствующий указанному типу.
В итоге в переменной m_bold_type будет записано переданное в метод значение перечисления, а во флаги шрифта будет записан флаг с типом ширины шрифта, соответствующий значению перечисления ENUM_FW_TYPE.


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

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

В файле класса-коллекции графических элементов \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh перед объявлением класса напишем перечисление:

//+------------------------------------------------------------------+
//| Коллекция графических объектов                                   |
//+------------------------------------------------------------------+
#resource "\\"+PATH_TO_EVENT_CTRL_IND;          // Индикатор контроля событий графических объектов, упакованный в ресурсы программы
enum ENUM_ADD_OBJ_RET_CODE                      // Перечисление кодов возврата метода добавления объекта в список
  {
   ADD_OBJ_RET_CODE_SUCCESS,                    // Успешно
   ADD_OBJ_RET_CODE_EXIST,                      // Объект существует в списке-коллекции
   ADD_OBJ_RET_CODE_ERROR,                      // Ошибка добавления в список-коллекцию
  };
class CGraphElementsCollection : public CBaseObj

Здесь три кода возврата:

  1. объект успешно добавлен в список,
  2. объект уже существует в списке-коллекции,
  3. произошла ошибка при добавлении объекта в список-коллекцию.

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

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

class CGraphElementsCollection : public CBaseObj
  {
private:
//--- ...

   bool              m_is_graph_obj_event;      // Флаг события в списке графических объектов
   int               m_total_objects;           // Количество графических объектов
   int               m_delta_graph_obj;         // Разница в количестве графических объектов по сравнению с прошлой проверкой
   string            m_name_prefix;             // Префикс имени объектов
   
//--- Возвращает флаг наличия объекта класса графического элемента в списке-коллекции графических элементов

Также добавим два приватных метода: метод, либо создающий новый графический элемент, либо возвращающий идентификатор существующего,
и метод, возвращающий индекс указанного графического элемента в списке-коллекции:

//--- Сбрасывает всем формам флаги взаимодействия кроме указанной
   void              ResetAllInteractionExeptOne(CGCnvElement *form);
//--- Добавляет элемент в список-коллекцию
   bool AddCanvElmToCollection(CGCnvElement *element);
//--- Добавляет элемент в список-коллекцию либо возвращает существующий
   ENUM_ADD_OBJ_RET_CODE AddOrGetCanvElmToCollection(CGCnvElement *element,int &id);
//--- Возвращает индекс графического элемента в списке-коллекции
   int               GetIndexGraphElement(const long chart_id,const string name);


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

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

//--- Возвращает список графических элементов по идентификатору графика и имени объекта
   CArrayObj        *GetListCanvElementByName(const long chart_id,const string name)
                       {
                        string nm=(::StringFind(name,this.m_name_prefix)<0 ? this.m_name_prefix : "")+name;
                        CArrayObj *list=CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),CANV_ELEMENT_PROP_CHART_ID,chart_id,EQUAL);
                        return CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL);
                       }

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

Сразу за этим методом напишем ещё один метод, возвращающий графический элемент по идентификатору графика и имени:

//--- Возвращает графический элемент по идентификатору графика и имени
   CGCnvElement     *GetCanvElement(const long chart_id,const string name)
                       {
                        CArrayObj *list=this.GetListCanvElementByName(chart_id,name);
                        return(list!=NULL ? list.At(0) : NULL);
                       }

//--- Конструктор

Здесь: получаем список графических элементов по указанным идентификатору графика и имени объекта. Если список получен — возвращаем указатель на единственный объект, лежащий в нём. Иначе — возвращаем NULL.

Чтобы при переключении таймфреймов мы могли пересоздать заново все элементы GUI, нам нужно очистить список-коллекцию графических элементов. Но так как деструктор класса, в котором уже прописана очистка всех списков, вызывается только при снятии программы с графика, то нам необходимо делать очистку в обработчике OnDeinit(). Объявим его:

//--- Обновляет список (1) всех графических объектов, (2) на указанном чарте, заполняет данные о количестве новых и устанавливает флаг события
   void              Refresh(void);
   void              Refresh(const long chart_id);
//--- Обработчики событий
   void              OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam);
   void              OnDeinit(void);
private:
//--- Перемещает все объекты на канвасе на передний план
   void              BringToTopAllCanvElm(void);


Теперь в каждом методе, создающем графический элемент, заменим этот блок кода:

                        if(!this.AddCanvElmToCollection(obj))
                          {
                           delete obj;
                           return WRONG_VALUE;
                          }

на такой:

//--- Создаёт объект-графический элемент на канвасе на указанном графике и подокне
   int               CreateElement(const long chart_id,
                                   const int subwindow,
                                   const string name,
                                   const int x,
                                   const int y,
                                   const int w,
                                   const int h,
                                   const color clr,
                                   const uchar opacity,
                                   const bool movable,
                                   const bool activity,
                                   const bool redraw=false)
                       {
                        int id=this.m_list_all_canv_elm_obj.Total();
                        CGCnvElement *obj=new CGCnvElement(GRAPH_ELEMENT_TYPE_ELEMENT,id,0,chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,activity,redraw);
                        ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id);
                        if(res==ADD_OBJ_RET_CODE_ERROR)
                           return WRONG_VALUE;
                        if(res==ADD_OBJ_RET_CODE_EXIST)
                           obj.SetID(id);
                        obj.Erase(clr,opacity,redraw);
                        return obj.ID();
                       }
//--- Создаёт объект-графический элемент на канвасе на указанном графике и подокне с заливкой вертикальным градиентом

Что мы здесь видим: сначала получаем код возврата от метода, создающего новый графический элемент, либо возвращающий идентификатор  существующего. Затем, если метод вернул ошибку — возвращаем -1, если код возврата говорит о том, что объект существует — устанавливаем в свойства вновь созданного объекта идентификатор, полученный из метода AddOrGetCanvElmToCollection(). Дело в том, что изначально мы для идентификатора нового объекта устанавливаем максимальное значение — количество объектов в списке, а для существующего уже объекта, идентификатор должен быть другим — именно уже существующего, то этот идентификатор записывается в переменную, передаваемому по ссылке в метод добавления объекта в список, либо возврата существующего объекта.
Таким образом мы либо добавляем новый объект в список, либо получаем указатель на существующий и записываем в него его же идентификатор.

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

//--- Создаёт объект-графический объект-форму на канвасе на указанном графике и подокне
   int               CreateForm(const long chart_id,
                                const int subwindow,
                                const string name,
                                const int x,
                                const int y,
                                const int w,
                                const int h,
                                const color clr,
                                const uchar opacity,
                                const bool movable,
                                const bool activity,
                                const bool shadow=false,
                                const bool redraw=false)
                       {
                        int id=this.m_list_all_canv_elm_obj.Total();
                        CForm *obj=new CForm(chart_id,subwindow,name,x,y,w,h);
                        ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id);
                        if(res==ADD_OBJ_RET_CODE_ERROR)
                           return WRONG_VALUE;
                        obj.SetID(id);
                        obj.SetActive(activity);
                        obj.SetMovable(movable);
                        obj.SetColorBackground(clr);
                        obj.SetColorFrame(clr);
                        obj.SetOpacity(opacity,false);
                        obj.SetShadow(shadow);
                        obj.DrawRectangle(0,0,obj.Width()-1,obj.Height()-1,obj.ColorFrame(),obj.Opacity());
                        obj.Done();
                        obj.Erase(clr,opacity,redraw);
                        return obj.ID();
                       }
//--- Создаёт объект-графический объект-форму на канвасе на указанном графике и подокне с заливкой вертикальным градиентом

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

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

//--- Создаёт объект-графический объект WinForms Panel на канвасе на указанном графике и подокне
   int               CreatePanel(const long chart_id,
                                 const int subwindow,
                                 const string name,
                                 const int x,
                                 const int y,
                                 const int w,
                                 const int h,
                                 const color clr,
                                 const uchar opacity,
                                 const bool movable,
                                 const bool activity,
                                 const int  frame_width=-1,
                                 ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL,
                                 const bool shadow=false,
                                 const bool redraw=false)
                       {
                        int id=this.m_list_all_canv_elm_obj.Total();
                        CPanel *obj=new CPanel(chart_id,subwindow,name,x,y,w,h);
                        ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id);
                        if(res==ADD_OBJ_RET_CODE_ERROR)

                           return WRONG_VALUE;

                        obj.SetID(id);
                        obj.SetActive(activity);
                        obj.SetMovable(movable);
                        obj.SetColorBackground(clr);
                        obj.SetColorFrame(clr);
                        obj.SetOpacity(opacity,false);
                        obj.SetShadow(shadow);
                        obj.DrawRectangle(0,0,obj.Width()-1,obj.Height()-1,obj.ColorFrame(),obj.Opacity());
                        obj.Erase(clr,opacity,redraw);
                        if(frame_width>0)
                           obj.FrameWidthAll(frame_width);
                        obj.SetActiveAreaShift(obj.FrameWidthLeft(),obj.FrameWidthBottom(),obj.FrameWidthRight(),obj.FrameWidthTop());
                        obj.DrawFormFrame(obj.FrameWidthTop(),obj.FrameWidthBottom(),obj.FrameWidthLeft(),obj.FrameWidthRight(),obj.ColorFrame(),obj.Opacity(),frame_style);
                        obj.Done();
                        return obj.ID();
                       }

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


В конструкторе класса устанавливаем значение для префикса имён графических объектов, очищаем список всех графических элементов и устанавливаем ему флаг сортированного списка:

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CGraphElementsCollection::CGraphElementsCollection()
  {
   this.m_type=COLLECTION_GRAPH_OBJ_ID;
   this.m_name_prefix=this.m_name_program+"_";
   ::ChartSetInteger(::ChartID(),CHART_EVENT_MOUSE_MOVE,true);
   ::ChartSetInteger(::ChartID(),CHART_EVENT_MOUSE_WHEEL,true);
   this.m_list_all_graph_obj.Type(COLLECTION_GRAPH_OBJ_ID);
   this.m_list_all_graph_obj.Sort(SORT_BY_CANV_ELEMENT_ID);
   this.m_list_all_graph_obj.Clear();
   this.m_list_charts_control.Sort();
   this.m_list_charts_control.Clear();
   this.m_total_objects=0;
   this.m_is_graph_obj_event=false;
   this.m_list_deleted_obj.Clear();
   this.m_list_deleted_obj.Sort();
   this.m_list_all_canv_elm_obj.Clear();
   this.m_list_all_canv_elm_obj.Sort();
  }
//+------------------------------------------------------------------+


Метод, добавляющий графический элемент на канвасе в коллекцию:

//+------------------------------------------------------------------+
//| Добавляет графический элемент на канвасе в коллекцию             |
//+------------------------------------------------------------------+
bool CGraphElementsCollection::AddCanvElmToCollection(CGCnvElement *element)
  {
   if(!this.m_list_all_canv_elm_obj.Add(element))
     {
      CMessage::ToLog(DFUN+element.Name()+": ",MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

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


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

//+------------------------------------------------------------------+
//| Добавляет элемент в список-коллекцию либо возвращает существующий|
//+------------------------------------------------------------------+
ENUM_ADD_OBJ_RET_CODE CGraphElementsCollection::AddOrGetCanvElmToCollection(CGCnvElement *element,int &id)
  {
//--- Если передан невалидный указатель - сообщаем об этом и возвращаем код ошибки
   if(element==NULL)
     {
      CMessage::ToLog(DFUN,MSG_GRAPH_ELM_COLLECTION_ERR_EMPTY_OBJECT);
      return ADD_OBJ_RET_CODE_ERROR;
     }
//--- Если графический элемент с указанным идентификатором графика и именем уже есть - 
   if(this.IsPresentCanvElmInList(element.ChartID(),element.Name()))
     {
      //--- сообщаем о существовании объекта,
      CMessage::ToLog(DFUN+element.Name()+": ",MSG_LIB_SYS_OBJ_ALREADY_IN_LIST);
      //--- получаем элемент из списка-коллекции.
      element=this.GetCanvElement(element.ChartID(),element.Name());
      //--- Если объект получить не удалось - сообщаем об этом и возвращаем код ошибки
      if(element==NULL)
        {
         CMessage::ToLog(DFUN,MSG_GRAPH_ELM_COLLECTION_ERR_FAILED_GET_ELEMENT);
         return ADD_OBJ_RET_CODE_ERROR;
        }
      //--- записываем в возвращаемое по ссылке значение идентификатор полученного из списка объекта
      //--- и возвращаем код существования объекта в списке
      id=element.ID();
      return ADD_OBJ_RET_CODE_EXIST;
     }
//--- Если добавить объект в список ек удалось - удаляем его и возвращаем код ошибки
   if(!this.AddCanvElmToCollection(element))
     {
      delete element;
      return ADD_OBJ_RET_CODE_ERROR;
     }
//--- Всё успешно
   return ADD_OBJ_RET_CODE_SUCCESS;
  }
//+------------------------------------------------------------------+

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


Метод, возвращающий индекс графического элемента в списке-коллекции:

//+------------------------------------------------------------------+
//| Возвращает индекс графического элемента в списке-коллекции       |
//+------------------------------------------------------------------+
int CGraphElementsCollection::GetIndexGraphElement(const long chart_id,const string name)
  {
   for(int i=0;i<this.m_list_all_canv_elm_obj.Total();i++)
     {
      CGCnvElement *obj=this.m_list_all_canv_elm_obj.At(i);
      if(obj==NULL)
         continue;
      if(obj.ChartID()==chart_id && obj.Name()==name)
         return i;
     }
   return WRONG_VALUE;
  }
//+------------------------------------------------------------------+

Здесь мы в цикле по всем графическим элементам в коллекции получаем очередной объект и, если его идентификатор графика и имя равны переданным в метод, возвращаем индекс цикла. По завершении цикла возвращаем -1.
Тут хочется уточнить почему не используем быстрый поиск при помощи класса библиотеки CSelect. Дело в том, что мы ищем именно индекс объекта в полном списке всей коллекции. Тогда как фильтрация списка по свойствам создаёт новые списки, и мы получим индекс объекта не из списка-коллекции, а из отфильтрованного списка. Естественно, их индексы в большинстве случаев будут несовпадающими.

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

//+------------------------------------------------------------------+
//|Находит объект, имеющийся в коллекции, но отсутствующий на графике|
//| Возвращает указатель на объект и индекс объекта в списке.        |
//+------------------------------------------------------------------+
CGStdGraphObj *CGraphElementsCollection::FindMissingObj(const long chart_id,int &index)
  {
   index=WRONG_VALUE;
   for(int i=0;i<this.m_list_all_graph_obj.Total();i++)
     {
      CGStdGraphObj *obj=this.m_list_all_graph_obj.At(i);
      if(obj==NULL)
         continue;
      if(!this.IsPresentGraphObjOnChart(obj.ChartID(),obj.Name()))
        {
         index=i;
         return obj;
        }
     }
   return NULL;
  }
//+------------------------------------------------------------------+

Ранее мы сначала получали список объектов по идентификатору графика, и делали цикл по полученному списку. Это было не верно.
Теперь мы делаем цикл по всему списку-коллекции и, соответственно, получаем правильный индекс объекта в списке.


Теперь о том, что в советнике из прошлой статьи у нас с мышкой могли взаимодействовать только объекты-формы.

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

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

Исправим это.

В методе GetFormUnderCursor() впишем изменение:

//--- Если список получить удалось и он не пустой
   if(list!=NULL && list.Total()>0)
     {
      //--- Получаем единственный в нём графический элемент
      elm=list.At(0);
      //--- Если этот элемент - объект-форма, или её наследники
      if(elm.TypeGraphElement()>=GRAPH_ELEMENT_TYPE_FORM)
        {
         //--- Присваиваем указатель на элемент указателю на объект-форму
         form=elm;
         //--- Получаем состояние мышки относительно формы
         mouse_state=form.MouseFormState(id,lparam,dparam,sparam);
         //--- Если курсор в пределах формы - возвращаем указатель на форму
         if(mouse_state>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL)
            return form;
        }
     }
//--- Если нет ни одного объекта-формы с установленным флагом взаимодействия -
//--- в цикле по всем объектам класса-коллекции графических элементов
   int total=this.m_list_all_canv_elm_obj.Total();
   for(int i=0;i<total;i++)
     {
      //--- получаем очередной элемент
      elm=this.m_list_all_canv_elm_obj.At(i);
      if(elm==NULL)
         continue;
      //--- если полученный элемент - объект-форма, или её наследники
      if(elm.TypeGraphElement()>=GRAPH_ELEMENT_TYPE_FORM)
        {
         //--- Присваиваем указатель на элемент указателю на объект-форму
         form=elm;
         //--- Получаем состояние мышки относительно формы
         mouse_state=form.MouseFormState(id,lparam,dparam,sparam);
         //--- Если курсор в пределах формы - возвращаем указатель на форму
         if(mouse_state>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL)
            return form;
        }
     }
//--- Если нет ни одного объекта-формы из списка-коллекции -
//--- Получаем список расширенных стандартных графических объектов

Ранее здесь было сравнение на равенство ("=="). Сейчас стало "больше либо равно". А так как значения констант перечисления типов графических элементов идут по возрастанию, то все последующие типы будут иметь значение константы больше, чем значение константы GRAPH_ELEMENT_TYPE_FORM.


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

//+------------------------------------------------------------------+
//| Сбрасывает всем формам флаги взаимодействия кроме указанной      |
//+------------------------------------------------------------------+
void CGraphElementsCollection::ResetAllInteractionExeptOne(CGCnvElement *form_exept)
  {
   //--- В цикле по всем объектам класса-коллекции графических элементов
   int total=this.m_list_all_canv_elm_obj.Total();
   for(int i=0;i<total;i++)
     {
      //--- получаем указатель на объект
      CGCnvElement *obj=this.m_list_all_canv_elm_obj.At(i);
      //--- если указатель получить не удалось, или это не форма, либо её наследники, или это та форма, указатель на которую передан в метод - идём далее
      if(obj==NULL || obj.TypeGraphElement()<GRAPH_ELEMENT_TYPE_FORM || (obj.Name()==form_exept.Name() && obj.ChartID()==form_exept.ChartID()))
         continue;
      //--- Сбрасываем флаг взаимодействия текущей форме в цикле
      obj.SetInteraction(false);
     }
  }
//+------------------------------------------------------------------+

Раньше здесь было сравнение на неравенство ("!=") и, соответственно все объекты, кроме объектов-форм пропускались. Теперь же пропускаться будут все объекты, стоящие в иерархии наследования ниже объекта-формы.


В методе SetZOrderMAX() немного изменим тестовый текст, выводящийся на графическом объекте (так как в тестовом советнике тоже эти тексты будут изменены), и поправим ошибку, не дающую взаимодействовать объектам с мышкой:

//+------------------------------------------------------------------+
//| Устанавливает ZOrder в указанный элемент,                        |
//| а в остальных элементах корректирует                             |
//+------------------------------------------------------------------+
bool CGraphElementsCollection::SetZOrderMAX(CGCnvElement *obj)
  {
//--- ...

//--- Временно объявим объект-форму - для рисования на нём текста для визуального отображения его ZOrder
   CForm *form=obj;
//--- и нарисуем на форме текст с указанием ZOrder
   form.TextOnBG(0,form.TypeElementDescription()+": ID "+(string)form.ID()+", ZD "+(string)form.Zorder(),form.Width()/2,form.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',255,true,true);
//--- Отсортируем список графических элементов по идентификатору элемента
   this.m_list_all_canv_elm_obj.Sort(SORT_BY_CANV_ELEMENT_ID);
//--- ...

//--- ...


//--- В цикле по полученному списку оставшихся объектов-графических элементов
   for(int i=0;i<list.Total();i++)
     {
      //--- получаем очередной объект
      CGCnvElement *elm=list.At(i);
      //--- Если объект получить не удалось, или это контрольная форма управления опорными точками расширенного стандартного графического объекта
      //--- или, если ZOrder объекта нулевой - пропускаем этот объект, так как изменять его ZOrder не нужно - он самий нижний
      if(elm==NULL || elm.Type()==OBJECT_DE_TYPE_GFORM_CONTROL || elm.Zorder()==0)
         continue;
      //--- Если не удалось установить ZOrder объекту на 1 меньше, чем он есть (ZOrder уменьшаем на 1) - добавляем к значению res значение false
      if(!elm.SetZorder(elm.Zorder()-1,false))
         res &=false;
      //--- Временно - для теста, если этот элемент - форма
      if(elm.Type()>=OBJECT_DE_TYPE_GFORM)
        {
         //--- присвоим форме указатель на элемент и нарисуем на форме текст с указанием ZOrder 
         form=elm;
         form.TextOnBG(0,form.TypeElementDescription()+": ID "+(string)form.ID()+", ZD "+(string)form.Zorder(),form.Width()/2,form.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',255,true,true);
        }
     }
//--- По окончании цикла возвращаем результат, записанный в res
   return res;
  }
//+------------------------------------------------------------------+


Напишем обработчик события деинициализации:

//+------------------------------------------------------------------+
//| Обработчик события деинициализации                               |
//+------------------------------------------------------------------+
void CGraphElementsCollection::OnDeinit(void)
  {
   this.m_list_all_canv_elm_obj.Clear();
  }
//+------------------------------------------------------------------+

Здесь всё просто — очищаем список-коллекцию графических элементов.

Соответственно, этот метод должен вызываться из главного объекта библиотеки CEngine в файле \MQL5\Include\DoEasy\Engine.mqh.

Откроем файл этого класса и впишем в его обработчик OnDeinit() вызов этого метода из класса-коллекции графических элементов:

//+------------------------------------------------------------------+
//| Деинициализация библиотеки                                       |
//+------------------------------------------------------------------+
void CEngine::OnDeinit(void)
  {
   this.m_indicators.GetList().Clear();
   this.m_graph_objects.OnDeinit();
  }
//+------------------------------------------------------------------+

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


Тестирование

Для тестирования возьмём советник из прошлой статьи и сохраним его в новой папке \MQL5\Experts\TestDoEasy\Part102\ под новым именем TestDoEasyPart102.mq5.

Существенных изменений никаких делать не будем. Лишь изменим координаты расположения графических объектов — чтобы дистанция между ними была чуть короче, и увеличим размер панели. Для создания панели будем использовать такой код в обработчике OnInit():

//--- Создадим объект WinForms Panel
   CPanel *pnl=NULL;
   obj_id=engine.GetGraphicObjCollection().CreatePanel(ChartID(),0,"WFPanel",elm.RightEdge()+20,50,230,150,array_clr[0],200,true,true);
   list=engine.GetListCanvElementByID(ChartID(),obj_id);
   pnl=list.At(0);
   if(pnl!=NULL)
     {
      pnl.FontDrawStyle(FONT_STYLE_NORMAL);
      pnl.Bold(true);
      Print(DFUN,EnumToString(pnl.FontDrawStyle()));
      Print(DFUN,EnumToString(pnl.FontBoldType()));
      pnl.SetFontSize(10);
      pnl.TextOnBG(0,pnl.TypeElementDescription()+": ID "+(string)pnl.ID()+", ZD "+(string)pnl.Zorder(),pnl.Width()/2,pnl.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',pnl.Opacity());
      pnl.Update(true);
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Если панель успешно создана, устанавливаем для её текстов тип начертания "Normal", устанавливаем "жирный" шрифт и выводим описание стиля шрифта и типа его толщины в журнал.

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

Все эти изменения можно посмотреть в прикреплённых к статье файлах.

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


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


Что дальше

В следующей статье продолжим развитие класса объекта-панели.

Ниже прикреплены все файлы текущей версии библиотеки, файлы тестового советника и индикатора контроля событий графиков для MQL5. Их можно скачать и протестировать всё самостоятельно. При возникновении вопросов, замечаний и пожеланий вы можете озвучить их в комментариях к статье.

К содержанию

*Статьи этой серии:

DoEasy. Элементы управления (Часть 1): Первые шаги


Прикрепленные файлы |
MQL5.zip (4461.25 KB)
Несколько индикаторов на графике (Часть 03): Разработка пользовательских определений Несколько индикаторов на графике (Часть 03): Разработка пользовательских определений
Сегодня мы впервые обновляем функциональность системы индикаторов. В предыдущей статье "Несколько индикаторов на одном графике" мы рассмотрели основы кода, позволяющего использовать более одного индикатора в подокне, но то, что было представлено, было лишь начальной основой для гораздо более крупной системы.
Как сделать график более интересным: добавление фона Как сделать график более интересным: добавление фона
Многие рабочие терминалы содержат некое репрезентативное изображение, которое показывает что-то о пользователе, эти изображения делают рабочий стол более красивым и разнообразным. Давайте посмотрим, как сделать графики более интересными, добавив фон.
DoEasy. Элементы управления (Часть 3): Создание привязанных элементов управления DoEasy. Элементы управления (Часть 3): Создание привязанных элементов управления
В статье разберём создание подчинённых элементов управления, привязанных к базовому элементу, создаваемых непосредственно при помощи функционала базового элемента управления. Помимо поставленной выше задачи, немного поработаем над объектом-тенью графического элемента, так как при её использовании для любого из объектов, позволяющих иметь тень, до сих пор есть неисправленные ошибки логики
Несколько индикаторов на графике (Часть 02): Первые эксперименты Несколько индикаторов на графике (Часть 02): Первые эксперименты
В предыдущей статье "Несколько индикаторов на графике" я представил концепции и основы того, как мы можем использовать несколько индикаторов на графике. В данной статье я представлю и детально объясню исходный код.