English 中文 Español Deutsch 日本語 Português
Графические интерфейсы IV: Многооконный режим и система приоритетов (Глава 2)

Графические интерфейсы IV: Многооконный режим и система приоритетов (Глава 2)

MetaTrader 5Примеры | 14 апреля 2016, 09:48
3 969 5
Anatoli Kazharski
Anatoli Kazharski

Содержание


Введение

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

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


Многооконный режим

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

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

class CWndEvents : public CWndContainer
  {
protected:
   //--- Индекс активного окна
   int               m_active_window_index;
  };

Давайте разберёмся, как будет определяться индекс активного окна. Например, пользователь назначил какой-нибудь кнопке открытие диалогового окна (W_DIALOG). Когда кнопка нажимается, генерируется пользовательское событие ON_CLICK_BUTTON, которое можно отследить в обработчике событий CProgram::OnEvent() пользовательского класса. Здесь же используем метод CWindow::Show() той формы, которую нужно показать. В текущей реализации библиотеки этого будет недостаточно, поэтому далее внесём необходимые дополнения.

Из метода CWindow::Show() нужно отправлять пользовательское событие, которое будет сообщать о том, что было открыто окно и нужно изменить значения параметров системы графического интерфейса. Для такого сообщения нужен отдельный идентификатор. Назовём его ON_OPEN_DIALOG_BOX и поместим в файл Defines.mqh, где находятся другие идентификаторы библиотеки: 

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#define ON_OPEN_DIALOG_BOX        (11) // Событие открытия диалогового окна

В метод CWindow::Show() в самом конце нужно добавить строку, как показано в листинге ниже (сокращённая версия метода). Для однозначного определения инициатора сообщения, помимо идентификатора события, нужно отправить еще идентификатор элемента и имя программы.

//+------------------------------------------------------------------+
//| Показывает окно                                                  |
//+------------------------------------------------------------------+
void CWindow::Show(void)
  {
//--- Сделать видимыми все объекты
//--- Состояние видимости
//--- Обнуление фокуса
//--- Отправить сообщение об этом
   ::EventChartCustom(m_chart_id,ON_OPEN_DIALOG_BOX,(long)CElement::Id(),0,m_program_name);
  }

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

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

class CWindow : public CElement
  {
private:
   //--- Индекс предыдущего активного окна
   int               m_prev_active_window_index;
   //---
public:
   //--- (1) Сохранение и (2) получение индекса предыдущего активного окна
   void              PrevActiveWindowIndex(const int index)                  { m_prev_active_window_index=index;   }
   int               PrevActiveWindowIndex(void)                       const { return(m_prev_active_window_index); }
  };

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

Для такого сообщения в файле Defines.mqh нужно создать идентификатор ON_RESET_WINDOW_COLORS:

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#define ON_RESET_WINDOW_COLORS    (13) // Сброс цвета всех элементов формы

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

class CWindow : public CElement
  {
public:
   //--- Установка состояния окна
   void              State(const bool flag);
  };
//+------------------------------------------------------------------+
//| Устанавливает состояние окна                                     |
//+------------------------------------------------------------------+
void CWindow::State(const bool flag)
  {
//--- Если нужно заблокировать окно
   if(!flag)
     {
      //--- Установим статус
      m_is_locked=true;
      //--- Установим цвет заголовка
      m_caption_bg.BackColor(m_caption_bg_color_off);
      //--- Сигнал на сброс цвета. Сброс будет и для других элементов.
      ::EventChartCustom(m_chart_id,ON_RESET_WINDOW_COLORS,(long)CElement::Id(),0,"");
     }
//--- Если нужно разблокировать окно
   else
     {
      //--- Установим статус
      m_is_locked=false;
      //--- Установим цвет заголовка
      m_caption_bg.BackColor(m_caption_bg_color);
      //--- Сброс фокуса
      CElement::MouseFocus(false);
     }
  }

Теперь возвращаемся к обработке события ON_OPEN_DIALOG_BOX. В главном классе обработки событий графического интерфейса (CWndEvents) создадим метод CWndEvents::OnOpenDialogBox(), вызывать который будем в общем методе обработки всех пользовательских событий CWndEvents::ChartEventCustom().

Метод CWndEvents::OnOpenDialogBox() начинается с двух проверок — на идентификатор события и на имя программы. Если они пройдены, то далее в цикле проходим по всем окнам, чтобы выяснить, от какого именно окна было сгенерировано событие. Выяснить это можно по идентификатору элемента, которое содержится в этом сообщении (lparam). Те формы, чьи идентификаторы не совпадают, будут заблокированы вместе со всеми присоединенными к ним элементами. Приоритеты всех объектов обнулятся с помощью метода ResetZorders() и не будут реагировать на нажатие левой кнопкой мыши. Дойдя до формы, чьи идентификаторы совпали, сохраняем в ней индекс текущего активного окна как индекс «предыдущего активного окна». Активируем эту форму и восстанавливаем всем её объектам приоритет на нажатие левой кнопкой мыши. Сохраняем индекс этого окна как "активное в данный момент". Затем делаем видимыми все элементы этой формы и восстанавливаем их приоритеты на нажатие левой кнопкой мыши, пропуская при этом элемент формы (так как она уже видима) и выпадающие элементы. 

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

class CWndEvents : public CWndContainer
  {
private:
   //--- Открытие диалогового окна
   bool              OnOpenDialogBox(void);
  };
//+------------------------------------------------------------------+
//| Событие CHARTEVENT_CUSTOM                                        |
//+------------------------------------------------------------------+
void CWndEvents::ChartEventCustom(void)
  {
//--- Если сигнал свернуть форму
//--- Если сигнал развернуть форму
//--- Если сигнал на скрытие контекстных меню от пункта инициатора
//--- Если сигнал на скрытие всех контекстных меню

//--- Если сигнал на открытие диалогового окна
   if(OnOpenDialogBox())
      return;
  }
//+------------------------------------------------------------------+
//| Событие ON_OPEN_DIALOG_BOX                                       |
//+------------------------------------------------------------------+
bool CWndEvents::OnOpenDialogBox(void)
  {
//--- Если сигнал на открытие диалогового окна
   if(m_id!=CHARTEVENT_CUSTOM+ON_OPEN_DIALOG_BOX)
      return(false);
//--- Выйти, если сообщение от другой программы
   if(m_sparam!=m_program_name)
      return(true);
//--- Пройдёмся по массиву окон
   int window_total=CWndContainer::WindowsTotal();
   for(int w=0; w<window_total; w++)
     {
      //--- Если идентификаторы совпадают
      if(m_windows[w].Id()==m_lparam)
        {
         //--- Запомним в этой форме индекс окна, с которого она была вызвана
         m_windows[w].PrevActiveWindowIndex(m_active_window_index);
         //--- Активируем форму
         m_windows[w].State(true);
         //--- Восстановим объектам формы приоритеты на нажатие левой кнопкой мыши
         m_windows[w].SetZorders();
         //--- Запомним индекс активированного окна
         m_active_window_index=w;
         //--- Сделать видимыми все элементы активированного окна
         int elements_total=CWndContainer::ElementsTotal(w);
         for(int e=0; e<elements_total; e++)
           {
            //--- Пропускаем формы и выпадающие элементы
            if(m_wnd[w].m_elements[e].ClassName()=="CWindow" || 
               m_wnd[w].m_elements[e].IsDropdown())
               continue;
            //--- Сделать видимым элемент
            m_wnd[w].m_elements[e].Show();
            //--- Восстановить элементу приоритет на нажатие левой кнопкой мыши
            m_wnd[w].m_elements[e].SetZorders();
           }
         //--- Скрытие всплывающих подсказок
         int tooltips_total=CWndContainer::TooltipsTotal(m_windows[w].PrevActiveWindowIndex());
         for(int t=0; t<tooltips_total; t++)
            m_wnd[m_windows[w].PrevActiveWindowIndex()].m_tooltips[t].FadeOutTooltip();
        }
      //--- Другие формы будут заблокированы, пока не закроется активированное окно
      else
        {
         //--- Заблокируем форму
         m_windows[w].State(false);
         //--- Обнулим приоритеты элементов формы на нажатие левой кнопкой мыши
         int elements_total=CWndContainer::ElementsTotal(w);
         for(int e=0; e<elements_total; e++)
            m_wnd[w].m_elements[e].ResetZorders();
        }
     }
//---
   return(true);
  }

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

class CElement
  {
public:
   //--- Сброс цвета элемента
   virtual void      ResetColors(void) {}
  };

Во всех производных классах нужно создать свои методы ResetColors(), с присущими каждому элементу особенностями. В листинге кода ниже представлен пример для элемента «Кнопка с картинкой» (CIconButton). Для всех остальных элементов метод ResetColors() можете посмотреть в приложенных к статье файлах.

class CIconButton : public CElement
  {
public:
   //--- Сброс цвета элемента
   void              ResetColors(void);
  };
//+------------------------------------------------------------------+
//| Сбрасывает цвет                                                  |
//+------------------------------------------------------------------+
void CIconButton::ResetColors(void)
  {
//--- Выйти, если режим двух состояний и кнопка нажата
   if(m_two_state && m_button_state)
      return;
//--- Сбросить цвет
   m_button.BackColor(m_back_color);
//--- Обнулим фокус
   m_button.MouseFocus(false);
   CElement::MouseFocus(false);
  }

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

Для обработки события ON_RESET_WINDOW_COLORS напишем метод CWndEvents::OnResetWindowColors(). Здесь всё довольно просто. По идентификатору элемента, который пришёл в сообщении, ищем в цикле только что дезактивированную форму, и если такая есть, то запоминаем её индекс. Далее, если индекс был сохранён, то сбрасываются цвета у всех элементов этой формы. Подробнее с этим методом можно ознакомиться в листинге кода ниже: 

class CWndEvents : public CWndContainer
  {
private:
   //--- Сброс цвета формы и её элементов
   bool              OnResetWindowColors(void);
  };
//+------------------------------------------------------------------+
//| Событие CHARTEVENT_CUSTOM                                        |
//+------------------------------------------------------------------+
void CWndEvents::ChartEventCustom(void)
  {
//--- Если сигнал свернуть форму
//--- Если сигнал развернуть форму
//--- Если сигнал на скрытие контекстных меню от пункта инициатора
//--- Если сигнал на скрытие всех контекстных меню
//--- Если сигнал на открытие диалогового окна
//--- Если сигнал на сброс цвета элементов на указанной форме
   if(OnResetWindowColors())
      return;
  }
//+------------------------------------------------------------------+
//| Событие ON_RESET_WINDOW_COLORS                                   |
//+------------------------------------------------------------------+
bool CWndEvents::OnResetWindowColors(void)
  {
//--- Если сигнал на сброс цвета окна
   if(m_id!=CHARTEVENT_CUSTOM+ON_RESET_WINDOW_COLORS)
      return(false);
//--- Для определения индекса формы, от которого пришло сообщение
   int index=WRONG_VALUE;
//--- Пройдёмся по массиву окон
   int window_total=CWndContainer::WindowsTotal();
   for(int w=0; w<window_total; w++)
     {
      //--- Если идентификаторы совпадают
      if(m_windows[w].Id()==m_lparam)
        {
         //--- Запомним индекс
         index=w;
         //--- Сбросим цвет формы
         m_windows[w].ResetColors();
         break;
        }
     }
//--- Выйдем, если индекс не определён
   if(index==WRONG_VALUE)
      return(true);
//--- Сбросим цвета у всех элементов формы
   int elements_total=CWndContainer::ElementsTotal(index);
   for(int e=0; e<elements_total; e++)
      m_wnd[index].m_elements[e].ResetColors();
//--- Перерисовка графика
   m_chart.Redraw();
   return(true);
  }

С открытием окон разобрались. Теперь нужно реализовать методы для закрытия и восстановления предыдущего активного окна. Для обработки этого события нужно создать идентификатор ON_CLOSE_DIALOG_BOX в файле Defines.mqh:

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#define ON_CLOSE_DIALOG_BOX       (12) // Событие закрытия диалогового окна

В классе CWindow для закрытия формы, а вместе с ней и закрытия программы, сейчас используется метод CWindow::CloseWindow(). В этом методе ветка для закрытия диалоговых окон (W_DIALOG) пока не заполнена. Напишем дополнительный метод, который будет генерировать событие для закрытия диалоговых окон. Кроме (1) идентификатора события, в сообщении будут содержаться также (2) идентификатор элемента, (3) индекс предыдущего активного окна и (4) текст заголовка. Назовём этот метод CWindow::CloseDialogBox(). В дальнейшем будем его использовать и в сложных составных элементах управления, где закрытие окна будет осуществляться посредством других элементов, кроме кнопки закрытия.

class CWindow : public CElement
  {
public:
   //--- Закрытие диалогового окна
   void              CloseDialogBox(void);
  };
//+------------------------------------------------------------------+
//| Закрытие диалогового окна                                        |
//+------------------------------------------------------------------+
void CWindow::CloseDialogBox(void)
  {
//--- Состояние видимости
   CElement::IsVisible(false);
//--- Отправить сообщение об этом
   ::EventChartCustom(m_chart_id,ON_CLOSE_DIALOG_BOX,CElement::Id(),m_prev_active_window_index,m_caption_text);
  }

В классе CWindow вызов метода CWindow::CloseDialogBox() нужно осуществлять в методе CWindow::CloseWindow(), как это показано в сокращённой версии в листинге ниже. Полную версию смотрите в приложенных файлах в конце статьи.

//+------------------------------------------------------------------+
//| Закрытие диалогового окна или программы                          |
//+------------------------------------------------------------------+
bool CWindow::CloseWindow(const string pressed_object)
  {
//--- Если было нажатие не на кнопке закрытия окна
   if(pressed_object!=m_button_close.Name())
      return(false);
//--- Если это главное окно
   if(m_window_type==W_MAIN)
     {
      //--- ...
     }
//--- Если это диалоговое окно
   else if(m_window_type==W_DIALOG)
     {
      //--- Закроем его
      CloseDialogBox();
     }
//---
   return(false);
  }

После отправки сообщения с идентификатором ON_CLOSE_DIALOG_BOX его нужно отследить и обработать в обработчике класса CWndEvents. Для этого напишем метод CWndEvents::OnCloseDialogBox(). В нём в цикле идём по всем окнам в базе и ищем то, чей идентификатор элемента будет совпадать с идентификатором в сообщении. Если такое окно найдено, то его нужно дезактивировать. Затем спрячем его вместе со всеми привязанными к нему элементами и активируем форму по переданному в сообщении индексу. Далее сохраняем индекс текущего активного окна и восстанавливаем приоритеты элементов на нажатие левой кнопкой мыши.

class CWndEvents : public CWndContainer
  {
private:
   //--- Закрытие диалогового окна
   bool              OnCloseDialogBox(void);
  };
//+------------------------------------------------------------------+
//| Событие CHARTEVENT_CUSTOM                                        |
//+------------------------------------------------------------------+
void CWndEvents::ChartEventCustom(void)
  {
//--- Если сигнал свернуть форму
//--- Если сигнал развернуть форму
//--- Если сигнал на скрытие контекстных меню от пункта инициатора
//--- Если сигнал на скрытие всех контекстных меню
//--- Если сигнал на открытие диалогового окна
//--- Если сигнал на закрытие диалогового окна
   if(OnCloseDialogBox())
      return;
//--- Если сигнал на сброс цвета элементов на указанной форме
  }
//+------------------------------------------------------------------+
//| Событие ON_CLOSE_DIALOG_BOX                                      |
//+------------------------------------------------------------------+
bool CWndEvents::OnCloseDialogBox(void)
  {
//--- Если сигнал на закрытие диалогового окна
   if(m_id!=CHARTEVENT_CUSTOM+ON_CLOSE_DIALOG_BOX)
      return(false);
//--- Пройдёмся по массиву окон
   int window_total=CWndContainer::WindowsTotal();
   for(int w=0; w<window_total; w++)
     {
      //--- Если идентификаторы совпадают
      if(m_windows[w].Id()==m_lparam)
        {
         //--- Заблокируем форму
         m_windows[w].State(false);
         //--- Спрячем форму
         int elements_total=CWndContainer::ElementsTotal(w);
         for(int e=0; e<elements_total; e++)
            m_wnd[w].m_elements[e].Hide();
         //--- Активируем предыдущую форму
         m_windows[int(m_dparam)].State(true);
         //--- Перерисовка графика
         m_chart.Redraw();
         break;
        }
     }
//--- Установка индекса предыдущего окна
   m_active_window_index=int(m_dparam);
//--- Восстановление приоритетов на нажатие левой кнопкой мыши у активированного окна
   int elements_total=CWndContainer::ElementsTotal(m_active_window_index);
   for(int e=0; e<elements_total; e++)
      m_wnd[m_active_window_index].m_elements[e].SetZorders();
//---
   return(true);
  }

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

 


Тест многооконного режима

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

В листинге кода ниже показано, что нужно добавить в пользовательский класс приложения (CProgram) на текущей стадии разработки: 

class CProgram : public CWndEvents
  {
private:
   //--- Форма 2
   CWindow           m_window2;
   //--- Кнопки с картинками
   CIconButton       m_icon_button6;
   CIconButton       m_icon_button7;
   CIconButton       m_icon_button8;

   //--- Форма 3
   CWindow           m_window3;
   //---
private:
   //--- Форма 2
   bool              CreateWindow2(const string text);
   //--- Кнопки с картинками
#define ICONBUTTON6_GAP_X        (7)
#define ICONBUTTON6_GAP_Y        (25)
   bool              CreateIconButton6(const string text);
#define ICONBUTTON7_GAP_X        (7)
#define ICONBUTTON7_GAP_Y        (50)
   bool              CreateIconButton7(const string text);
#define ICONBUTTON8_GAP_X        (7)
#define ICONBUTTON8_GAP_Y        (75)
   bool              CreateIconButton8(const string text);

   //--- Форма 3
   bool              CreateWindow3(const string text);
  };

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

//+------------------------------------------------------------------+
//| Создаёт торговую панель                                          |
//+------------------------------------------------------------------+
bool CProgram::CreateTradePanel(void)
  {
//--- Создание формы 1 для элементов управления
//--- Создание элементов управления:
//    Главное меню
//--- Контекстные меню
//--- Создание статусной строки
//--- Кнопки с картинкой

//--- Создание формы 2 для элементов управления
   if(!CreateWindow2("Icon Button 1"))
      return(false);
//--- Кнопки с картинкой
   if(!CreateIconButton6("Icon Button 6..."))
      return(false);
   if(!CreateIconButton7("Icon Button 7"))
      return(false);
   if(!CreateIconButton8("Icon Button 8"))
      return(false);

//--- Создание формы 3 для элементов управления
   if(!CreateWindow3("Icon Button 6"))
      return(false);

//--- Всплывающие подсказки
//--- Перерисовка графика
   m_chart.Redraw();
   return(true);
  }

Рассмотрим метод только первого диалогового окна (вторая форма). Вы уже знаете, что для добавления формы в базу нужно использовать метод CWndContainer::AddWindow(). Далее обратите внимание (см. листинг кода ниже), как определяются координаты для формы. По умолчанию, при загрузке программы на график они имеют нулевые значения, поэтому будут установлены те, которые вы сами посчитаете нужным. В данном случае, для примера, это значения: x=1, y=20. После этого можно переместить форму, а потом переключить таймфрейм или символ графика. При таком подходе, как показано в листинге кода ниже, форма останется там, где была в последний раз. Если вам нужно, чтобы форма устанавливалась в первоначальное местоположение, как при первой загрузке программы на график, то просто уберите эти условия. В нашем же примере такие условия будут у всех трёх форм графического интерфейса программы.

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

//+------------------------------------------------------------------+
//| Создаёт форму 2 для элементов управления                         |
//+------------------------------------------------------------------+
bool CProgram::CreateWindow2(const string caption_text)
  {
//--- Добавим указатель окна в массив окон
   CWndContainer::AddWindow(m_window2);
//--- Координаты
   int x=(m_window2.X()>0) ? m_window2.X() : 1;
   int y=(m_window2.Y()>0) ? m_window2.Y() : 20;
//--- Свойства
   m_window2.Movable(true);
   m_window2.WindowType(W_DIALOG);
   m_window2.XSize(160);
   m_window2.YSize(160);
   m_window2.IconFile("Images\\EasyAndFastGUI\\Icons\\bmp16\\coins.bmp");
   m_window2.CaptionBgColor(clrCornflowerBlue);
   m_window2.CaptionBgColorHover(C'150,190,240');
//--- Создание формы
   if(!m_window2.CreateWindow(m_chart_id,m_subwin,caption_text,x,y))
      return(false);
//---
   return(true);
  }

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

Помните, что:

  • Элементу нужно передать указатель той формы, к которой он должен быть присоединён
  • Когда сохраняете указатель на элемент в базу, указывайте индекс той формы, к которой нужно присоединить элемент. В данном случае это индекс 1. 
//+------------------------------------------------------------------+
//| Создаёт кнопку с картинкой 6                                     |
//+------------------------------------------------------------------+
bool CProgram::CreateIconButton6(const string button_text)
  {
//--- Сохраним указатель на окно
   m_icon_button6.WindowPointer(m_window2);
//--- Координаты
   int x=m_window2.X()+ICONBUTTON6_GAP_X;
   int y=m_window2.Y()+ICONBUTTON6_GAP_Y;
//--- Установим свойства перед созданием
   m_icon_button6.TwoState(false);
   m_icon_button6.ButtonXSize(146);
   m_icon_button6.ButtonYSize(22);
   m_icon_button6.LabelColor(clrBlack);
   m_icon_button6.LabelColorPressed(clrBlack);
   m_icon_button6.BorderColorOff(clrWhite);
   m_icon_button6.BackColor(clrLightGray);
   m_icon_button6.BackColorHover(C'193,218,255');
   m_icon_button6.BackColorPressed(C'153,178,215');
   m_icon_button6.IconFileOn("Images\\EasyAndFastGUI\\Icons\\bmp16\\script.bmp");
   m_icon_button6.IconFileOff("Images\\EasyAndFastGUI\\Icons\\bmp16\\script_colorless.bmp");
//--- Создадим элемент управления
   if(!m_icon_button6.CreateIconButton(m_chart_id,m_subwin,button_text,x,y))
      return(false);
//--- Добавим указатель на элемент в базу
   CWndContainer::AddToElementsArray(1,m_icon_button6);
   return(true);
  }

Управлением показа того или иного окна должен заниматься разработчик приложения. В обработчике событий пользовательского класса (CProgram) нужно отследить нажатие на каком-либо элементе управления и показать нужное окно. Вызов первого диалогового окна свяжем с кнопкой на главном окне эксперта (вторая форма), вызов второго —  с кнопкой на первом диалоговом окне (третья форма).

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Событие нажатия на кнопке
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- Если текст совпадает
      if(sparam==m_icon_button1.Text())
        {
         //--- Показать окно 2
         m_window2.Show();
        }
      //--- Если текст совпадает
      if(sparam==m_icon_button6.Text())
        {
         //--- Показать окно 3
         m_window3.Show();
        }
     }
  }

На скриншоте ниже показан результат, который должен получиться в итоге. Обратите внимание на многоточия в названиях кнопок «Icon Button 1...» и «Icon Button 6...». Обычно таким образом пользователю дают понять, что при нажатии на этот элемент откроется диалоговое окно.

Рис. 1. Тест многооконного режима.

Рис. 1. Тест многооконного режима.

 

Если сейчас, в момент, когда открыто несколько форм, переключить символ или таймфрейм графика, вы столкнётесь с проблемой. Диалоговые окна исчезнут, как им и положено, но управление главному окну не будет передано. Из-за этого форма не будет реагировать на действия пользователя. Решается эта проблема просто. Вспомним, что в методе деинициализации пользовательского класса CProgram::OnDeinitEvent() вызывается метод CWndEvents::Destroy(). В нём удаляется графический интерфейс приложения. Управление главному окну нужно передавать как раз в момент удаления графического интерфейса. Поэтому в метод CWndEvents::Destroy() нужно внести небольшие дополнения:

  • Установить индекс главного окна как "активного".
  • Активировать главное окно, а все остальные дезактивировать.

Текущая версия метода CWndEvents::Destroy() представлена в листинге кода ниже: 

//+------------------------------------------------------------------+
//| Удаление всех объектов                                           |
//+------------------------------------------------------------------+
void CWndEvents::Destroy(void)
  {
//--- Установим индекс главного окна
   m_active_window_index=0;
//--- Получим количество окон
   int window_total=CWndContainer::WindowsTotal();
//--- Пройдёмся по массиву окон
   for(int w=0; w<window_total; w++)
     {
      //--- Активируем главное окно
      if(m_windows[w].WindowType()==W_MAIN)
         m_windows[w].State(true);
      //--- Заблокируем диалоговые окна
      else
         m_windows[w].State(false);
     }
//--- Освободим массивы элементов
   for(int w=0; w<window_total; w++)
     {
      int elements_total=CWndContainer::ElementsTotal(w);
      for(int e=0; e<elements_total; e++)
        {
         //--- Если указатель невалидный, перейти к следующему
         if(::CheckPointer(m_wnd[w].m_elements[e])==POINTER_INVALID)
            continue;
         //--- Удалить объекты элемента
         m_wnd[w].m_elements[e].Delete();
        }
      //--- Освободить массивы элементов
      ::ArrayFree(m_wnd[w].m_objects);
      ::ArrayFree(m_wnd[w].m_elements);
      ::ArrayFree(m_wnd[w].m_context_menus);
     }
//--- Освободить массивы форм
   ::ArrayFree(m_wnd);
   ::ArrayFree(m_windows);
  }

Первая версия многооконного режима реализована. Как видите, всё оказалось не так сложно, как могло показаться на первый взгляд.  

 


Доработка системы приоритетов на нажатие левой кнопкой мыши

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

  • ON_ZERO_PRIORITIES – обнуление приоритетов.
  • ON_SET_PRIORITIES – восстановление приоритетов.

Добавим их в файл Defines.mqh:

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#define ON_ZERO_PRIORITIES        (14) // Сброс приоритетов на нажатие кнопки мыши
#define ON_SET_PRIORITIES         (15) // Восстановление приоритетов на нажатие кнопки мыши

Генерация событий с этими идентификаторами должна располагаться в классах тех элементов, которые являются или бывают (могут быть) выпадающими. На текущем этапе разработки библиотеки, в имеющемся наборе интерфейса, таким элементом является «Контекстное меню». Поэтому сейчас в классе CContextMenu в методах Show() и Hide() нужно добавить код, как это показано в листинге кода ниже (сокращённые версии методов):

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

//--- Отправим сигнал на обнуление приоритетов на нажатие левой кнопкой мыши
   ::EventChartCustom(m_chart_id,ON_ZERO_PRIORITIES,CElement::Id(),0.0,"");
  }
//+------------------------------------------------------------------+
//| Скрывает контекстное меню                                        |
//+------------------------------------------------------------------+
void CContextMenu::Hide(void)
  {
//--- Выйти, если элемент скрыт
//--- Скрыть объекты контекстного меню
//--- Скрыть пункты меню
//--- Обнулить фокус
//--- Состояние контекстного меню
//--- Отметить состояние в предыдущем узле

//--- Отправим сигнал на восстановление приоритетов на нажатие левой кнопкой мыши
   ::EventChartCustom(m_chart_id,ON_SET_PRIORITIES,0,0.0,"");
  }

Принимать эти сообщения будем в главном классе обработки всех сообщений (CWndEvents). С этими целями для каждого идентификатора напишем отдельный метод-обработчик. Эти методы будут вызываться в главном методе обработки пользовательских событий CWndEvents::ChartEventCustom(). 

class CWndEvents : public CWndContainer
  {
private:
   //--- Сброс приоритетов на нажатие левой кнопкой мыши
   bool              OnZeroPriorities(void);
   //--- Восстановление приоритетов на нажатие левой кнопкой мыши
   bool              OnSetPriorities(void);
  };
//+------------------------------------------------------------------+
//| Событие CHARTEVENT_CUSTOM                                        |
//+------------------------------------------------------------------+
void CWndEvents::ChartEventCustom(void)
  {
//--- Если сигнал свернуть форму
//--- Если сигнал развернуть форму
//--- Если сигнал на скрытие контекстных меню от пункта инициатора
//--- Если сигнал на скрытие всех контекстных меню
//--- Если сигнал на открытие диалогового окна
//--- Если сигнал на закрытие диалогового окна
//--- Если сигнал на сброс цвета элементов на указанной форме

//--- Если сигнал на сброс приоритетов на нажатие левой кнопкой мыши
   if(OnZeroPriorities())
      return;
//--- Если сигнал на восстановление приоритетов на нажатие левой кнопкой мыши
   if(OnSetPriorities())
      return;
  }

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

//+------------------------------------------------------------------+
//| Событие ON_ZERO_PRIORITIES                                       |
//+------------------------------------------------------------------+
bool CWndEvents::OnZeroPriorities(void)
  {
//--- Если сигнал на обнуление приоритетов на нажатие левой кнопкой мыши
   if(m_id!=CHARTEVENT_CUSTOM+ON_ZERO_PRIORITIES)
      return(false);
//---
   int elements_total=CWndContainer::ElementsTotal(m_active_window_index);
   for(int e=0; e<elements_total; e++)
     {
      //--- Обнулить приоритеты всех элементов кроме того, чей id был передан в событии и ...
      if(m_lparam!=m_wnd[m_active_window_index].m_elements[e].Id())
        {
         //--- ... кроме контекстных меню
         if(m_wnd[m_active_window_index].m_elements[e].ClassName()=="CMenuItem" ||
            m_wnd[m_active_window_index].m_elements[e].ClassName()=="CContextMenu")
            continue;
         //---
         m_wnd[m_active_window_index].m_elements[e].ResetZorders();
        }
     }
//---
   return(true);
  }

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

//+------------------------------------------------------------------+
//| Событие ON_SET_PRIORITIES                                        |
//+------------------------------------------------------------------+
bool CWndEvents::OnSetPriorities(void)
  {
//--- Если сигнал на восстановление приоритетов на нажатие левой кнопкой мыши
   if(m_id!=CHARTEVENT_CUSTOM+ON_SET_PRIORITIES)
      return(false);
//---
   int elements_total=CWndContainer::ElementsTotal(m_active_window_index);
   for(int e=0; e<elements_total; e++)
      m_wnd[m_active_window_index].m_elements[e].SetZorders();
//---
   return(true);
  }

 


Заключение

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

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

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

 

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

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

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (5)
Artyom Trishkin
Artyom Trishkin | 15 апр. 2016 в 07:55

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

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

Anatoli Kazharski
Anatoli Kazharski | 15 апр. 2016 в 10:49
Artyom Trishkin:

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

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

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

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

Artyom Trishkin
Artyom Trishkin | 15 сент. 2016 в 20:59

Анатолий, а возможно ли сделать так, чтобы можно было с главной панели открыть два окна так, чтобы все три окна оставались активными?

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

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

Нажали на главной панели кнопку2 при уже открытой панели1 - открылась панель2. При этом и на главной панели, и на панели1, и на панели2 все их кнопки остаются активными.

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

Сейчас есть возможность иметь только одну активную панель в одно и то же время.

Anatoli Kazharski
Anatoli Kazharski | 16 сент. 2016 в 10:07
Artyom Trishkin:

Анатолий, а возможно ли сделать так, чтобы можно было с главной панели открыть два окна так, чтобы все три окна оставались активными?

...

На текущий момент такой возможности нет. Но в планах есть.

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

Artyom Trishkin
Artyom Trishkin | 16 сент. 2016 в 15:47
Anatoli Kazharski:

На текущий момент такой возможности нет. Но в планах есть.

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

Ясно, жаль. С нетерпением ждём ;)
Самооптимизация экспертов: Эволюционные и генетические алгоритмы Самооптимизация экспертов: Эволюционные и генетические алгоритмы
В статье будут рассмотрены основные принципы, заложенные в эволюционных алгоритмах, их разновидности и особенности. На примере простого эксперта с помощью экспериментов покажем, что может дать нашей торговой системе использование оптимизации. Рассмотрим программные пакеты, реализующие генетические, эволюционные и другие виды оптимизации и приведем примеры применения при оптимизации набора предикторов и оптимизации параметров торговой системы.
Калькулятор сигналов Калькулятор сигналов
Калькулятор сигналов работает прямо из терминала MetaTrader 5, и это большое его преимущество, так как терминал осуществляет предварительный отбор и сортировку сигналов. Таким образом, пользователь видит в терминале MetaTrader 5 только сигналы с максимальной совместимостью с его торговым счётом.
Графические интерфейсы V: Вертикальная и горизонтальная полоса прокрутки (Глава 1) Графические интерфейсы V: Вертикальная и горизонтальная полоса прокрутки (Глава 1)
Продолжаем рассматривать процесс разработки библиотеки для создания графических интерфейсов в среде торговых терминалов MetaTrader. В первой главе пятой части серии разработаем классы для создания таких элементов управления, как вертикальные и горизонтальные полосы прокрутки.
Графические интерфейсы IV: Информационные элементы интерфейса (Глава 1) Графические интерфейсы IV: Информационные элементы интерфейса (Глава 1)
На текущий момент в разрабатываемой библиотеке для создания графических интерфейсов есть форма и несколько элементов управления, которые можно к ней присоединять. Сейчас у нас все готово для рассмотрения вопроса многооконного режима, однако этим мы займемся во второй главе данной статьи. Прежде мы напишем классы, с помощью которых можно будет создавать информационные элементы интерфейса, такие, как «статусная строка» и «всплывающая подсказка».