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

Графические интерфейсы II: Элемент "Главное меню" (Глава 4)

MetaTrader 5Примеры | 7 марта 2016, 14:53
4 633 0
Anatoli Kazharski
Anatoli Kazharski

Содержание

 


Введение

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

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

 


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

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

  • CMenuItem – пункт-меню.
  • CSeparateLine – разделительная линия.
  • CContextMenu – контекстное меню.

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

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

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

Рис. 1. Основные части главного меню.

Рис. 1. Основные части главного меню.


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

//+------------------------------------------------------------------+
//| Класс для создания главного меню                                 |
//+------------------------------------------------------------------+
class CMenuBar : public CElement
  {
private:
   //--- Указатель на форму, к которой элемент присоединён
   CWindow          *m_wnd;
   //--- Объекты для создания пункта меню
   CRectLabel        m_area;
   CMenuItem         m_items[];
   //--- Массив указателей на контекстные меню
   CContextMenu     *m_contextmenus[];
   //---
public:
                     CMenuBar(void);
                    ~CMenuBar(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      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);
   //---
  };

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

  • свойства фона главного меню;
  • общие свойства пунктов.

Также понадобятся методы для установки и получения текущего состояния главного меню. Все поля нужно инициализировать в конструкторе значениями по умолчанию. Высота фона главного меню по умолчанию будет 22 пикселя. Высота пунктов меню будет рассчитываться автоматически относительно высоты фона главного меню. Но если нужно, то вы можете изменить это значение перед тем, как установить элемент на график, воспользовавшись методом базового класса CElement::YSize(). Ширина фона главного меню обычно равна ширине формы, к которой оно присоединяется. Поэтому расчёт значения этого параметра элемента тоже автоматизируем при установке фона на график.

class CMenuBar : public CElement
  {
private:
   //--- Свойства фона
   int               m_area_zorder;
   color             m_area_color;
   color             m_area_color_hover;
   color             m_area_border_color;
   //--- Общие свойства пунктов меню
   int               m_item_y_size;
   color             m_item_color;
   color             m_item_color_hover;
   color             m_item_border_color;
   int               m_label_x_gap;
   int               m_label_y_gap;
   color             m_label_color;
   color             m_label_color_hover;
   //--- Состояние главного меню
   bool              m_menubar_state;
   //---
public:
   //--- Цвет (1) фона и (2) рамки фона главного меню
   void              MenuBackColor(const color clr)       { m_area_color=clr;                    }
   void              MenuBorderColor(const color clr)     { m_area_border_color=clr;             }
   //--- (1) цвет фона, (2) цвет фона при наведении курсора и (3) цвет рамки пунктов главного меню
   void              ItemBackColor(const color clr)       { m_item_color=clr;                    }
   void              ItemBackColorHover(const color clr)  { m_item_color_hover=clr;              }
   void              ItemBorderColor(const color clr)     { m_item_border_color=clr;             }
   //--- Отступы текстовой метки от крайней точки фона пункта
   void              LabelXGap(const int x_gap)           { m_label_x_gap=x_gap;                 }
   void              LabelYGap(const int y_gap)           { m_label_y_gap=y_gap;                 }
   //--- Цвет текста (1) обычный и (2) в фокусе
   void              LabelColor(const color clr)          { m_label_color=clr;                   }
   void              LabelColorHover(const color clr)     { m_label_color_hover=clr;             }
   //--- Состояние главного меню
   void              State(const bool state);
   bool              State(void)                    const { return(m_menubar_state);             }
   //---
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CMenuBar::CMenuBar(void) : m_menubar_state(false),
                           m_area_zorder(0),
                           m_area_color(C'240,240,240'),
                           m_area_border_color(clrSilver),
                           m_item_color(C'240,240,240'),
                           m_item_color_hover(C'51,153,255'),
                           m_item_border_color(C'240,240,240'),
                           m_label_x_gap(15),
                           m_label_y_gap(3),
                           m_label_color(clrBlack),
                           m_label_color_hover(clrWhite)
  {
//--- Сохраним имя класса элемента в базовом классе
   CElement::ClassName(CLASS_NAME);
//--- Высота главного меню по умолчанию
   m_y_size=22;
  }
//+------------------------------------------------------------------+
//| Установка состояния главного меню                                |
//+------------------------------------------------------------------+
void CMenuBar::State(const bool state)
  {
   if(state)
      m_menubar_state=true;
   else
     {
      m_menubar_state=false;
      //--- Пройтись по всем пунктам главного меню для
      //    установки статуса отключенных контекстных меню
      int items_total=ItemsTotal();
      for(int i=0; i<items_total; i++)
         m_items[i].ContextMenuState(false);
      //--- Разблокировать форму
      m_wnd.IsLocked(false);
     }
  }

Для установки уникальных свойств для каждого пункта понадобятся массивы. К уникальным свойствам относятся:

  • ширина пункта;
  • отображаемый текст.

Эти свойства будут устанавливаться до установки главного меню на график, то есть, в процессе его формирования в момент добавления каждого пункта. Для этого будет использоваться метод CMenuBar::AddItem(), подобный тому, который был создан ранее в классе CContextMenu. Отличие между ними — только в передаваемых (устанавливаемых) параметрах. 

Для привязки контекстных меню к каждому пункту главного меню создадим метод CMenuBar::AddContextMenuPointer(). В него нужно передавать индекс пункта главного меню и объект контекстного меню, указатель которого будет сохраняться в массиве m_contextmenus[].

class CMenuBar : public CElement
  {
private:
   //--- Массивы уникальных свойств пунктов меню:
   //    (1) Ширина, (2) текст
   int               m_width[];
   string            m_label_text[];
   //---
public:
   //--- Добавляет пункт меню с указанными свойствами до создания главного меню
   void              AddItem(const int width,const string text);
   //--- Присоединяет переданное контекстное меню к указанному пункту главного меню
   void              AddContextMenuPointer(const int index,CContextMenu &object);
   //---
  };
//+------------------------------------------------------------------+
//| Добавляет пункт меню                                             |
//+------------------------------------------------------------------+
void CMenuBar::AddItem(const int width,const string text)
  {
//--- Увеличим размер массивов на один элемент  
   int array_size=::ArraySize(m_items);
   ::ArrayResize(m_items,array_size+1);
   ::ArrayResize(m_contextmenus,array_size+1);
   ::ArrayResize(m_width,array_size+1);
   ::ArrayResize(m_label_text,array_size+1);
//--- Сохраним значения переданных параметров
   m_width[array_size]      =width;
   m_label_text[array_size] =text;
  }
//+------------------------------------------------------------------+
//| Добавляет указатель контекстного меню                            |
//+------------------------------------------------------------------+
void CMenuBar::AddContextMenuPointer(const int index,CContextMenu &object)
  {
//--- Проверка на выход из диапазона
   int size=::ArraySize(m_contextmenus);
   if(size<1 || index<0 || index>=size)
      return;
//--- Сохранить указатель
   m_contextmenus[index]=::GetPointer(object);
  }

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

class CMenuBar : public CElement
  {
public:
   //--- (1) Получение указателя указанного пункта меню, (2) получение указателя указанного контекстного меню
   CMenuItem        *ItemPointerByIndex(const int index);
   CContextMenu     *ContextMenuPointerByIndex(const int index);

   //--- Количество (1) пунктов и (2) контекстных меню
   int               ItemsTotal(void)               const { return(::ArraySize(m_items));        }
   int               ContextMenusTotal(void)        const { return(::ArraySize(m_contextmenus)); }
   //---
  };
//+------------------------------------------------------------------+
//| Возвращает указатель пункта меню по индексу                      |
//+------------------------------------------------------------------+
CMenuItem *CMenuBar::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]));
  }
//+------------------------------------------------------------------+
//| Возвращает указатель контекстного меню по индексу                |
//+------------------------------------------------------------------+
CContextMenu *CMenuBar::ContextMenuPointerByIndex(const int index)
  {
   int array_size=::ArraySize(m_contextmenus);
//--- Если нет ни одного пункта в главном меню, сообщить об этом
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > Вызов этого метода нужно осуществлять, "
      "когда в главном меню есть хотя бы один пункт!");
     }
//--- Корректировка в случае выхода из диапазона
   int i=(index>=array_size)? array_size-1 : (index<0)? 0 : index;
//--- Вернуть указатель
   return(::GetPointer(m_contextmenus[i]));
  }

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

Ранее для того, чтобы пункты контекстного меню попали в базу всех элементов в классе CWndContainer, был написан специальный метод AddContextMenuElements(), который вызывается в главном методе добавления элементов в базу CWndContainer::AddToElementsArray(). Такой же метод нужно написать и для элемента «главное меню», иначе пункты меню не будут перемещаться вместе с формой, а также не будут изменять свой цвет при наведении курсора мыши. 

Краткий список действий, когда нужно, чтобы составные элементы от какого-либо главного элемента тоже попали в базу, а указатель главного элемента попал в персональный массив, выглядит так:

  • Подключить файл с классом элемента к файлу WndContainer.mqh.
  • Добавить массив элемента в структуру WindowElements.
  • Добавить метод для получения количества главных меню в персональном массиве.
  • Объявить и имплементировать приватный метод для сохранения указателей на элементы, которые являются частью другого (главного) элемента.
  • Разместить вызов нового метода в общем методе, который предназначен для использования в классе приложения в качестве добавления указателя главного элемента в базу.

Ниже показан сокращённый листинг кода из файла WndContainer.mqh, в котором показано только то, что нужно добавить в него:

//+------------------------------------------------------------------+
//|                                                 WndContainer.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "MenuBar.mqh"
//+------------------------------------------------------------------+
//| Класс для хранения всех объектов интерфейса                      |
//+------------------------------------------------------------------+
class CWndContainer
  {
protected:
   //--- Структура массивов элементов
   struct WindowElements
     {
      //--- Массив главных меню
      CMenuBar         *m_menu_bars[];
     };
   //---
public:
   //--- Количество главных меню
   int               MenuBarsTotal(const int window_index);
   //---
private:
   //--- Сохраняет указатели на элементы главного меню в базу
   bool              AddMenuBarElements(const int window_index,CElement &object);
  };
//+------------------------------------------------------------------+
//| Возвращает кол-во главных меню по указанному индексу окна        |
//+------------------------------------------------------------------+
int CWndContainer::MenuBarsTotal(const int window_index)
  {
   if(window_index>=::ArraySize(m_wnd))
     {
      ::Print(PREVENTING_OUT_OF_RANGE);
      return(WRONG_VALUE);
     }
//---
   return(::ArraySize(m_wnd[window_index].m_menu_bars));
  }
//+------------------------------------------------------------------+
//| Добавляет указатель в массив элементов                           |
//+------------------------------------------------------------------+
void CWndContainer::AddToElementsArray(const int window_index,CElement &object)
  {
//--- Если в базе нет форм для элементов управления
//--- Если запрос на несуществующую форму
//--- Добавим в общий массив элементов
//--- Добавим объекты элемента в общий массив объектов
//--- Запомним во всех формах id последнего элемента
//--- Увеличим счётчик идентификаторов элементов
//--- Сохраняет указатели на объекты контекстного меню в базу
//--- Сохраняет указатели на объекты главного меню в базу
   if(AddMenuBarElements(window_index,object))
      return;
  }
//+------------------------------------------------------------------+
//| Сохраняет указатели на объекты главного меню в базу              |
//+------------------------------------------------------------------+
bool CWndContainer::AddMenuBarElements(const int window_index,CElement &object)
  {
//--- Выйдем, если это не главное меню
   if(object.ClassName()!="CMenuBar")
      return(false);
//--- Получим указатель на главное меню
   CMenuBar *mb=::GetPointer(object);
//--- Сохраним указатели на его объекты в базе
   int items_total=mb.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=mb.ItemPointerByIndex(i);
      //--- Сохраняем указатель в массив
      m_wnd[window_index].m_elements[size]=mi;
      //--- Добавляем указатели на все объекты пункта меню в общий массив
      AddToObjectsArray(window_index,mi);
     }
//--- Добавим указатель в персональный массив
   AddToRefArray(mb,m_wnd[window_index].m_menu_bars);
   return(true);
  }

 

 

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

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

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

class CProgram : public CWndEvents
  {
private:
   //--- Главное меню
   CMenuBar          m_menubar;
   //---
private:
   //---
#define MENUBAR_GAP_X    (1)
#define MENUBAR_GAP_Y    (20)
   bool              CreateMenuBar(void);
   //---
#define MENU_ITEM1_GAP_X (6)
#define MENU_ITEM1_GAP_Y (45)
   //---
#define SEP_LINE_GAP_X   (6)
#define SEP_LINE_GAP_Y   (75)
   //---
  };
//+------------------------------------------------------------------+
//| Создаёт торговую панель                                          |
//+------------------------------------------------------------------+
bool CProgram::CreateTradePanel(void)
  {
//--- Создание формы для элементов управления
//--- Создание элементов управления:
//    Главное меню
   if(!CreateMenuBar())
      return(false);
//--- Пункт меню
//--- Разделительная линия
//--- Перерисовка графика
   return(true);
  }
//+------------------------------------------------------------------+
//| Создаёт главное меню                                             |
//+------------------------------------------------------------------+
bool CProgram::CreateMenuBar(void)
  {
//--- Три пункта в главном меню
#define MENUBAR_TOTAL 3
//--- Сохраним указатель на окно
   m_menubar.WindowPointer(m_window);
//--- Координаты
   int x=m_window.X()+MENUBAR_GAP_X;
   int y=m_window.Y()+MENUBAR_GAP_Y;
//--- Массивы с уникальными свойствами пунктов
   int    width[MENUBAR_TOTAL] ={50,55,53};
   string text[MENUBAR_TOTAL]  ={"File","View","Help"};
//--- Добавить пункты в главное меню
   for(int i=0; i<MENUBAR_TOTAL; i++)
      m_menubar.AddItem(width[i],text[i]);
//--- Создадим элемент управления
   if(!m_menubar.CreateMenuBar(m_chart_id,m_subwin,x,y))
      return(false);
//--- Добавим объект в общий массив групп объектов
   CWndContainer::AddToElementsArray(0,m_menubar);
   return(true);
  }

Скомпилируйте файлы и загрузите эксперта на график. Результат должен быть таким, как показано на скриншоте ниже. Главное меню будет перемещаться вместе с формой, а его пункты — изменять свой цвет при наведении курсора мыши.

Рис. 2. Тест элемента «Главное меню».

Рис. 2. Тест элемента «Главное меню».

 

 


Блокировка неактивных элементов управления

Прежде чем создать контекстные меню и прикрепить их к пунктам главного меню, нужно внедрить в библиотеку функционал, который будет блокировать форму и остальные элементы, когда какой-либо элемент активирован. Почему это нужно сделать? Под активированными элементами здесь имеются в виду те, которые делаются видимыми (вызываются), посредством других элементов и скрываются, когда больше не нужны. Например, к ним относятся выпадающие списки, контекстные меню, календари и т.д. Попробуйте активировать, какое-нибудь контекстное меню или выпадающий список в торговом терминале MetaTrader. Обратите внимание: когда элемент такого типа активирован, то все остальные элементы управления во всём терминале становятся недоступными. Это выражается, например, в том, что при наведении на них курсора мыши они не изменяют свой цвет. Цель такой блокировки — исключение ситуации, когда на курсор реагирует элемент, который в данный момент временно закрыт выпадающим списком.

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

Добавим в класс для создания формы CWindow специальное поле и методы для установки и получения состояния (заблокировано/разблокировано) формы (см. код ниже). Поле m_is_locked в конструкторе нужно инициализировать значением false — то есть, по умолчанию форма должна быть разблокирована. Сразу можно добавить условие, по которому изменение цвета формы и её элементов должно осуществляться только тогда, когда она не заблокирована.

class CWindow : public CElement
  {
private:
   //--- Статус заблокированного окна
   bool              m_is_locked;
   //---
public:
   //--- Статус заблокированного окна
   bool              IsLocked(void)                                    const { return(m_is_locked);                }
   void              IsLocked(const bool flag)                               { m_is_locked=flag;                   }
   //---
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CWindow::CWindow(void) : m_is_locked(false)
  {
  }
//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void CWindow::OnEventTimer(void)
  {
//--- Если окно не заблокировано
   if(!m_is_locked)
     {
      //--- Изменение цвета объектов формы
      ChangeObjectsColor();
     }
  }

Что касается других элементов управления, то на текущий момент у нас есть только один кликабельный элемент – пункт меню, на котором можно протестировать этот функционал. Изменение цвета пункта при наведении курсора мыши будет теперь зависеть и от состояния формы, к которой он присоединён. Поэтому такую же проверку нужно разместить и в его классе CMenuItem, как это показано в листинге кода ниже:

//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void CMenuItem::OnEventTimer(void)
  {
//--- Если окно доступно
   if(!m_wnd.IsLocked())
     {
      //--- Если статус отключенного контекстного меню
      if(!m_context_menu_state)
         //--- Изменение цвета объектов формы
         ChangeObjectsColor();
     }
  }

Блокировать форму нужно в тот момент, когда контекстное меню делается видимым. То есть, в методе CContextMenu::Show().

//+------------------------------------------------------------------+
//| Показывает контекстное меню                                      |
//+------------------------------------------------------------------+
void CContextMenu::Show(void)
  {
//--- Выйти, если элемент уже видим
//--- Показать объекты контекстного меню
//--- Показать пункты меню
//--- Присвоить статус видимого элемента
//--- Состояние контекстного меню
//--- Отметить состояние в предыдущем узле
//--- Заблокируем форму
   m_wnd.IsLocked(true);
  }

На первый взгляд, кажется, что для разблокировки достаточно было бы вызывать метод CWindow::IsLocked() в методе CContextMenu::Hide(). Но этот вариант не подходит, потому что одновременно может быть открыто несколько контекстных меню. Не все они закрываются одновременно. Вспомним, в каких случаях открытые контекстные меню закрываются все вместе. Для этого должны исполниться некоторые условия — к примеру, в методе CContextMenu::CheckHideContextMenus(), когда после проверки всех условий отправляется сигнал на закрытие всех контекстных меню. Второй случай — в методе CContextMenu::ReceiveMessageFromMenuItem(), когда обрабатывается событие ON_CLICK_MENU_ITEM

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

//+------------------------------------------------------------------+
//| Проверка условий на закрытие всех контекстных меню               |
//+------------------------------------------------------------------+
void CContextMenu::CheckHideContextMenus(void)
  {
//--- Выйти, если курсор в области контекстного меню или в области предыдущего узла
//--- Если же курсор вне области этих элементов, то ...
//    ... нужно проверить, есть ли открытые контекстные меню, которые были активированы после этого
//--- Для этого пройдёмся в цикле по списку этого контекстного меню ...
//    ... для определения наличия пункта, который содержит в себе контекстное меню
//--- Разблокируем форму
   m_wnd.IsLocked(false);
//--- Послать сигнал на скрытие всех контекстных меню
  }
//+------------------------------------------------------------------+
//| Приём сообщения от пункта меню для обработки                     |
//+------------------------------------------------------------------+
void CContextMenu::ReceiveMessageFromMenuItem(const int id_item,const int index_item,const string message_item)
  {
//--- Если есть признак сообщения этой программы и id элемента совпадает
//--- Скрытие контекстного меню
//--- Разблокируем форму
   m_wnd.IsLocked(false);
  }

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

class CContextMenu : public CElement
  {
public:
   //--- Изменяет цвет пунктов меню при наведении курсора
   void              ChangeObjectsColor(void);
  };
//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void CContextMenu::OnEventTimer(void)
  {
//--- Изменение цвета пунктов меню при наведении курсора
   ChangeObjectsColor();
  }
//+------------------------------------------------------------------+
//| Изменение цвета объекта при наведении курсора                    |
//+------------------------------------------------------------------+
void CContextMenu::ChangeObjectsColor(void)
  {
//--- Выйти, если контекстное меню отключено
   if(!m_context_menu_state)
      return;
//--- Пройтись по всем пунктам меню
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Изменить цвет пункта меню
      m_items[i].ChangeObjectsColor();
     }
  }

Теперь всё будет работать так, как задумывалось:

Рис. 3. Тест блокировки формы и всех элементов управления, кроме активизированного в текущий момент.

Рис. 3. Тест блокировки формы и всех элементов управления, кроме активизированного в текущий момент.

 

 


Методы для взаимодействия с главным меню

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

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

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

//+------------------------------------------------------------------+
//| Создаёт контекстное меню                                         |
//+------------------------------------------------------------------+
bool CProgram::CreateMBContextMenu1(void)
  {
//--- Три пункта в контекстном меню
//--- Сохраним указатель на окно
   m_mb_contextmenu1.WindowPointer(m_window);
//--- Сохраним указатель на предыдущий узел
   m_mb_contextmenu1.PrevNodePointer(m_menubar.ItemPointerByIndex(0));
//--- Прикрепить контекстное меню к указанному пункту меню
   m_menubar.AddContextMenuPointer(0,m_mb_contextmenu1);
//--- Массив названий пунктов
//--- Массив ярлыков для доступного режима
//--- Массив ярлыков для заблокированного режима
//--- Массив типов пунктов
//--- Установим свойства перед созданием
   m_mb_contextmenu1.FixSide(FIX_BOTTOM);
//--- Добавить пункты в контекстное меню
//--- Разделительная линия после второго пункта
//--- Деактивировать второй пункт
//--- Создать контекстное меню
   if(!m_mb_contextmenu1.CreateContextMenu(m_chart_id,m_subwin))
      return(false);
//--- Добавим объект в общий массив групп объектов
   CWndContainer::AddToElementsArray(0,m_mb_contextmenu1);
   return(true);
  }

Далее настроим обработчики событий в классе главного меню СMenuBar. Начнём с обработки нажатия на пунктах меню. Для этого, как в классах CMenuItem и CContextMenu, понадобится метод OnClickMenuItem(), а также специальные методы для извлечения из имени нажатого объекта индекса и идентификатора пункта меню. 

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

class CMenuBar : public CElement
  {
private:
   //--- Обработка нажатия на пункте меню
   bool              OnClickMenuItem(const string clicked_object);
   //--- Получение (1) идентификатора и (2) индекса из имени пункта меню
   int               IdFromObjectName(const string object_name);
   int               IndexFromObjectName(const string object_name);
   //---
  };
//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CMenuBar::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Обработка события нажатия левой кнопки мыши на пункте главного меню
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      if(OnClickMenuItem(sparam))
         return;
     }
  }
//+------------------------------------------------------------------+
//| Нажатие на пункте главного меню                                  |
//+------------------------------------------------------------------+
bool CMenuBar::OnClickMenuItem(const string clicked_object)
  {
//--- Выйдем, если нажатие было не на пункте меню
   if(::StringFind(clicked_object,CElement::ProgramName()+"_menuitem_",0)<0)
      return(false);
//--- Получим идентификатор и индекс из имени объекта
   int id    =IdFromObjectName(clicked_object);
   int index =IndexFromObjectName(clicked_object);
//--- Выйти, если идентификатор не совпадает
   if(id!=CElement::Id())
      return(false);
//--- Если есть указатель на контекстное меню
   if(CheckPointer(m_contextmenus[index])!=POINTER_INVALID)
     {
      //--- Состояние главного меню зависит от видимости контекстного меню
      m_menubar_state=(m_contextmenus[index].ContextMenuState())? false : true;
      //--- Установить состояние формы
      m_wnd.IsLocked(m_menubar_state);
      //--- Если главное меню отключено
      if(!m_menubar_state)
         //--- Послать сигнал на скрытие всех контекстных меню
         ::EventChartCustom(m_chart_id,ON_HIDE_CONTEXTMENUS,0,0,"");
     }
//--- Если нет указателя на контекстное меню
   else
     {
      //--- Послать сигнал на скрытие всех контекстных меню
      ::EventChartCustom(m_chart_id,ON_HIDE_CONTEXTMENUS,0,0,"");
     }
//---
   return(true);
  }

Помним, что обработка события ON_HIDE_CONTEXTMENUS производится в классе CWndEvents. В метод CWndEvents::OnHideContextMenus() нужно добавить ещё один цикл, в котором будут принудительно отключаться все главные меню, имеющиеся в базе.

//+------------------------------------------------------------------+
//| Событие ON_HIDE_CONTEXTMENUS                                     |
//+------------------------------------------------------------------+
bool CWndEvents::OnHideContextMenus(void)
  {
//--- Если сигнал на скрытие всех контекстных меню
   if(m_id!=CHARTEVENT_CUSTOM+ON_HIDE_CONTEXTMENUS)
      return(false);
//--- Скрыть все контекстные меню
   int cm_total=CWndContainer::ContextMenusTotal(0);
   for(int i=0; i<cm_total; i++)
      m_wnd[0].m_context_menus[i].Hide();
//--- Отключить главные меню
   int menu_bars_total=CWndContainer::MenuBarsTotal(0);
   for(int i=0; i<menu_bars_total; i++)
      m_wnd[0].m_menu_bars[i].State(false);
//---
   return(true);
  }

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

Рис. 4. Тест вызова контекстных меню из главного меню.

Рис. 4. Тест вызова контекстных меню из главного меню.

 

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

class CMenuBar : public CElement
  {
public:
   //--- Изменяет цвет при наведении курсора мыши
   void              ChangeObjectsColor(void);
   //---
private:
   //--- Возвращает активный пункт главного меню
   int               ActiveItemIndex(void);
   //---
  };
//+------------------------------------------------------------------+
//| Изменение цвета объекта при наведении курсора                    |
//+------------------------------------------------------------------+
void CMenuBar::ChangeObjectsColor(void)
  {
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
      m_items[i].ChangeObjectsColor();
  }
//+------------------------------------------------------------------+
//| Возвращает индекс активированного пункта меню                    |
//+------------------------------------------------------------------+
int CMenuBar::ActiveItemIndex(void)
  {
   int active_item_index=WRONG_VALUE;
//---
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Если пункт в фокусе
      if(m_items[i].MouseFocus())
        {
         //--- Запомним индекс и остановим цикл
         active_item_index=i;
         break;
        }
     }
//---
   return(active_item_index);
  }

Кроме этого, создадим метод, который будет осуществлять переключение контекстных меню наведением курсора, когда главное меню активировано. Назовём этот метод SwitchContextMenuByFocus(). В него нужно передать индекс активированного пункта главного меню, по которому можно определить, какое именно контекстное меню сделать видимым. Все остальные контекстные меню скрываются, при этом осуществляется проверка, не открыты ли контекстные меню из тех, что вызываются из пунктов главного меню. Если оказывается, что такие имеются, то генерируется пользовательское событие ON_HIDE_BACK_CONTEXTMENUS, которое уже было подробно рассмотрено ранее в этой статье.

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

class CMenuBar : public CElement
  {
private:
   //--- Переключает контекстные меню главного меню наведением курсора
   void              SwitchContextMenuByFocus(const int active_item_index);
   //---
  };
//+------------------------------------------------------------------+
//| Переключает контекстные меню главного меню наведением курсора   |
//+------------------------------------------------------------------+
void CMenuBar::SwitchContextMenuByFocus(const int active_item_index)
  {
   int items_total=ItemsTotal();
   for(int i=0; i<items_total; i++)
     {
      //--- Перейти к следующему, если в этом пункте нет контекстного меню
      if(::CheckPointer(m_contextmenus[i])==POINTER_INVALID)
         continue;
      //--- Если дошли до указанного пункта, то сделать его контекстное меню видимым
      if(i==active_item_index)
         m_contextmenus[i].Show();
      //--- Все остальные контекстные меню нужно скрыть
      else
        {
         CContextMenu *cm=m_contextmenus[i];
         //--- Скрыть контекстные меню, которые открыты из других контекстных меню.
         //    Пройдёмся в цикле по пунктам текущего контекстного меню, чтобы выяснить, есть ли такие.
         int cm_items_total=cm.ItemsTotal();
         for(int c=0; c<cm_items_total; c++)
           {
            CMenuItem *mi=cm.ItemPointerByIndex(c);
            //--- Перейти к следующему, если указатель на пункт некорректный
            if(::CheckPointer(mi)==POINTER_INVALID)
               continue;
            //--- Перейти к следующему, если этот пункт не содержит в себе контекстное меню
            if(mi.TypeMenuItem()!=MI_HAS_CONTEXT_MENU)
               continue;
            //--- Если контекстное меню есть и оно активировано
            if(mi.ContextMenuState())
              {
               //--- Отправить сигнал на закрытие всех контекстных меню, которые открыты из этого
               ::EventChartCustom(m_chart_id,ON_HIDE_BACK_CONTEXTMENUS,CElement::Id(),0,"");
               break;
              }
           }
         //--- Скрыть контекстное меню главного меню
         m_contextmenus[i].Hide();
         //--- Сбросить цвет пункта меню
         m_items[i].ResetColors();
        }
     }
  }

Теперь осталось только добавить новые созданные методы в обработчик событий класса CMenuBar по проверке события перемещения курсора мыши CHARTEVENT_MOUSE_MOVE:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CMenuBar::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Выйти, если главное меню не активировано
      if(!m_menubar_state)
         return;
      //--- Получим индекс активированного пункта главного меню
      int active_item_index=ActiveItemIndex();
      if(active_item_index==WRONG_VALUE)
         return;
      //--- Изменить цвет, если фокус изменился
      ChangeObjectsColor();
      //--- Переключить контекстное меню по активированному пункту главного меню
      SwitchContextMenuByFocus(active_item_index);
      return;
     }
  }

 


Финальный тест главного меню

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

Скомпилируйте файлы и загрузите эксперта на график. Чтобы получить такой же результат, как на скриншоте ниже, вы можете загрузить к себе на компьютер файлы, приложенные в конце статьи.

Рис. 5. Генеральный тест главного меню.

Рис. 5. Генеральный тест главного меню.


В файлах, приложенных в конце статьи, есть также индикатор для тестов с аналогичным графическим интерфейсом, как у эксперта на скриншоте выше. Кроме этого, есть версии для тестов в торговой платформе MetaTrader 4.

 

 


Заключение

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

Рис. 6. Структура библиотеки на текущей стадии разработки.

Рис. 6. Структура библиотеки на текущей стадии разработки.


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

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

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


Прикрепленные файлы |
Рецепты MQL5 - Программируем скользящие каналы Рецепты MQL5 - Программируем скользящие каналы
В данной статье представлен способ программирования системы равноудалённых каналов. Рассматриваются некоторые нюансы построения таких каналов. Приводится типизация каналов, предлагается способ универсального типа скользящих каналов. При реализации кода используется инструментарий ООП.
Вклад Томаса Демарка в технический анализ Вклад Томаса Демарка в технический анализ
В статье описаны изобретенные Томасом Демарком TD-точки и TD-линии. Показано их применение на практике. Также продемонстрирован процесс написания трех индикаторов и двух экспертов с использованием идей Томаса Демарка.
Графические интерфейсы III: Простые и многофункциональные кнопки (Глава 1) Графические интерфейсы III: Простые и многофункциональные кнопки (Глава 1)
Приступим к рассмотрению такого элемента управления, как кнопка. Будут показаны примеры нескольких классов для создания простой кнопки, кнопок с расширенным функционалом («кнопка с картинкой» и «сдвоенная кнопка»), а также кнопок, которые взаимосвязаны между собой (группы кнопок и «радио-кнопки»). Кроме этого, внесём некоторые дополнения в уже существующие классы элементов управления, чтобы расширить их возможности.
Графические интерфейсы II: Настройка обработчиков событий библиотеки (Глава 3) Графические интерфейсы II: Настройка обработчиков событий библиотеки (Глава 3)
В предыдущих статьях были реализованы классы для создания всех составных частей главного меню. Теперь же настало время познакомиться с обработчиками событий в главных базовых классах и в классах созданных элементов управления. Отдельное внимание уделено управлению состоянием графика в зависимости от того, где находится курсор мыши.