English 中文 Español Deutsch 日本語 Português
Графические интерфейсы II: Элементы "Разделительная линия" и "Контекстное меню" (Глава 2)

Графические интерфейсы II: Элементы "Разделительная линия" и "Контекстное меню" (Глава 2)

MetaTrader 5Примеры | 24 февраля 2016, 13:35
5 364 3
Anatoli Kazharski
Anatoli Kazharski

Содержание.  


 

Введение

О том, для чего предназначена эта библиотека, более подробно можно прочитать в первой статье серии: Графические интерфейсы I: Подготовка структуры библиотеки (Глава 1). В конце каждой из статей серии представлен список глав со ссылками. Там же есть возможность загрузить к себе на компьютер полную версию библиотеки на текущей стадии разработки. Файлы нужно разместить по тем же директориям, как они расположены в архиве.  

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



Разработка класса для создания разделительной линии

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

Чтобы имитировать рельефность, разделительная линия должна состоять минимум из двух частей. Если одну линию сделать светлее фона, а другую — темнее, то визуально мы получим своеобразную канавку на поверхности. Есть два варианта для создания разделительной линии: (1) из двух объектов-примитивов типа CRectLabel, который у нас уже есть в файле Objects.mqh, или (2) создать объект типа OBJ_BITMAP_LABEL и использовать его как холст для рисования. Воспользуемся вторым вариантом. Для рисования стандартная библиотека предлагает класс CCanvas. В этом классе уже есть все необходимые методы для рисования простых геометрических фигур, что существенно облегчит реализацию задуманного и сэкономит много времени. 

Класс CCanvas нужно внедрить в нашу разрабатываемую библиотеку таким образом, чтобы была возможность использовать его подобно тем объектам-примитивам, которые сейчас содержатся в файле Objects.mqh. Этого легко можно добиться, сделав класс CCanvas производным от класса CChartObjectBmpLabel. Но придётся внести маленькое изменение в код класса CCanvas, чтобы впоследствии не возникло ошибок или предупреждений при компиляции программы. Дело в том, что и в классе CCanvas, и в классе CChartObject, который является базовым для класса CChartObjectBmpLabel, есть поле (переменная) m_chart_id. В результате компилятор выдаст предупреждение о том, что переменная с таким именем уже существует:

Рис. 1. Предупреждение компилятора о том, что переменная с таким именем уже существует.

Рис. 1. Предупреждение компилятора

 

На самом деле такое предупреждение не вызовет критических ошибок, и компиляция, несмотря на это, совершится. Однако рекомендуется делать всё так, чтобы избежать подобных ситуаций, ведь неизвестно, как это может отразиться в конечном итоге на работе программы. Возьмём это за правило и будем придерживаться его. Тем более, изменения в файл Canvas.mqh придется внести в любом случае: класс CCanvas нужно сделать производным от класса CChartObjectBmpLabel. Таким образом, избавиться от навязчивого предупреждения можно очень легко. Мы просто удалим переменную m_chart_id в классе CCanvas. Но, внося изменения в классы стандартной библиотеки, нужно помнить о том, что при следующем обновлении терминала файлы стандартной библиотеки тоже могут обновиться, а значит, и изменения обнулятся. Поэтому, раз уж для достижения наших целей не получится обойтись без внесения изменений в класс CCanvas, создадим его копию и поместим в директорию, где лежат файлы нашей библиотеки.

Сейчас в папке #Include создайте папку Canvas. Создайте копию файла с классом СCanvas и переименуйте его в CustomCanvas.mqh, а класс — в CCustomCanvas. Подключите к файлу CustomCanvas.mqh файл стандартной библиотеки ChartObjectsBmpControls.mqh и сделайте класс CCustomCanvas производным от класса CChartObjectBmpLabel. Затем удалите из тела класса CCustomCanvas и из конструктора переменную m_chart_id.

//+------------------------------------------------------------------+
//|                                                 CustomCanvas.mqh |
//|                   Copyright 2009-2013, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include <Files\FileBin.mqh>
#include <Controls\Rect.mqh>
#include <ChartObjects\ChartObjectsBmpControls.mqh>

//...

//+------------------------------------------------------------------+
//| Class CCustomCanvas                                              |
//| Usage: class for working with a dynamic resource                 |
//+------------------------------------------------------------------+
class CCustomCanvas : public CChartObjectBmpLabel
  {
//...

Теперь к файлу Objects.mqh нужно подключить файл CustomCanvas.mqh:

//+------------------------------------------------------------------+
//|                                                      Objects.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Enums.mqh"
#include "Defines.mqh"
#include "..\Canvas\CustomCanvas.mqh"
#include <ChartObjects\ChartObjectsBmpControls.mqh>
#include <ChartObjects\ChartObjectsTxtControls.mqh>

Далее создаем класс CRectCanvas, который нужно сделать производным от класса CCustomCanvas. Класс CRectCanvas будет подобен всем другим классам, которые находятся в файле Objects.mqh (их содержание рассматривалось в предыдущей статье). Теперь его можно будет использовать для рисования любых других элементов интерфейса, которые при этом будут частью разрабатываемой библиотеки. 

Итак, у нас всё готово для разработки класса CSeparateLine, который будет предназначен для создания разделительной линии. В папке Controls создайте файл SeparateLine.mqh. К нему подключите файлы Element.mqh и Window.mqh. Затем нужно:

1) создать класс CSeparateLine;

2) объявить в нём указатель на форму, к которой будет присоединяться элемент, а также создать метод для сохранения указателя в нём;

3) создать экземпляр класса CRectCanvas;

4) объявить и имплементировать стандартные виртуальные методы для всех элементов, с помощью которых можно управлять элементом.

//+------------------------------------------------------------------+
//|                                                 SeparateLine.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
#include "Window.mqh"
//+------------------------------------------------------------------+
//| Класс для создания разделительной линии                          |
//+------------------------------------------------------------------+
class CSeparateLine : public CElement
  {
private:
   //--- Указатель на форму, к которой элемент присоединён
   CWindow          *m_wnd;
   //--- Объект для создания разделительной линии
   CRectCanvas       m_canvas;
   //---
public:
                     CSeparateLine(void);
                    ~CSeparateLine(void);
   //--- Сохраняет указатель переданной формы
   void              WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); }
   //---
public:
   //--- Обработчик события графика
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   //--- Перемещение элемента
   virtual void      Moving(const int x,const int y);
   //--- (1) Показ, (2) скрытие, (3) сброс, (4) удаление
   virtual void      Show(void);
   virtual void      Hide(void);
   virtual void      Reset(void);
   virtual void      Delete(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CSeparateLine::CSeparateLine(void)
  {
//--- Сохраним имя класса элемента в базовом классе  
   CElement::ClassName(CLASS_NAME);
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CSeparateLine::~CSeparateLine(void)
  {
  }
//+------------------------------------------------------------------+

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

  • Тип разделительной линии: (1) горизонтальная, (2) вертикальная.
  • Цвет тёмной части.
  • Цвет светлой части.

В файл Enums.mqh нужно добавить перечисление, которое будет использоваться для указания типа:

//+------------------------------------------------------------------+
//| Перечисление типа разделительной линии                           |
//+------------------------------------------------------------------+
enum ENUM_TYPE_SEP_LINE
  {
   H_SEP_LINE =0,
   V_SEP_LINE =1
  };

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

class CSeparateLine : public CElement
  {
private:
   //--- Свойства
   ENUM_TYPE_SEP_LINE m_type_sep_line;   
   color             m_dark_color;
   color             m_light_color;
   //---
public:
   //--- (1) Тип линии, (2) цвета линии
   void              TypeSepLine(const ENUM_TYPE_SEP_LINE type) { m_type_sep_line=type; }
   void              DarkColor(const color clr)                 { m_dark_color=clr;     }
   void              LightColor(const color clr)                { m_light_color=clr;    }
   //---
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CSeparateLine::CSeparateLine(void) : m_type_sep_line(H_SEP_LINE),
                                     m_dark_color(clrBlack),
                                     m_light_color(clrDimGray)
  {
  }

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

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

class CSeparateLine : public CElement
  {
public:
   //--- Создание разделительной линии
   bool              CreateSeparateLine(const long chart_id,const int subwin,const int index,
                                        const int x,const int y,const int x_size,const int y_size);
   //---
private:
   //--- Создаёт холст для рисования разделительной линии
   bool              CreateSepLine(void);
   //--- Рисует разделительную линию
   void              DrawSeparateLine(void);
   //---
  };

Код метода CreateSeparateLine() ничем особым не отличается от других подобных методов (например, в классе CMenuItem), поэтому перейдём к рассмотрению кода метода CreateSepLine(). 

В самом начале, как и во всех методах подобного рода, формируется имя графического объекта. Далее создаётся графический объект (холст), на котором можно будет рисовать. Здесь следует обратить внимание на то, что для создания объекта типа OBJ_BITMAP_LABEL используется метод CreateBitmapLabel(), который принадлежит классу CCustomCanvas. В этом классе при создании объектов не предусмотрено присоединение объекта к графику, как это реализовано в классе CChartObjectBmpLabel, где сразу после создания объекта используется метод CChartObject::Attach() базового класса. Поэтому здесь мы должны сами об этом позаботиться. Так как до этого мы уже сделали класс CCustomCanvas производным от класса CChartObjectBmpLabel, то нам уже доступен метод CChartObject::Attach() его базового класса. Если не присоединить объект к графику, то не получится управлять им.

После создания объекта, присоединения его к графику и установки свойств на нашем условном холсте можно нарисовать разделительную линию с помощью метода DrawSeparateLine(). Это показано в коде ниже. Затем указатель объекта сохраняется в массиве базового класса CElement.

//+------------------------------------------------------------------+
//| Создание холста для рисования разделительной линии                 |
//+------------------------------------------------------------------+
bool CSeparateLine::CreateSepLine(void)
  {
//--- Формирование имени объекта  
   string name=CElement::ProgramName()+"_separate_line_"+(string)CElement::Index()+"__"+(string)CElement::Id();
//--- Создание объекта
   if(!m_canvas.CreateBitmapLabel(m_chart_id,m_subwin,name,m_x,m_y,m_x_size,m_y_size,COLOR_FORMAT_ARGB_NORMALIZE))
      return(false);
//--- Прикрепление к графику
   if(!m_canvas.Attach(m_chart_id,name,m_subwin,1))
      return(false);
//--- Свойства
   m_canvas.Background(false);
//--- Отступы от крайней точки
   m_canvas.XGap(m_x-m_wnd.X());
   m_canvas.YGap(m_y-m_wnd.Y());
//--- Нарисовать разделительную линию
   DrawSeparateLine();
//--- Добавить в массив
   CElement::AddToArray(m_canvas);
   return(true);
  }

Код метода DrawSeparateLine() прост для понимания. Сначала получаем размеры холста. Затем очищаем холст, делая его прозрачным с помощью метода CCustomCanvas::Erase(). Далее, в зависимости от того, какую нужно линию нарисовать (горизонтальную или вертикальную) программа перейдёт в соответствующий блок кода. Для примера опишем построение горизонтальной линии. Сначала определяются координаты для двух точек линии, затем отрисовывается первая линия в верхней части холста. Координаты для второй линии определяются в нижней части холста. Если высоту холста установили в два пикселя, то линии будут идти сразу одна за другой. Но вы можете сделать отступ между верхней и нижней линиями, установив высоту холста больше двух. В самом конце метода для отображения изменений нужно обязательно обновить холст с помощью метода CCustomCanvas::Update().

//+------------------------------------------------------------------+
//|  Рисует разделительную линию                                     |
//+------------------------------------------------------------------+
void CSeparateLine::DrawSeparateLine(void)
  {
//--- Координаты для линий
   int x1=0,x2=0,y1=0,y2=0;
//--- Размеры холста
   int   x_size =m_canvas.X_Size()-1;
   int   y_size =m_canvas.Y_Size()-1;
//--- Очистить холст
   m_canvas.Erase(::ColorToARGB(clrNONE,0));
//--- Если линия горизонтальная
   if(m_type_sep_line==H_SEP_LINE)
     {
      //--- Сверху тёмная линия
      x1=0;
      y1=0;
      x2=x_size;
      y2=0;
      //---
      m_canvas.Line(x1,y1,x2,y2,::ColorToARGB(m_dark_color));
      //--- Снизу светлая линия
      x1=0;
      x2=x_size;
      y1=y_size;
      y2=y_size;
      //---
      m_canvas.Line(x1,y1,x2,y2,::ColorToARGB(m_light_color));
     }
//--- Если линия вертикальная
   else
     {
      //--- Слева тёмная линия
      x1=0;
      x2=0;
      y1=0;
      y2=y_size;
      //---
      m_canvas.Line(x1,y1,x2,y2,::ColorToARGB(m_dark_color));
      //--- Справа светлая линия
      x1=x_size;
      y1=0;
      x2=x_size;
      y2=y_size;
      //---
      m_canvas.Line(x1,y1,x2,y2,::ColorToARGB(m_light_color));
     }
//--- Обновление холста
   m_canvas.Update();
  }

 



Тест установки разделительной линии

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

Очень кратко напомню о процессе добавления элемента на форму.

  • Если класса элемента ещё нет в базе, то его файл нужно подключить к файлу WndContainer.mqh.
  • Так как пользовательский класс приложения (в нашем случае это CProgram) должен быть производным от CWndContainer -> CWndEvents, то после подключения файла элемента становится доступным создание экземпляра класса элемента и метод для него.
  • Затем надо вызвать метод создания элемента в методе, где создаётся графический интерфейс программы.

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

Рис. 2. Тест элемента интерфейса «разделительная линия».

Рис. 2. Тест элемента интерфейса «разделительная линия».


Разработка класса CSeparateLine завершена, и теперь у нас всё готово, чтобы начать реализацию класса для создания контекстного меню.

 


Разработка класса для создания контекстного меню

До этого момента в процессе разработки библиотеки были созданы три элемента интерфейса: (1) форма для элементов управления (CWindow), (2) элемент управления «пункт меню» (CMenuItem) и (3) элемент «разделительная линия» (CSeparateLine). Каждый из них можно отнести к простому типу элементов, так как собираются они только из объектов-примитивов. А вот контекстное меню уже относится к сложному (составному) типу элементов управления. Оно будет собираться не только из объектов-примитивов, но и из других элементов, базовым классом которых также является CElement

В директории разрабатываемой библиотеки в папке Controls создайте файл ContextMenu.mqh и подключите к нему файлы, которые понадобятся для создания контекстного меню:

//+------------------------------------------------------------------+
//|                                                  ContextMenu.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
#include "Window.mqh"
#include "MenuItem.mqh"
#include "SeparateLine.mqh"

Затем нужно создать класс CContextMenu со стандартным для всех элементов библиотеки набором виртуальных методов, указателем на форму и методом для его сохранения. В качестве фона элемента нужен объект типа OBJ_RECTANGLE_LABEL, поэтому для его создания будем использовать класс CRectLabel из файла Object.mqh. Для пунктов меню в этой статье ранее был создан класс CMenuItem. Так как их в контекстном меню обычно более одного, причем это количество неизвестно заранее, то нужно объявить динамический массив экземпляров этого класса. То же самое относится и к разделительным линиям (CSeparateLine) контекстного меню.

//+------------------------------------------------------------------+
//| Класс для создания контекстного меню                             |
//+------------------------------------------------------------------+
class CContextMenu : public CElement
  {
private:
   //--- Указатель на форму, к которой элемент присоединён
   CWindow          *m_wnd;
   //--- Объекты для создания пункта меню
   CRectLabel        m_area;
   CMenuItem         m_items[];
   CSeparateLine     m_sep_line[];
   //---
public:
                     CContextMenu(void);
                    ~CContextMenu(void);
   //--- Сохраняет указатель переданной формы
   void              WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); }

   //--- Обработчик событий графика
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   //--- Таймер
   virtual void      OnEventTimer(void);
   //--- Перемещение элемента
   virtual void      Moving(const int x,const int y);
   //--- (1) Показ, (2) скрытие, (3) сброс, (4) удаление
   virtual void      Show(void);
   virtual void      Hide(void);
   virtual void      Reset(void);
   virtual void      Delete(void);
   //--- (1) Установка, (2) сброс приоритетов на нажатие левой кнопки мыши
   virtual void      SetZorders(void);
   virtual void      ResetZorders(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CContextMenu::CContextMenu(void)
  {
//--- Сохраним имя класса элемента в базовом классе
   CElement::ClassName(CLASS_NAME);
//--- Контекстное меню является выпадающим элементом
   CElement::m_is_dropdown=true;
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CContextMenu::~CContextMenu(void)
  {
  }
//+------------------------------------------------------------------+

Для настройки внешнего вида контекстного меню понадобятся соответствующие поля и методы:

class CContextMenu : public CElement
  {
private:
   //--- Свойства фона
   int               m_area_zorder;
   color             m_area_color;
   color             m_area_border_color;
   color             m_area_color_hover;
   color             m_area_color_array[];
   //--- Свойства пункта меню
   int               m_item_y_size;
   color             m_item_back_color;
   color             m_item_border_color;
   color             m_item_back_color_hover;
   color             m_item_back_color_hover_off;
   color             m_label_color;
   color             m_label_color_hover;
   string            m_right_arrow_file_on;
   string            m_right_arrow_file_off;
   //--- Свойства разделительной линии
   color             m_sepline_dark_color;
   color             m_sepline_light_color;
   //---
public:
   //--- Количество пунктов меню
   int               ItemsTotal(void)                         const { return(::ArraySize(m_items));         }
   //--- Методы для настройки внешнего вида контекстного меню:
   //    Цвет фона контекстного меню
   void              MenuBackColor(const color clr)                 { m_area_color=clr;                     }
   void              MenuBorderColor(const color clr)               { m_area_border_color=clr;              }
   //--- (1) Высота, (2) цвет фона и (3) цвет рамки пункта меню 
   void              ItemYSize(const int y_size)                    { m_item_y_size=y_size;                 }
   void              ItemBackColor(const color clr)                 { m_item_back_color=clr;                }
   void              ItemBorderColor(const color clr)               { m_item_border_color=clr;              }
   //--- Цвет фона (1) доступного и (2) заблокированного пункта меню при наведении курсора мыши
   void              ItemBackColorHover(const color clr)            { m_item_back_color_hover=clr;          }
   void              ItemBackColorHoverOff(const color clr)         { m_item_back_color_hover_off=clr;      }
   //--- Цвет текста (1) обычный и (2) в фокусе
   void              LabelColor(const color clr)                    { m_label_color=clr;                    }
   void              LabelColorHover(const color clr)               { m_label_color_hover=clr;              }
   //--- Определение картинки для признака наличия контекстного меню в пункте
   void              RightArrowFileOn(const string file_path)       { m_right_arrow_file_on=file_path;      }
   void              RightArrowFileOff(const string file_path)      { m_right_arrow_file_off=file_path;     }
   //--- (1) Тёмный и (2) светлый цвет разделительной линии
   void              SeparateLineDarkColor(const color clr)         { m_sepline_dark_color=clr;             }
   void              SeparateLineLightColor(const color clr)        { m_sepline_light_color=clr;            }
   //---
  };

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

Добавление указателя и методов для его сохранения и получения в классе CContextMenu:

class CContextMenu : public CElement
  {
private:
   //--- Указатель на предыдущий узел
   CMenuItem        *m_prev_node;
   //---
public:
   //--- Получение и сохранение указателя предыдущего узла
   CMenuItem        *PrevNodePointer(void)                    const { return(m_prev_node);                  }
   void              PrevNodePointer(CMenuItem &object)             { m_prev_node=::GetPointer(object);     }
   //---
  };

То же самое нужно добавить и в класс CMenuItem:

class CMenuItem : public CElement
  {
private:
   //--- Указатель на предыдущий узел
   CMenuItem        *m_prev_node;
   //---
public:
   //--- Получение и сохранение указателя предыдущего узла
   CMenuItem        *PrevNodePointer(void)                    const { return(m_prev_node);                  }
   void              PrevNodePointer(CMenuItem &object)             { m_prev_node=::GetPointer(object);     }
   //---
  };

Графический интерфейс будет строиться в пользовательском классе приложения (CProgram). В нем при создании контекстного меню понадобится метод, с помощью которого перед созданием можно будет указать будущее количество пунктов в контекстном меню, а также уникальные значения некоторых параметров, не общих для всех пунктов. Напишем метод CContextMenu::AddItem(), который в качестве параметров принимает: (1) текст пункта меню, (2) путь к картинке для ярлыка доступного пункта, (3) путь к картинке для заблокированного пункта и (4) тип пункта меню. Также понадобятся массивы, в которых будут сохраняться переданные значения. Размер этих массивов будет увеличиваться на один элемент каждый раз при вызове метода CContextMenu::AddItem().

class CContextMenu : public CElement
  {
private:
   //--- Массивы свойств пунктов меню:
   //    (1) Текст, (2) ярлык доступного пункта, (3) ярлык заблокированного пункта
   string            m_label_text[];
   string            m_path_bmp_on[];
   string            m_path_bmp_off[];
   //---
public:
   //--- Добавляет пункт меню с указанными свойствами до создания контекстного меню
   void              AddItem(const string text,const string path_bmp_on,const string path_bmp_off,const ENUM_TYPE_MENU_ITEM type);
   //---
  };
//+------------------------------------------------------------------+
//| Добавляет пункт меню                                             |
//+------------------------------------------------------------------+
void CContextMenu::AddItem(const string text,const string path_bmp_on,const string path_bmp_off,const ENUM_TYPE_MENU_ITEM type)
  {
//--- Увеличим размер массивов на один элемент
   int array_size=::ArraySize(m_items);
   ::ArrayResize(m_items,array_size+1);
   ::ArrayResize(m_label_text,array_size+1);
   ::ArrayResize(m_path_bmp_on,array_size+1);
   ::ArrayResize(m_path_bmp_off,array_size+1);
//--- Сохраним значения переданных параметров
   m_label_text[array_size]   =text;
   m_path_bmp_on[array_size]  =path_bmp_on;
   m_path_bmp_off[array_size] =path_bmp_off;
//--- Установка типа пункта меню
   m_items[array_size].TypeMenuItem(type);
  }

Для добавления разделительных линий в контекстное меню понадобится массив, где будет сохраняться номер индекса пункта меню, после которого эту линию нужно установить. Номер индекса пункта меню будет передаваться в метод CContextMenu::AddSeparateLine(), код которого показан ниже:

class CContextMenu : public CElement
  {
private:
   //--- Массив номеров индексов пунктов меню, после которых нужно установить разделительную линию
   int               m_sep_line_index[];
   //---
public:
   //--- Добавляет разделительную линию после указанного пункта до создания контекстного меню
   void              AddSeparateLine(const int item_index);
   //---
  };
//+------------------------------------------------------------------+
//| Добавляет разделительную линию                                   |
//+------------------------------------------------------------------+
void CContextMenu::AddSeparateLine(const int item_index)
  {
//--- Увеличим размер массивов на один элемент
   int array_size=::ArraySize(m_sep_line);
   ::ArrayResize(m_sep_line,array_size+1);
   ::ArrayResize(m_sep_line_index,array_size+1);
//--- Сохраним номер индекса
   m_sep_line_index[array_size]=item_index;
  }

Понадобятся методы, с помощью которых, указывая номер индекса пункта меню, можно получить (1) указатель пункта меню, (2) описание (отображаемый текст) и (3) тип. В каждом методе, перед тем как вернуть значение свойства, сначала производится проверка на выход из диапазона массива и производится корректировка индекса. Это реализовано таким образом, что если переданный индекс больше размера массива, то обращение будет к последнему пункту, а если меньше нуля, то к первому. 

class CContextMenu : public CElement
  {
public:
   //--- Возвращает указатель пункта из контекстного меню
   CMenuItem        *ItemPointerByIndex(const int index);
   //--- Возвращает описание (отображаемый текст)
   string            DescriptionByIndex(const int index);
   //--- Возвращает тип пункта меню
   ENUM_TYPE_MENU_ITEM TypeMenuItemByIndex(const int index);
   //---
  };
//+------------------------------------------------------------------+
//| Возвращает указатель пункта меню по индексу                      |
//+------------------------------------------------------------------+
CMenuItem *CContextMenu::ItemPointerByIndex(const int index)
  {
   int array_size=::ArraySize(m_items);
//--- Если нет ни одного пункта в контекстном меню, сообщить об этом
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > Вызов этого метода нужно осуществлять, "
              "когда в контекстном меню есть хотя бы один пункт!");
     }
//--- Корректировка в случае выхода из диапазона
   int i=(index>=array_size)? array_size-1 :(index<0)? 0 : index;
//--- Вернуть указатель
   return(::GetPointer(m_items[i]));
  }
//+------------------------------------------------------------------+
//| Возвращает название пункта по индексу                            |
//+------------------------------------------------------------------+
string CContextMenu::DescriptionByIndex(const int index)
  {
   int array_size=::ArraySize(m_items);
//--- Если нет ни одного пункта в контекстном меню, сообщить об этом
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > Вызов этого метода нужно осуществлять, "
              "когда в контекстном меню есть хотя бы один пункт!");
     }
//--- Корректировка в случае выхода из диапазона
   int i=(index>=array_size)? array_size-1 :(index<0)? 0 : index;
//--- Вернуть описание пункта
   return(m_items[i].LabelText());
  }
//+------------------------------------------------------------------+
//| Возвращает тип пункта по индексу                                 |
//+------------------------------------------------------------------+
ENUM_TYPE_MENU_ITEM CContextMenu::TypeMenuItemByIndex(const int index)
  {
   int array_size=::ArraySize(m_items);
//--- Если нет ни одного пункта в контекстном меню, сообщить об этом
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > Вызов этого метода нужно осуществлять, "
              "когда в контекстном меню есть хотя бы один пункт!");
     }
//--- Корректировка в случае выхода из диапазона
   int i=(index>=array_size)? array_size-1 :(index<0)? 0 : index;
//--- Вернуть тип пункта
   return(m_items[i].TypeMenuItem());
  }

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

Рис. 3. Схема идентификаторов и индексов различных групп в контекстном меню.

Рис. 3. Схема идентификаторов и индексов различных групп в контекстном меню.

 

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

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

class CContextMenu : public CElement
  {
public:
   //--- (1) Получение и (2) установка состояния чекбокса
   bool              CheckBoxStateByIndex(const int index);
   void              CheckBoxStateByIndex(const int index,const bool state);
   //--- (1) Возвращает и (2) устанавливает id радио-пункта по индексу
   int               RadioItemIdByIndex(const int index);
   void              RadioItemIdByIndex(const int item_index,const int radio_id);
   //--- (1) Возвращает выделенный радио-пункт, (2) переключает радио-пункт
   int               SelectedRadioItem(const int radio_id);
   void              SelectedRadioItem(const int radio_index,const int radio_id);
   //---
  };
//+------------------------------------------------------------------+
//| Возвращает состояние чекбокса по индексу                         |
//+------------------------------------------------------------------+
bool CContextMenu::CheckBoxStateByIndex(const int index)
  {
   int array_size=::ArraySize(m_items);
//--- Если нет ни одного пункта в контекстном меню, сообщить об этом
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > Вызов этого метода нужно осуществлять, "
              "когда в контекстном меню есть хотя бы один пункт!");
     }
//--- Корректировка в случае выхода из диапазона
   int i=(index>=array_size)? array_size-1 :(index<0)? 0 : index;
//--- Вернуть состояние пункта
   return(m_items[i].CheckBoxState());
  }
//+------------------------------------------------------------------+
//| Устанавливает состояние чекбокса по индексу                      |
//+------------------------------------------------------------------+
void CContextMenu::CheckBoxStateByIndex(const int index,const bool state)
  {
//--- Проверка на выход из диапазона
   int size=::ArraySize(m_items);
   if(size<1 || index<0 || index>=size)
      return;
//--- Установить состояние
   m_items[index].CheckBoxState(state);
  }
//+------------------------------------------------------------------+
//| Возвращает id радио-пункта по индексу                            |
//+------------------------------------------------------------------+
int CContextMenu::RadioItemIdByIndex(const int index)
  {
   int array_size=::ArraySize(m_items);
//--- Если нет ни одного пункта в контекстном меню, сообщить об этом
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > Вызов этого метода нужно осуществлять, "
              "когда в контекстном меню есть хотя бы один пункт!");
     }
//--- Корректировка в случае выхода из диапазона
   int i=(index>=array_size)? array_size-1 :(index<0)? 0 : index;
//--- Вернуть идентификатор
   return(m_items[i].RadioButtonID());
  }
//+------------------------------------------------------------------+
//| Устанавливает id для радио-пункта по индексу                     |
//+------------------------------------------------------------------+
void CContextMenu::RadioItemIdByIndex(const int index,const int id)
  {
//--- Проверка на выход из диапазона
   int array_size=::ArraySize(m_items);
   if(array_size<1 || index<0 || index>=array_size)
      return;
//--- Установить идентификатор
   m_items[index].RadioButtonID(id);
  }
//+------------------------------------------------------------------+
//| Возвращает индекс радио-пункта по id                             |
//+------------------------------------------------------------------+
int CContextMenu::SelectedRadioItem(const int radio_id)
  {
//--- Счётчик радио-пунктов
   int count_radio_id=0;
//--- Пройдёмся в цикле по списку пунктов контекстного меню
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Перейти к следующему, если не радио-пункт
      if(m_items[i].TypeMenuItem()!=MI_RADIOBUTTON)
         continue;
      //--- Если идентификаторы совпадают
      if(m_items[i].RadioButtonID()==radio_id)
        {
         //--- Если это активный радио-пункт, выходим из цикла
         if(m_items[i].RadioButtonState())
            break;
         //--- Увеличить счётчик радио-пунктов
         count_radio_id++;
        }
     }
//--- Вернуть индекс
   return(count_radio_id);
  }
//+------------------------------------------------------------------+
//| Переключает радио-пункт по индексу и id                          |
//+------------------------------------------------------------------+
void CContextMenu::SelectedRadioItem(const int radio_index,const int radio_id)
  {
//--- Счётчик радио-пунктов
   int count_radio_id=0;
//--- Пройдёмся в цикле по списку пунктов контекстного меню
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Перейти к следующему, если не радио-пункт
      if(m_items[i].TypeMenuItem()!=MI_RADIOBUTTON)
         continue;
      //--- Если идентификаторы совпадают
      if(m_items[i].RadioButtonID()==radio_id)
        {
         //--- Переключить радио-пункт
         if(count_radio_id==radio_index)
            m_items[i].RadioButtonState(true);
         else
            m_items[i].RadioButtonState(false);
         //--- Увеличить счётчик радио-пунктов
         count_radio_id++;
        }
     }
  }

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

  • Создание фона контекстного меню.
  • Создание пунктов меню.
  • Создание разделительных линий.

Для каждого этапа нужен свой приватный (private) метод. В дальнейшем они будут вызываться в общем публичном (public) методе. Объявим их в теле класса:

class CContextMenu : public CElement
  {
public:
   //--- Методы для создания контекстного меню
   bool              CreateContextMenu(const long chart_id,const int window,const int x=0,const int y=0);
   //---
private:
   bool              CreateArea(void);
   bool              CreateItems(void);
   bool              CreateSeparateLine(const int line_number,const int x,const int y);
   //---
  };

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

//+------------------------------------------------------------------+
//| Создаёт общую площадь контекстного меню                          |
//+------------------------------------------------------------------+
bool CContextMenu::CreateArea(void)
  {
//--- Формирование имени объекта
   string name=CElement::ProgramName()+"_contextmenu_bg_"+(string)CElement::Id();
//--- Расчёт высоты контекстного меню зависит от количества пунктов меню и разделительных линий
   int items_total =ItemsTotal();
   int sep_y_size  =::ArraySize(m_sep_line)*9;
   m_y_size        =(m_item_y_size*items_total+2)+sep_y_size-(items_total-1);
//--- Установим фон контекстного меню
   if(!m_area.Create(m_chart_id,name,m_subwin,m_x,m_y,m_x_size,m_y_size))
      return(false);
//--- Установим свойства
   m_area.BackColor(m_area_color);
   m_area.Color(m_area_border_color);
   m_area.BorderType(BORDER_FLAT);
   m_area.Corner(m_corner);
   m_area.Selectable(false);
   m_area.Z_Order(m_area_zorder);
   m_area.Tooltip("\n");
//--- Отступы от крайней точки
   m_area.XGap(m_x-m_wnd.X());
   m_area.YGap(m_y-m_wnd.Y());
//--- Размеры фона
   m_area.XSize(m_x_size);
   m_area.YSize(m_y_size);
//--- Сохраним указатель объекта
   CElement::AddToArray(m_area);
   return(true);
  }

Установка разделительных линий будет производиться с помощью метода CContextMenu::CreateSeparateLine(). В качестве параметров этому методу нужно передать номер линии и координаты:

//+------------------------------------------------------------------+
//| Создаёт разделительную линию                                     |
//+------------------------------------------------------------------+
bool CContextMenu::CreateSeparateLine(const int line_number,const int x,const int y)
  {
//--- Сохраним указатель формы
   m_sep_line[line_number].WindowPointer(m_wnd);
//--- Установим свойства
   m_sep_line[line_number].TypeSepLine(H_SEP_LINE);
   m_sep_line[line_number].DarkColor(m_sepline_dark_color);
   m_sep_line[line_number].LightColor(m_sepline_light_color);
//--- Создание разделительной линии
   if(!m_sep_line[line_number].CreateSeparateLine(m_chart_id,m_subwin,line_number,x,y,m_x_size-10,2))
      return(false);
//--- Сохраним указатель объекта
   CElement::AddToArray(m_sep_line[line_number].Object(0));
   return(true);
  }

Метод CContextMenu::CreateSeparateLine() будет вызываться в методе, который предназначен для установки пунктов меню — CContextMenu::CreateItems(). В одном цикле будут определяться координаты и последовательность установки этих элементов. Ранее был рассмотрен массив m_sep_line_index[], куда при формировании контекстного меню будут сохраняться номера индексов пунктов меню, после которых должна устанавливаться разделительная линия. Сверяя номер текущей итерации цикла и номера индексов пунктов меню в массиве m_sep_line_index[], можно определить, после какого пункта меню нужно установить разделительную линию. 

Также перед установкой каждого пункта в контекстном меню необходимо сохранить указатель на предыдущий узел. Ниже представлен листинг кода метода CContextMenu::CreateItems() с подробными комментариями:

//+------------------------------------------------------------------+
//| Создаёт список пунктов меню                                      |
//+------------------------------------------------------------------+
bool CContextMenu::CreateItems(void)
  {
   int s =0;     // Для определения положения разделительных линий
   int x =m_x+1; // Координата X
   int y =m_y+1; // Координата Y. Будет рассчитываться в цикле для каждого пункта меню.
//--- Количество разделительных линий
   int sep_lines_total=::ArraySize(m_sep_line_index);
//---
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Расчёт координаты Y
      y=(i>0)? y+m_item_y_size-1 : y;
      //--- Сохраним указатель формы
      m_items[i].WindowPointer(m_wnd);
      //--- Добавим указатель на предыдущий узел
      m_items[i].PrevNodePointer(m_prev_node);
      //--- Установим свойства
      m_items[i].XSize(m_x_size-2);
      m_items[i].YSize(m_item_y_size);
      m_items[i].IconFileOn(m_path_bmp_on[i]);
      m_items[i].IconFileOff(m_path_bmp_off[i]);
      m_items[i].AreaBackColor(m_area_color);
      m_items[i].AreaBackColorOff(m_item_back_color_hover_off);
      m_items[i].AreaBorderColor(m_area_color);
      m_items[i].LabelColor(m_label_color);
      m_items[i].LabelColorHover(m_label_color_hover);
      m_items[i].RightArrowFileOn(m_right_arrow_file_on);
      m_items[i].RightArrowFileOff(m_right_arrow_file_off);
      m_items[i].IsDropdown(m_is_dropdown);
      //--- Отступы от крайней точки панели
      m_items[i].XGap(x-m_wnd.X());
      m_items[i].YGap(y-m_wnd.Y());
      //--- Создание пункта меню
      if(!m_items[i].CreateMenuItem(m_chart_id,m_subwin,i,m_label_text[i],x,y))
         return(false);
      //--- Перейти к следующему, если все разделительные линии установлены
      if(s>=sep_lines_total)
         continue;
      //--- Если индексы совпали, значит после этого пункта нужно установить разделительную линию
      if(i==m_sep_line_index[s])
        {
         //--- Координаты
         int l_x=x+5;
         y=y+m_item_y_size+2;
         //--- Установка разделительной линии
         if(!CreateSeparateLine(s,l_x,y))
            return(false);
         //--- Корректировка координаты Y для следующего пункта
         y=y-m_item_y_size+7;
         //--- Увеличение счётчика разделительных линий
         s++;
        }
     }
   return(true);
  }

Затем нужно имплементировать метод CContextMenu::CreateContextMenu(), который предназначен для внешнего использования. На данном этапе рассмотрим вариант, при котором контекстное меню должно быть обязательно привязано к пункту внешнего меню либо к независимому одиночному пункту. Поэтому еще до создания контекстного меню ему нужно передать указатель на предыдущий узел, как уже было сказано ранее. Кроме проверки на существование указателя на форму, будем делать также проверку существования указателя на предыдущий узел. Для пользователя библиотеки это будет дополнительным ориентиром, когда исключается неправильная последовательность формирования графического интерфейса. 

Обычно контекстное меню всегда скрывается после создания. Ведь оно предназначено для вызова — либо при нажатии на какой-либо другой элемент управления, либо при клике мышью на рабочей области. Чтобы скрывать объекты в каждом элементе, ранее уже был предусмотрен метод Hide(). В классе CContextMenu есть такой же метод. В нём сначала скрываются объекты контекстного меню, а именно — фон и разделительные линии. Затем в цикле скрываются все пункты меню. При этом для пунктов меню вызываются свои методы CMenuItem::Hide(). Разделительные линии тоже можно было бы скрывать подобным образом, ведь этот элемент имеет свой метод CSeparateLine::Hide(). Но, поскольку он является лишь элементом дизайна, состоит из одного графического объекта и не предназначен для взаимодействия с пользователем, то при создании был добавлен в общий массив объектов контекстного меню и будет скрываться в соответствующем цикле.

//+------------------------------------------------------------------+
//| Скрывает контекстное меню                                        |
//+------------------------------------------------------------------+
void CContextMenu::Hide(void)
  {
//--- Выйти, если элемент скрыт
   if(!CElement::m_is_visible)
      return;
//--- Скрыть объекты контекстного меню
   for(int i=0; i<ObjectsElementTotal(); i++)
      Object(i).Timeframes(OBJ_NO_PERIODS);
//--- Скрыть пункты меню
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
      m_items[i].Hide();
//--- Обнулить фокус
   CElement::MouseFocus(false);
//--- Присвоить статус скрытого элемента
   CElement::m_is_visible=false;
  }

Таким же образом будут устроены все методы, предназначенные для управления контекстным меню, поэтому не будем приводить здесь их код. Вы можете посмотреть его в приложенных к статье файлах. Здесь лишь рассмотрим код метода CContextMenu::Delete() для удаления элемента. В нём, кроме удаления всех графических объектов, освобождаются все массивы, которые использовались для формирования контекстного меню. Если этого не сделать, то каждый раз при смене символа или таймфрейма графика список пунктов меню будет увеличиваться. Когда мы перейдём к тестам, вы можете попробовать поэкспериментировать с этим, просто закомментировав эти строки.

//+------------------------------------------------------------------+
//| Удаление                                                         |
//+------------------------------------------------------------------+
void CContextMenu::Delete(void)
  {
//--- Удаление объектов  
   m_area.Delete();
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
      m_items[i].Delete();
//--- Удаление разделительных линий
   int sep_total=::ArraySize(m_sep_line);
   for(int i=0; i<sep_total; i++)
      m_sep_line[i].Delete();
//--- Освобождение массивов элемента
   ::ArrayFree(m_items);
   ::ArrayFree(m_sep_line);
   ::ArrayFree(m_sep_line_index);
   ::ArrayFree(m_label_text);
   ::ArrayFree(m_path_bmp_on);
   ::ArrayFree(m_path_bmp_off);
//--- Освобождение массива объектов
   CElement::FreeObjectsArray();
  }

Возвращаясь к методу создания контекстного меню, следует ещё упомянуть о том, что координаты будут устанавливаться относительно предыдущего узла. Сделаем так, чтобы пользователь библиотеки смог установить и свои координаты, если появится такая необходимость. По умолчанию, в методе создания контекстного меню CContextMenu::CreateContextMenu(), координаты будут нулевыми. Автоматически координаты будут рассчитываться от предыдущего узла в том случае, если не будет указано хотя бы одной координаты. Если обе координаты указаны, то автоматический расчёт отменяется.

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

//+------------------------------------------------------------------+
//| Перечисление сторон прикрепления меню                               |
//+------------------------------------------------------------------+
enum ENUM_FIX_CONTEXT_MENU
  {
   FIX_RIGHT  =0,
   FIX_BOTTOM =1
  };

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

class CContextMenu : public CElement
  {
private:
   //--- Сторона фиксации контекстного меню
   ENUM_FIX_CONTEXT_MENU m_fix_side;
   //---
public:
   //--- Установка режима фиксации контекстного меню
   void              FixSide(const ENUM_FIX_CONTEXT_MENU side)      { m_fix_side=side;                      }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CContextMenu::CContextMenu(void) : m_fix_side(FIX_RIGHT)
  {
  }

Ниже представлен код метода CContextMenu::CreateContextMenu(). Так как создание элемента возможно только при наличии указателя на него, то пройдя соответствующую проверку, о которой было сказано ранее, будут доступны свойства этого узла, что и позволит рассчитать относительные координаты автоматически. Скрытие контекстного меню в коде должно располагаться после его создания.

//+------------------------------------------------------------------+
//| Создаёт контекстное меню                                         |
//+------------------------------------------------------------------+
bool CContextMenu::CreateContextMenu(const long chart_id,const int subwin,const int x=0,const int y=0)
  {
//--- Выйти, если нет указателя на форму
   if(::CheckPointer(m_wnd)==POINTER_INVALID)
     {
      ::Print(__FUNCTION__," > Перед созданием выпадающего контекстного меню ему нужно передать "
              "объект окна с помощью метода WindowPointer(CWindow &object).");
      return(false);
     }
//--- Выйти, если нет указателя на предыдущий узел 
   if(::CheckPointer(m_prev_node)==POINTER_INVALID)
     {
      ::Print(__FUNCTION__," > Перед созданием контекстного меню ему нужно передать "
              "указатель на предыдущий узел с помощью метода CContextMenu::PrevNodePointer(CMenuItem &object).");
      return(false);
     }
//--- Инициализация переменных
   m_id       =m_wnd.LastId()+1;
   m_chart_id =chart_id;
   m_subwin   =subwin;
//--- Если координаты не указаны
   if(x==0 || y==0)
     {
      m_x =(m_fix_side==FIX_RIGHT)? m_prev_node.X2()-3 : m_prev_node.X()+1;
      m_y =(m_fix_side==FIX_RIGHT)? m_prev_node.Y()-1  : m_prev_node.Y2()-1;
     }
//--- Если координаты указаны
   else
     {
      m_x =x;
      m_y =y;
     } 
//--- Отступы от крайней точки
   CElement::XGap(m_x-m_wnd.X());
   CElement::YGap(m_y-m_wnd.Y());
//--- Создание контекстного меню
   if(!CreateArea())
      return(false);
   if(!CreateItems())
      return(false);
//--- Скрыть элемент
   Hide();
   return(true);
  }

В классе CMenuItem в методе CreateMenuItem() нужно также добавить проверку на наличие указателя на предыдущий узел, но с дополнительным условием. Если указателя нет, это будет означать, что предполагается независимый (то есть, не являющийся частью контекстного меню) пункт меню. Такими пунктами могут быть только пункты простого типа (MI_SIMPLE) либо пункты, которые содержат в себе контекстное меню (MI_HAS_CONTEXT_MENU). С этим сейчас могут возникнуть некоторые трудности в понимании, но всё станет предельно ясно, когда мы рассмотрим примеры в конце статьи.

Этот код поместите в метод CMenuItem::CreateMenuItem() сразу после проверки на наличие указателя на форму:

//--- Если указателя на предыдущий узел нет, то
//    предполагается независимый пункт меню, то есть такой, который не является частью контекстного меню
   if(::CheckPointer(m_prev_node)==POINTER_INVALID)
     {
      //--- Выйти, если установленный тип не соответствует
      if(m_type_menu_item!=MI_SIMPLE && m_type_menu_item!=MI_HAS_CONTEXT_MENU)
        {
         ::Print(__FUNCTION__," > Тип независимого пункта меню может быть только MI_SIMPLE или MI_HAS_CONTEXT_MENU, ",
                 "то есть, с наличием контекстного меню.\n",
                 __FUNCTION__," > Установить тип пункта меню можно с помощью метода CMenuItem::TypeMenuItem()");
         return(false);
        }
     }



Тест установки контекстного меню

Сейчас можно протестировать установку контекстного меню на график. Подключаем файл ContextMenu.mqh с классом контекстного меню CContextMenu к библиотеке:

//+------------------------------------------------------------------+
//|                                                 WndContainer.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Window.mqh"
#include "MenuItem.mqh"
#include "ContextMenu.mqh"
#include "SeparateLine.mqh"

Создаем экземпляр класса CContextMenu в пользовательском классе приложения CProgram и объявляем метод для создания контекстного меню. Отступы от крайней точки формы для него указывать не нужно, так как они будут рассчитываться относительно пункта меню, к которому будут присоединены.

class CProgram : public CWndEvents
  {
private:
   //--- Пункт меню и контекстное меню
   CMenuItem         m_menu_item1;
   CContextMenu      m_mi1_contextmenu1;
   //---
private:
#define MENU_ITEM1_GAP_X (6)
#define MENU_ITEM1_GAP_Y (25)
   bool              CreateMenuItem1(const string item_text);
   bool              CreateMI1ContextMenu1(void);
  };

Теперь сформируем контекстное меню. Пусть в нём будет пять пунктов: три простых (MI_SIMPLE) и два пункта с типом «чекбокс» (MI_CHECKBOX). Для простых пунктов подключим ресурсы с картинками для их ярлыков (вне тела метода). Для доступного пункта картинка будет цветная, а для заблокированного — обесцвеченная. Вы можете скачать их по ссылкам в конце статьи. Затем, в самом начале метода, в контекстное меню нужно сохранить указатель на форму и на предыдущий узел. Помним, что без этих действий не получится создать графический интерфейс, и программа будет выгружена с графика. Далее идут массивы с (1) описанием пунктов (отображаемый текст), с картинками (2) доступных и (3) заблокированных состояний, а также с (4) типом пунктов. После этого нужно указать общие свойства для всех пунктов, и затем в цикле с помощью метода CContextMenu::AddItem() добавить их в класс контекстного меню. Добавим разделительную линию после второго пункта (индекс 1). После этого, наконец, можно установить контекстное меню на график. В самом конце метода нужно добавить указатель на элемент в базу. Код метода можно подробнее изучить ниже:

//+------------------------------------------------------------------+
//| Создаёт контекстное меню                                         |
//+------------------------------------------------------------------+
#resource "\\Images\\Controls\\coins.bmp"
#resource "\\Images\\Controls\\coins_colorless.bmp"
#resource "\\Images\\Controls\\line_chart.bmp"
#resource "\\Images\\Controls\\line_chart_colorless.bmp"
#resource "\\Images\\Controls\\safe.bmp"
#resource "\\Images\\Controls\\safe_colorless.bmp"
//---
bool CProgram::CreateMI1ContextMenu1(void)
  {
//--- Пять пунктов в контекстном меню
#define CONTEXTMENU_ITEMS1 5
//--- Сохраним указатель на окно
   m_mi1_contextmenu1.WindowPointer(m_window);
//--- Сохраним указатель на предыдущий узел
   m_mi1_contextmenu1.PrevNodePointer(m_menu_item1);
//--- Массив названий пунктов
   string items_text[CONTEXTMENU_ITEMS1]=
     {
      "ContextMenu 1 Item 1",
      "ContextMenu 1 Item 2",
      "ContextMenu 1 Item 3",
      "ContextMenu 1 Item 4",
      "ContextMenu 1 Item 5"
     };
//--- Массив ярлыков для доступного режима
   string items_bmp_on[CONTEXTMENU_ITEMS1]=
     {
      "Images\\Controls\\coins.bmp",
      "Images\\Controls\\line_chart.bmp",
      "Images\\Controls\\safe.bmp",
      "",""
     };
//--- Массив ярлыков для заблокированного режима
   string items_bmp_off[CONTEXTMENU_ITEMS1]=
     {
      "Images\\Controls\\coins_colorless.bmp",
      "Images\\Controls\\line_chart_colorless.bmp",
      "Images\\Controls\\safe_colorless.bmp",
      "",""
     };
//--- Массив типов пунктов
   ENUM_TYPE_MENU_ITEM items_type[CONTEXTMENU_ITEMS1]=
     {
      MI_SIMPLE,
      MI_SIMPLE,
      MI_SIMPLE,
      MI_CHECKBOX,
      MI_CHECKBOX
     };
//--- Установим свойства перед созданием
   m_mi1_contextmenu1.XSize(160);
   m_mi1_contextmenu1.ItemYSize(24);
   m_mi1_contextmenu1.AreaBackColor(C'240,240,240');
   m_mi1_contextmenu1.AreaBorderColor(clrSilver);
   m_mi1_contextmenu1.ItemBackColorHover(C'240,240,240');
   m_mi1_contextmenu1.ItemBackColorHoverOff(clrLightGray);
   m_mi1_contextmenu1.ItemBorderColor(C'240,240,240');
   m_mi1_contextmenu1.LabelColor(clrBlack);
   m_mi1_contextmenu1.LabelColorHover(clrWhite);
   m_mi1_contextmenu1.RightArrowFileOff("Images\\EasyAndFastGUI\\Controls\\RightTransp_black.bmp");
   m_mi1_contextmenu1.SeparateLineDarkColor(C'160,160,160');
   m_mi1_contextmenu1.SeparateLineLightColor(clrWhite);
//--- Добавить пункты в контекстное меню
   for(int i=0; i<CONTEXTMENU_ITEMS1; i++)
      m_mi1_contextmenu1.AddItem(items_text[i],items_bmp_on[i],items_bmp_off[i],items_type[i]);
//--- Разделительная линия после второго пункта
   m_mi1_contextmenu1.AddSeparateLine(1);
//--- Создать контекстное меню
   if(!m_mi1_contextmenu1.CreateContextMenu(m_chart_id,m_subwin))
      return(false);
//--- Добавим указатель на элемент в базу
   CWndContainer::AddToElementsArray(0,m_mi1_contextmenu1);
   return(true);
  }

Теперь добавим вызов метода создания контекстного меню в основной метод создания графического интерфейса. Так как контекстное меню при установке по правилам будет скрываться, то для текущего теста нужно принудительно показать его. Ниже показано, какие строки нужно добавить в метод CProgram::CreateTradePanel().

//+------------------------------------------------------------------+
//| Создаёт торговую панель                                          |
//+------------------------------------------------------------------+
bool CProgram::CreateTradePanel(void)
  {
//--- Создание формы для элементов управления
   if(!CreateWindow("EXPERT PANEL"))
      return(false);
//--- Создание элементов управления:
//    Пункт меню
   if(!CreateMenuItem1("Menu item"))
      return(false);
   if(!CreateMI1ContextMenu1())
      return(false);
//--- Разделительная линия
   if(!CreateSepLine())
      return(false);
//--- Показать контекстное меню
   m_mi1_contextmenu1.Show();
//--- Перерисовка графика
   m_chart.Redraw();
   return(true);
  }

Скомпилируйте файлы и загрузите программу на график. Должен получиться результат, как на скриншоте ниже:

Рис. 4. Тест элемента «контекстное меню».

Рис. 4. Тест элемента «контекстное меню».


На данном этапе при наведении курсора мыши на пункты в контекстном меню они не изменяют свой цвет. Этот функционал можно сделать в классе контекстного меню CContextMenu, а можно воспользоваться уже готовым в классе пункта меню CMenuItem. После установки контекстного меню на график его указатель добавляется в базу, но указатели на каждый пункт меню в нём остаются недоступны для использования в классе обработки событий CWndEvents при текущей реализации добавления указателей в общий массив элементов. Для каждого сложного (составного) элемента управления, который состоит из нескольких других элементов, в классе CWndContainer мы будем создавать метод, в котором можно получить указатели на эти элементы. Именно для этого ранее в классе CContextMenu был реализован метод ItemPointerByIndex(), с помощью которого можно получить указатель на пункт меню, указав его индекс.

 


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

Далее в классе CWndContainer реализуем метод AddContextMenuElements() для работы с элементом «контекстное меню». В качестве параметров в него нужно передавать индекс формы и объект элемента. В самом начале метода будет производиться проверка, является ли переданный элемент контекстным меню. Если да, то далее понадобится указатель на контекстное меню (CContextMenu), чтобы получить доступ к его методам. Но как это сделать, если передаётся объект базового класса (CElement)? Для этого достаточно объявить указатель с типом CContextMenu и присвоить ему указатель переданного объекта (в листинге кода ниже это отмечено жёлтым маркером). Таким образом открывается доступ к пунктам контекстного меню, и далее в цикле они поочерёдно добавляются в общий массив элементов своей формы. В конце каждой итерации пункты меню передаются в метод CWndContainer::AddToObjectsArray() для сохранения указателей на объекты, из которых они построены, в массиве объектов типа CChartObject.

//+------------------------------------------------------------------+
//| Класс для хранения всех объектов интерфейса                      |
//+------------------------------------------------------------------+
class CWndContainer
  {
protected:
   //--- Сохраняет указатели на элементы контекстного меню в базу
   bool              AddContextMenuElements(const int window_index,CElement &object);
  };
//+------------------------------------------------------------------+
//| Сохраняет указатели на объекты контекстного меню в базу          |
//+------------------------------------------------------------------+
bool CWndContainer::AddContextMenuElements(const int window_index,CElement &object)
  {
//--- Выйдем, если это не контекстное меню
   if(object.ClassName()!="CContextMenu")
      return(false);
//--- Получим указатель на контекстное меню
   CContextMenu *cm=::GetPointer(object);
//--- Сохраним указатели на его объекты в базе
   int items_total=cm.ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Увеличение массива элементов
      int size=::ArraySize(m_wnd[window_index].m_elements);
      ::ArrayResize(m_wnd[window_index].m_elements,size+1);
      //--- Получение указателя на пункт меню
      CMenuItem *mi=cm.ItemPointerByIndex(i);
      //--- Сохраняем указатель в массив
      m_wnd[window_index].m_elements[size]=mi;
      //--- Добавляем указатели на все объекты пункта меню в общий массив
      AddToObjectsArray(window_index,mi);
     }
//---
   return(true);
  }

Вызываться он будет в методе CWndContainer::AddToElementsArray() сразу после увеличения счётчика элементов. Код в листинге ниже сокращён для экономии места в статье. Полную версию можно посмотреть в приложенных к статье файлах.

//+------------------------------------------------------------------+
//| Добавляет указатель в массив элементов                           |
//+------------------------------------------------------------------+
void CWndContainer::AddToElementsArray(const int window_index,CElement &object)
  {
//--- Если в базе нет форм для элементов управления
//--- Если запрос на несуществующую форму
//--- Добавим в общий массив элементов
//--- Добавим объекты элемента в общий массив объектов
//--- Запомним во всех формах id последнего элемента
//--- Увеличим счётчик идентификаторов элементов

//--- Сохраняет указатели на объекты контекстного меню в базу
   if(AddContextMenuElements(window_index,object))
      return;
  }

Точно также в дальнейшем будем дополнять код метода CWndContainer::AddToElementsArray() другими подобными методами для сложных (составных) элементов.

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

Рис. 5. Тест пунктов в контекстном меню.

Рис. 5. Тест пунктов в контекстном меню.


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

 


Заключение

На текущий момент в разрабатываемой библиотеке есть уже три класса для создания таких элементов, как:

  • пункт меню;
  • разделительная линия;
  • контекстное меню.

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

Ниже вы можете загрузить на свой компьютер архивы с файлами библиотеки на текущей стадии разработки, изображения и файлы рассматриваемых в статье программ (эксперта, индикаторов и скрипта) для тестов в терминалах Metatrader 4 и Metatrader 5. Если у вас возникают вопросы по использованию материала предоставленного в этих файлах, вы можете обратиться к подробному описанию процесса разработки библиотеки в одной из статей в представленном списке ниже или задать вопрос в комментариях к статье. 

Список статей (глав) второй части:

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
Igor Volodin
Igor Volodin | 24 февр. 2016 в 15:30
А предусматривается использование механизма привязки/делегирования действий/команд к разным контролам? Например к кнопке в тулбаре и пункте контекстного меню? Для этого подошел бы паттерн "команда". Заодно, можно реализовывать механизм Undo/Redo через хранение в истории выполненных команд сохраняя исходные значения получателя.

Также неплохо ввести понятие модели, которую слушает тот или иной контрол (паттерн наблюдатель). Это позволит прозрачно реагировать всем котролам в интерфейсе на изменения в модели.

Например, пока у модели чарта пользователь не проинициализировал все параметры, все контролы связанные с управлением чарта имеют disable вид. Это избавляет от обращений непосредственно к контролам и логики отвечающей за их поведение в тех или иных случаях.
Anatoli Kazharski
Anatoli Kazharski | 24 февр. 2016 в 18:50
Igor Volodin:
А предусматривается использование механизма привязки/делегирования действий/команд к разным контролам? Например к кнопке в тулбаре и пункте контекстного меню? Для этого подошел бы паттерн "команда". Заодно, можно реализовывать механизм Undo/Redo через хранение в истории выполненных команд сохраняя исходные значения получателя.

Также неплохо ввести понятие модели, которую слушает тот или иной контрол (паттерн наблюдатель). Это позволит прозрачно реагировать всем котролам в интерфейсе на изменения в модели.

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

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

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

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

Dmitriy Konogorov
Dmitriy Konogorov | 18 дек. 2021 в 12:58
А зачем массив переменных ?
color             m_area_color_array[];
И в конструкторе CContextMenu() нужно переменную m_item_Ysize задать размер большим нуля по умолчанию.
Метод площадей Метод площадей
Торговая система "Метод площадей" работает на необычной интерпретации показаний осциллятора RSI. В настоящей статье приводится индикатор, который визуализирует метод площадей, и советник, торгующий по этой системе. Статья дополнена подробными результатами тестирования советника для различных символов, таймфреймов и значений площади.
Как быстро добавить панель управления к индикатору и советнику Как быстро добавить панель управления к индикатору и советнику
Вы хотите добавить к своему индикатору или советнику графическую панельку для удобного и быстрого управления, но не знаете, как это сделать? В этой статье шаг за шагом я покажу как "прикрутить" панель диалога со входными параметрами к вашей MQL4/MQL5-программе.
Графические интерфейсы II: Настройка обработчиков событий библиотеки (Глава 3) Графические интерфейсы II: Настройка обработчиков событий библиотеки (Глава 3)
В предыдущих статьях были реализованы классы для создания всех составных частей главного меню. Теперь же настало время познакомиться с обработчиками событий в главных базовых классах и в классах созданных элементов управления. Отдельное внимание уделено управлению состоянием графика в зависимости от того, где находится курсор мыши.
Создание ручных торговых стратегий с использованием нечеткой логики Создание ручных торговых стратегий с использованием нечеткой логики
В статье рассматривается возможность улучшения ручных торговых стратегий с помощью теории нечетких множеств. В качестве примера пошагово описан поиск стратегии и подбор ее параметров, а затем — применение нечеткой логики для размытия слишком формальных критериев входа в рынок. Таким образом, после модификации стратегии мы получаем гибкие условия открытия позиции, более оптимально реагирующие на рыночную ситуацию.