Графические интерфейсы I: "Оживление" графического интерфейса (Глава 3)

Anatoli Kazharski | 23 декабря, 2015

Содержание

 

Введение

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

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

 

Управление графическим интерфейсом

Форма для элементов управления устанавливается на график, но сейчас она абсолютно безжизненна. Текущая задача состоит в том, чтобы форма и ее элементы начали реагировать на действия пользователя. Чтобы это можно было реализовать, нужно отслеживать положение курсора на графике. Программа должна «знать» координаты курсора в любой момент времени. С параметрами графика по умолчанию в MQL-приложении не получится это реализовать. Отслеживание положения курсора и нажатия кнопок мыши нужно включить. Забегая вперед, отмечу, что в процессе разработки понадобится использовать еще некоторые свойства графика, поэтому подключим к нашей библиотеке в файл WndEvents.mqh класс из стандартной библиотеки CChart и создадим его экземпляр в теле класса.

//+------------------------------------------------------------------+
//|                                                    WndEvents.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Defines.mqh"
#include "WndContainer.mqh"
#include <Charts\Chart.mqh>
//+------------------------------------------------------------------+
//| Класс для обработки событий                                      |
//+------------------------------------------------------------------+
class CWndEvents : public CWndContainer
  {
protected:
   CChart            m_chart;
  };

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

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CWndEvents::CWndEvents(void) : m_chart_id(0),
                               m_subwin(0),
                               m_indicator_name(""),
                               m_program_name(PROGRAM_NAME)
  {
//--- Получим ID текущего графика
   m_chart.Attach();
//--- Включим слежение за событиями мыши
   m_chart.EventMouseMove(true);
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CWndEvents::~CWndEvents(void)
  {
//--- Отсоединиться от графика
   m_chart.Detach();
  }

Как уже упоминалось ранее, в классе каждого элемента управления будет свой обработчик событий. Переместимся сейчас в класс CWindow и создадим несколько методов, которые понадобятся для управления формой. Все они будут вызываться в методе CWindow::OnEvent(). Определимся с функционалом, который добавим в класс CWindow:

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

2. Если MQL-приложение является индикатором и находится не в главном окне графика, то понадобится корректировать Y-координату.

3. Нужно проверять состояние левой кнопки мыши, а также, где она была нажата. Будет четыре состояния:

  • NOT_PRESSED — кнопка не нажата.
  • PRESSED_OUTSIDE — кнопка нажата вне области формы.
  • PRESSED_INSIDE_WINDOW — кнопка нажата в области формы.
  • PRESSED_INSIDE_HEADER — кнопка нажата в области заголовка.

Добавим в файл Enums.mqh перечисление ENUM_WMOUSE_STATE:

//+------------------------------------------------------------------+
//|                                                        Enums.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Перечисление состояний левой кнопки мыши для формы               |
//+------------------------------------------------------------------+
enum ENUM_WMOUSE_STATE
  {
   NOT_PRESSED           =0,
   PRESSED_OUTSIDE       =1,
   PRESSED_INSIDE_WINDOW =2,
   PRESSED_INSIDE_HEADER =3
  };

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

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

6. При обновлении координат будет производиться проверка на выход из области графика и соответствующая корректировка. Для такой проверки понадобится (1) объект класса CChart, (2) переменные и метод для получения размеров графика.

7. Нужен метод для перемещения всех объектов формы относительно обновленных координат. Для этих целей уже был ранее объявлен виртуальный метод CWindow::Moving(). Теперь нужно реализовать его.

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

9. Отслеживание фокуса мыши будет работать для всех объектов, из которых состоит окно. Поэтому создадим метод, в котором будет производиться такая проверка.

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

 

Функционал для возможности перемещения формы

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

//+------------------------------------------------------------------+
//|                                                       Window.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
#include <Charts\Chart.mqh>
//+------------------------------------------------------------------+
//| Класс создания формы для элементов управления                    |
//+------------------------------------------------------------------+
class CWindow : public CElement
  {
private:
   CChart            m_chart;
   //--- Возможность перемещать окно на графике
   bool              m_movable;
   //--- Размеры графика
   int               m_chart_width;
   int               m_chart_height;
   //--- Переменные, связанные с перемещением
   int               m_prev_x;        // Точка фиксации X при нажатии
   int               m_prev_y;        // Точка фиксации Y при нажатии
   int               m_size_fixing_x; // Расстояние от X-координаты до точки фиксации X
   int               m_size_fixing_y; // Расстояние от Y-координаты до точки фиксации Y
   //--- Состояние кнопки мыши с учетом, где она была нажата
   ENUM_WMOUSE_STATE m_clamping_area_mouse;
   //---
public:
   //--- Возможность перемещения окна
   bool              Movable(void)                                     const { return(m_movable);                  }
   void              Movable(const bool flag)                                { m_movable=flag;                     }
   //--- Получение размеров графика
   void              SetWindowProperties(void);
   //--- Преобразует координату Y в относительную
   void              YToRelative(const int y);
   //--- Проверка курсора в области заголовка 
   bool              CursorInsideCaption(const int x,const int y);
   //--- Обнуление переменных
   void              ZeroPanelVariables(void);
   //--- Проверка фокуса мыши
   void              CheckMouseFocus(const int x,const int y,const int subwin);
   //--- Проверка состояния левой кнопки мыши
   void              CheckMouseButtonState(const int x,const int y,const string state);
   //--- Установка режима графика
   void              SetChartState(const int subwindow_number);
   //--- Обновление координат формы
   void              UpdateWindowXY(const int x,const int y);
  };

Код методов SetWindowProperties(), YToRelative(), CursorInsideCaption(), ZeroPanelVariables() довольно прост и не требует каких-то дополнительных пояснений. Единственное, о чем здесь можно было бы напомнить, так это быть внимательным при передаче в методы объекта CChart номера подокна (m_subwin). Это должен быть номер подокна, в котором находится MQL-программа.

//+------------------------------------------------------------------+
//| Получение размеров графика                                       |
//+------------------------------------------------------------------+
void CWindow::SetWindowProperties(void)
  {
//--- Получим ширину и высоту окна графика
   m_chart_width  =m_chart.WidthInPixels();
   m_chart_height =m_chart.HeightInPixels(m_subwin);
  }
//+------------------------------------------------------------------+
//| Преобразует координату Y в относительную                         |
//+------------------------------------------------------------------+
int CWindow::YToRelative(const int y)
  {
//--- Получим расстояние от верха графика до подокна индикатора
   int chart_y_distance=m_chart.SubwindowY(m_subwin);
//--- Преобразуем координату Y в относительную
   return(y-chart_y_distance);
  }
//+------------------------------------------------------------------+
//| Проверка положения курсора в области заголовка окна              |
//+------------------------------------------------------------------+
bool CWindow::CursorInsideCaption(const int x,const int y)
  {
   return(x>m_x && x<X2()-m_right_limit && y>m_y && y<m_caption_bg.Y2());
  }
//+------------------------------------------------------------------+
//| Обнуление переменных, связанных с перемещением окна и            |
//| состоянием левой кнопки мыши                                     |
//+------------------------------------------------------------------+
void CWindow::ZeroPanelVariables(void)
  {
   m_prev_x              =0;
   m_prev_y              =0;
   m_size_fixing_x       =0;
   m_size_fixing_y       =0;
   m_clamping_area_mouse =NOT_PRESSED;
  }

В методе CWindow::CheckMouseButtonState() производится проверка состояния левой кнопки мыши относительно формы. Для этого в метод передаются координаты курсора и строковый параметр событийной модели графика, который при обработке события CHARTEVENT_MOUSE_MOVE показывает состояние левой кнопки мыши. То есть этот параметр может показывать значение «0», если кнопка мыши отжата, либо «1», если кнопка мыши нажата.

Если кнопка отжата, то все служебные переменные обнуляются и работа метода завершается. Если же кнопка нажата, то далее в методе CWindow::CheckMouseButtonState() производится проверка, в какой области графика нажата кнопка. Если окажется, что она уже имеет зафиксированное состояние, то есть уже была зажата в какой-либо области графика, то программа выйдет из метода (return).

//+------------------------------------------------------------------+
//| Проверяет состояние кнопки мыши                                  |
//+------------------------------------------------------------------+
void CWindow::CheckMouseButtonState(const int x,const int y,const string state)
  {
//--- Если кнопка отжата
   if(state=="0")
     {
      //--- Обнулим переменные
      ZeroPanelVariables();
      return;
     }
//--- Если кнопка нажата
   if(state=="1")
     {
      //--- Выйдем, если состояние уже зафиксировано
      if(m_clamping_area_mouse!=NOT_PRESSED)
         return;
      //--- Вне области панели
      if(!CElement::MouseFocus())
         m_clamping_area_mouse=PRESSED_OUTSIDE;
      //--- В области панели
      else
        {
         //--- Если внутри заголовка
         if(CursorInsideCaption(x,y))
           {
            m_clamping_area_mouse=PRESSED_INSIDE_HEADER;
            return;
           }
         //--- Если в области окна
         m_clamping_area_mouse=PRESSED_INSIDE_WINDOW;
        }
     }
  }

Во всех классах объектов-примитивов в файле Objects.mqh были предусмотрены методы для определения границ объекта. Теперь с помощью этих методов можно легко и быстро узнать, находится ли тот или иной объект в фокусе мыши. В классе CWindow с помощью метода CheckMouseFocus() можно проверить фокус для формы и для всех объектов формы. В этот метод будут передаваться текущие координаты курсора и номер подокна, в котором находится курсор. Как получить номер подокна, в котором находится курсор, будет показано чуть позже.

//+------------------------------------------------------------------+
//| Проверка фокуса мыши                                             |
//+------------------------------------------------------------------+
void CWindow::CheckMouseFocus(const int x,const int y,const int subwin)
  {
//--- Если курсор в зоне окна программы
   if(subwin==m_subwin)
     {
      //--- Если сейчас не в режиме перемещения формы
      if(m_clamping_area_mouse!=PRESSED_INSIDE_HEADER)
        {
         //--- Проверим местоположение курсора
         CElement::MouseFocus(x>m_x && x<X2() && y>m_y && y<Y2());
         //---
         m_button_rollup.MouseFocus(x>m_button_rollup.X() && x<m_button_rollup.X2() && 
                                    y>m_button_rollup.Y() && y<m_button_rollup.Y2());
         m_button_close.MouseFocus(x>m_button_close.X() && x<m_button_close.X2() && 
                                   y>m_button_close.Y() && y<m_button_close.Y2());
         m_button_unroll.MouseFocus(x>m_button_unroll.X() && x<m_button_unroll.X2() && 
                                    y>m_button_unroll.Y() && y<m_button_unroll.Y2());
        }
     }
   else
     {
      CElement::MouseFocus(false);
     }
  }

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

//+------------------------------------------------------------------+
//| Установим состояние графика                                      |
//+------------------------------------------------------------------+
void CWindow::SetChartState(const int subwindow_number)
  {
//--- Если (курсор в области панели и кнопка мыши отжата) или
//    кнопка мыши была нажата внутри области формы или заголовка
   if((CElement::MouseFocus() && m_clamping_area_mouse==NOT_PRESSED) || 
      m_clamping_area_mouse==PRESSED_INSIDE_WINDOW ||
      m_clamping_area_mouse==PRESSED_INSIDE_HEADER)
     {
      //--- Отключим скролл и управление торговыми уровнями
      m_chart.MouseScroll(false);
      m_chart.SetInteger(CHART_DRAG_TRADE_LEVELS,false);
     }
//--- Включим управление, если курсор вне зоны окна
   else
     {
      m_chart.MouseScroll(true);
      m_chart.SetInteger(CHART_DRAG_TRADE_LEVELS,true);
     }
  }

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

//+------------------------------------------------------------------+
//| Обновление координат окна                                        |
//+------------------------------------------------------------------+
void CWindow::UpdateWindowXY(const int x,const int y)
  {
//--- Если установлен режим фиксированной формы
   if(!m_movable)
      return;
//---
   int new_x_point =0; // Новая координата X
   int new_y_point =0; // Новая координата Y
//--- Лимиты
   int limit_top    =0;
   int limit_left   =0;
   int limit_bottom =0;
   int limit_right  =0;
//--- Если кнопка мыши нажата
   if((bool)m_clamping_area_mouse)
     {
      //--- Запомним текущие координаты XY курсора
      if(m_prev_y==0 || m_prev_x==0)
        {
         m_prev_y=y;
         m_prev_x=x;
        }
      //--- Запомним расстояние от крайней точки формы до курсора
      if(m_size_fixing_y==0 || m_size_fixing_x==0)
        {
         m_size_fixing_y=m_y-m_prev_y;
         m_size_fixing_x=m_x-m_prev_x;
        }
     }
//--- Установим лимиты
   limit_top    =y-::fabs(m_size_fixing_y);
   limit_left   =x-::fabs(m_size_fixing_x);
   limit_bottom =m_y+m_caption_height;
   limit_right  =m_x+m_x_size;
//--- Если не выходим за пределы графика вниз/вверх/вправо/влево
   if(limit_bottom<m_chart_height && limit_top>=0 && 
      limit_right<m_chart_width && limit_left>=0)
     {
      new_y_point =y+m_size_fixing_y;
      new_x_point =x+m_size_fixing_x;
     }
//--- Если вышли из границ графика
   else
     {
      if(limit_bottom>m_chart_height) // > вниз
        {
         new_y_point =m_chart_height-m_caption_height;
         new_x_point =x+m_size_fixing_x;
        }
      if(limit_top<0) // > вверх
        {
         new_y_point =0;
         new_x_point =x+m_size_fixing_x;
        }
      if(limit_right>m_chart_width) // > вправо
        {
         new_x_point =m_chart_width-m_x_size;
         new_y_point =y+m_size_fixing_y;
        }
      if(limit_left<0) // > влево
        {
         new_x_point =0;
         new_y_point =y+m_size_fixing_y;
        }
     }
//--- Обновим координаты, если было перемещение
   if(new_x_point>0 || new_y_point>0)
     {
      //--- Скорректируем координаты формы
      m_x =(new_x_point<=0)? 1 : new_x_point;
      m_y =(new_y_point<=0)? 1 : new_y_point;
      //---
      if(new_x_point>0)
         m_x=(m_x>m_chart_width-m_x_size-1) ? m_chart_width-m_x_size-1 : m_x;
      if(new_y_point>0)
         m_y=(m_y>m_chart_height-m_caption_height-1) ? m_chart_height-m_caption_height-2 : m_y;
      //--- Обнулим точки фиксации
      m_prev_x=0;
      m_prev_y=0;
     }
  }

В обработчике событий графика OnEvent() класса CWindow при обработке события перемещения мыши (CHARTEVENT_MOUSE_MOVE) номер подокна можно получить с помощью функции ChartXYToTimePrice(). Если функция вернула true, то далее получаем с помощью ранее созданного метода CWindow::YToRelative() относительную координату Y и после этого проходим по всем перечисленным выше методам:

//+------------------------------------------------------------------+
//| Обработчик событий графика                                       |
//+------------------------------------------------------------------+
void CWindow::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      int      x      =(int)lparam; // Координата по оси X
      int      y      =(int)dparam; // Координата по оси Y
      int      subwin =WRONG_VALUE; // Номер окна, в котором находится курсор
      datetime time   =NULL;        // Время соответствующее координате X
      double   level  =0.0;         // Уровень (цена) соответствующий координате Y
      int      rel_y  =0;           // Для определения относительной координаты Y
      //--- Получим местоположение курсора
      if(!::ChartXYToTimePrice(m_chart_id,x,y,subwin,time,level))
         return;
      //--- Получим относительную координату Y
      rel_y=YToRelative(y);
      //--- Проверим и запомним состояние кнопки мыши
      CheckMouseButtonState(x,rel_y,sparam);
      //--- Проверка фокуса мыши
      CheckMouseFocus(x,rel_y,subwin);
      //--- Установим состояние графика
      SetChartState(subwin);
      //--- Если управление передано окну, определим ее положение
      if(m_clamping_area_mouse==PRESSED_INSIDE_HEADER)
        {
         //--- Обновление координат окна
         UpdateWindowXY(x,rel_y);
        }
      return;
     }
  }

Осталось реализовать метод CWindow::Moving(), чтобы уже можно было протестировать перемещение формы. Можно подумать, что этот метод будет использоваться во внутреннем обработчике класса сразу после метода UpdateWindowXY(), но это не так. На самом деле, если так сделать сейчас, когда на форме нет других элементов управления, то результат будет отличным. Следуя этой логике, далее вы придете к тому, что и в классах других элементов управления нужно делать так же. В итоге, когда на форме будет множество элементов управления, при перемещении формы все элементы будут перемещаться вслед за ней с некоторой задержкой. А вот это уже будет очень плохой результат.

Дело в том, что если каждый метод для перемещения элемента интерфейса делать во внутреннем обработчике, то все их (элементы) будет разделять множество различных условий и проверок. Именно из-за этого будет появляться задержка. Для того чтобы все элементы управления перемещались синхронно, между использованием методов Moving() каждого из них не должно быть никаких других операций. Все это можно реализовать в классе CWndEvents, так как являясь производным классом от CWndContainer, он имеет прямой доступ ко всем указателям на объекты, которые будут там сохраняться.

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

//+------------------------------------------------------------------+
//| Перемещение окна                                                 |
//+------------------------------------------------------------------+
void CWindow::Moving(const int x,const int y)
  {
//--- Сохранение координат в переменных
   m_bg.X(x);
   m_bg.Y(y);
   m_caption_bg.X(x);
   m_caption_bg.Y(y);
   m_icon.X(x+m_icon.XGap());
   m_icon.Y(y+m_icon.YGap());
   m_label.X(x+m_label.XGap());
   m_label.Y(y+m_label.YGap());
   m_button_close.X(x+m_button_close.XGap());
   m_button_close.Y(y+m_button_close.YGap());
   m_button_unroll.X(x+m_button_unroll.XGap());
   m_button_unroll.Y(y+m_button_unroll.YGap());
   m_button_rollup.X(x+m_button_rollup.XGap());
   m_button_rollup.Y(y+m_button_rollup.YGap());
   m_button_tooltip.X(x+m_button_tooltip.XGap());
   m_button_tooltip.Y(y+m_button_tooltip.YGap());
//--- Обновление координат графических объектов
   m_bg.X_Distance(m_bg.X());
   m_bg.Y_Distance(m_bg.Y());
   m_caption_bg.X_Distance(m_caption_bg.X());
   m_caption_bg.Y_Distance(m_caption_bg.Y());
   m_icon.X_Distance(m_icon.X());
   m_icon.Y_Distance(m_icon.Y());
   m_label.X_Distance(m_label.X());
   m_label.Y_Distance(m_label.Y());
   m_button_close.X_Distance(m_button_close.X());
   m_button_close.Y_Distance(m_button_close.Y());
   m_button_unroll.X_Distance(m_button_unroll.X());
   m_button_unroll.Y_Distance(m_button_unroll.Y());
   m_button_rollup.X_Distance(m_button_rollup.X());
   m_button_rollup.Y_Distance(m_button_rollup.Y());
   m_button_tooltip.X_Distance(m_button_tooltip.X());
   m_button_tooltip.Y_Distance(m_button_tooltip.Y());
  }

 

Тест перемещения формы на графике

Обработку всех событий нужно сделать в методе CWndEvents::ChartEvent(), который был создан ранее и на текущий момент пуст. Вначале будет осуществляться проверка на размер массива указателей окон. Если он пуст, то нет смысла продолжать работу метода и программа выйдет из него (return). Далее будет производиться инициализация полей класса, которые относятся к параметрам событий графика. И разместим пока две функции обработки событий: (1) для проверки событий в обработчиках каждого элемента управления CWndEvents::CheckElementsEvents() и (2) для отслеживания курсора мыши CWndEvents::ChartEventMouseMove().

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

//+------------------------------------------------------------------+
//| Обработка событий программы                                      |
//+------------------------------------------------------------------+
void CWndEvents::ChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Если массив пуст, выйдем
   if(CWndContainer::WindowsTotal()<1)
      return;
//--- Инициализация полей параметров событий
   InitChartEventsParams(id,lparam,dparam,sparam);
//--- Проверка событий элементов интерфейса
   CheckElementsEvents();
//--- Событие перемещения мыши
   ChartEventMouseMove();
  }

Наполним методы CWndEvents::CheckElementsEvents() и CWndEvents::ChartEventMouseMove(), так как они на данный момент пусты. Проверка событий элементов интерфейса производится в одном цикле, где последовательно вызываются обработчики OnEvent() всех элементов управления, которые находятся в базе. Так как на текущий момент в нашем тестовом файле в базу добавлено только одно окно, то временно в классе CWndEvents будет обращение к нулевому индексу массивов окон. Но это нужно будет изменить, когда перейдем к разработке многооконного режима.

//+------------------------------------------------------------------+
//| Проверка событий элементов управления                            |
//+------------------------------------------------------------------+
void CWndEvents::CheckElementsEvents(void)
  {
   int elements_total=CWndContainer::ElementsTotal(0);
   for(int e=0; e<elements_total; e++)
      m_wnd[0].m_elements[e].OnEvent(m_id,m_lparam,m_dparam,m_sparam);
  }

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

class CWndEvents : public CWndContainer
  {
private:
   //--- Перемещение окна
   void              MovingWindow(void);
  };
//+------------------------------------------------------------------+
//| Перемещение окна                                                 |
//+------------------------------------------------------------------+
void CWndEvents::MovingWindow(void)
  {
//--- Перемещение окна
   int x=m_windows[0].X();
   int y=m_windows[0].Y();
   m_windows[0].Moving(x,y);
//--- Перемещение элементов управления
   int elements_total=CWndContainer::ElementsTotal(0);
   for(int e=0; e<elements_total; e++)
      m_wnd[0].m_elements[e].Moving(x,y);
  }

И теперь в метод CWndEvents::ChartEventMouseMove() можно добавить код, как это показано ниже. Не забывайте перерисовывать график каждый раз после обновления координат объекта на графике, иначе вы не увидите изменений.

//+------------------------------------------------------------------+
//| Событие CHARTEVENT MOUSE MOVE                                    |
//+------------------------------------------------------------------+
void CWndEvents::ChartEventMouseMove(void)
  {
//--- Выйти, если это не событие перемещения курсора
   if(m_id!=CHARTEVENT_MOUSE_MOVE)
      return;
//--- Перемещение окна
   MovingWindow();
//--- Перерисуем график
   m_chart.Redraw();
  }

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

После теста этот код нужно удалить.

::Comment("x: ",x,"\n",
                "y: ",y,"\n",
                "rel_y: ",rel_y,"\n",
                "w.x: ",m_x,"\n",
                "w.y: ",m_y,"\n",
                "subwin: ",subwin,"\n",
                "m_subwin: ",m_subwin,"\n",
                "clamping mode: ",m_clamping_area_mouse);

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

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

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

//+------------------------------------------------------------------+
//| Создает форму для элементов управления                           |
//+------------------------------------------------------------------+
bool CProgram::CreateWindow(const string caption_text)
  {
//--- Добавим указатель окна в массив окон
   CWndContainer::AddWindow(m_window);
//--- Координаты
   int x=1;
   int y=1;
//--- Свойства
   m_window.Movable(true);
   m_window.XSize(200);
   m_window.YSize(200);
//--- Создание формы
   if(!m_window.CreateWindow(m_chart_id,m_subwin,caption_text,x,y))
      return(false);
//---
   return(true);
  }

Теперь не должно быть причин, которые бы помешали переместить форму:

Рис. 1. Тест перемещения формы на графике.

Рис. 1. Тест перемещения формы на графике

Бывает так, что нужно изменить размер окна графика, и в такой момент генерируется событие изменения свойств графика CHARTEVENT_CHART_CHANGE. Сейчас у нас это никак не отслеживается, поэтому можно попасть в ситуацию, когда форма частично или даже полностью выйдет за границы окна графика. Чтобы этого не произошло, в обработчике событий графика CWindow::OnEvent() нужно проверять и этот тип событий.

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

//+------------------------------------------------------------------+
//| Обработчик событий графика                                       |
//+------------------------------------------------------------------+
void CWindow::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Событие изменения свойств графика
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      //--- Если кнопка отжата
      if(m_clamping_area_mouse==NOT_PRESSED)
        {
         //--- Получим размеры окна графика
         SetWindowProperties();
         //--- Корректировка координат
         UpdateWindowXY(m_x,m_y);
        }
      return;
     }
  }

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

 

Изменение внешнего вида элементов интерфейса при наведении курсора

Ранее, когда рассматривалась реализация класса CElement, который является базовым классом для всех элементов управления, в качестве одного из его членов был создан метод CElement::ChangeObjectColor() для изменения цвета объекта при наведении курсора. Пришло время создать механизм, который позволит использовать это в своей работе. Для того чтобы включить такой функционал, понадобится таймер. По умолчанию он отключен в настройках MQL-приложения, и разработчик этого приложения сам решает, включать ли его в зависимости от того, какие задачи перед ним поставлены.

Для включения таймера язык MQL предоставляет две функции с разной частотой: EventSetTimer() и EventSetMillisecondTimer(). Первая позволяет установить интервал не менее одной секунды, и для наших задач это не подходит, так как одна секунда — очень большой интервал для изменения внешнего вида элемента управления при наведении курсора. Изменение должно производиться мгновенно и без задержек. Поэтому воспользуемся функцией EventSetMillisecondTimer(), которая позволяет установить таймер с интервалами, измеряющимися в миллисекундах. В справочнике MQL написано, что минимальный интервал, который можно установить с помощью этой функции: 10-16 миллисекунд. Этого вполне достаточно для реализации задуманного.

Добавим в файл Defines.mqh константу с шагом таймера, который необходим для работы библиотеки:

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- Шаг таймера (миллисекунды)
#define TIMER_STEP_MSC (16)

Вполне логичны вопросы: «Не слишком ли это часто?» и «Не будет ли программа потреблять слишком много ресурсов процессора?». К ответам на эти вопросы вернемся, когда будем тестировать задуманную идею.

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

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CWndEvents::CWndEvents(void)
  {
//--- Включим таймер
   if(!::MQLInfoInteger(MQL_TESTER))
      ::EventSetMillisecondTimer(TIMER_STEP_MSC);
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CWndEvents::~CWndEvents(void)
  {
//--- Удалить таймер
   ::EventKillTimer();
  }

В конструкторе класса CWndEvents таймер будет включаться при условии, что программа находится вне тестера стратегий. В тестере наша библиотека пока работать не будет, так как в нем на текущий момент есть некоторые ограничения в работе с графическими объектами. К тому же в тестере минимальный интервал для таймера равен одной секунде, даже если вы используете функцию EventSetMillisecondTimer(). В деструкторе осуществляется отключение таймера с помощью функции EventKillTimer().

В классе каждого элемента управления будет своя реализация метода OnEventTimer(), и для этого в классе CElement уже есть виртуальный метод с таким названием. Подобно тому как реализован метод CWndEvents::CheckElementsEvents(), где в цикле производится проход по методам OnEvent() каждого элемента управления, нужно создать метод, в котором также в цикле программа будет проходить по методам OnEventTimer(). Назовем его CheckElementsEventsTimer().

Объявление и реализация метода CWndEvents::CheckElementsEventsTimer():

class CWndEvents : public CWndContainer
  {
private:
   //--- Проверка событий всех элементов по таймеру
   void              CheckElementsEventsTimer(void);
  };
//+------------------------------------------------------------------+
//| Проверка событий всех элементов по таймеру                       |
//+------------------------------------------------------------------+
void CWndEvents::CheckElementsEventsTimer(void)
  {
   int elements_total=CWndContainer::ElementsTotal(0);
   for(int e=0; e<elements_total; e++)
      m_wnd[0].m_elements[e].OnEventTimer();
  }

Далее этот метод нужно вызвать в методе CWndEvents::OnTimerEvent() с проверкой на размер массива окон в самом начале, подобно тому как это было сделано в методе CWndEvents::ChartEvent().

//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void CWndEvents::OnTimerEvent(void)
  {
//--- Если массив пуст, выйдем  
   if(CWndContainer::WindowsTotal()<1)
      return;
//--- Проверка событий всех элементов по таймеру
   CheckElementsEventsTimer();
//--- Перерисуем график
   m_chart.Redraw();
  }

Теперь в классе CProgram, где разработчик MQL-приложения будет создавать графический интерфейс, в методе CProgram::OnTimerEvent(), который связан с главным файлом программы, нужно просто вызвать одноименный метод из базового класса, как это показано в коде ниже:

//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void CProgram::OnTimerEvent(void)
  {
   CWndEvents::OnTimerEvent();
  }

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

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

Метод для изменения цвета объектов назовем CWindow::ChangeObjectsColor(), ниже представлено его объявление и реализация в классе CWindow:

class CWindow: public CElement
  {
private:
   //--- Изменение цвета объектов формы
   void              ChangeObjectsColor(void);
  };
//+------------------------------------------------------------------+
//| Изменение цвета объекта при наведении курсора                    |
//+------------------------------------------------------------------+
void CWindow::ChangeObjectsColor(void)
  {
//--- Изменение картинки в кнопках
   m_button_rollup.State(m_button_rollup.MouseFocus());
   m_button_unroll.State(m_button_unroll.MouseFocus());
   m_button_close.State(m_button_close.MouseFocus());
//--- Изменение цвета в заголовке
   CElement::ChangeObjectColor(m_caption_bg.Name(),CElement::MouseFocus(),OBJPROP_BGCOLOR,
                               m_caption_bg_color,m_caption_bg_color_hover,m_caption_color_bg_array);
  }

Как видно в листинге кода выше, в методе CWindow::ChangeObjectsColor() нет ничего сложного. Методы MouseFocus() возвращают фокус курсора над всеми объектами, который проверяется в методе CWindow::CheckMouseFocus() при перемещении курсора на графике. В объекты-картинки, которые выступают здесь в роли кнопок, ранее было загружено по два изображения. Их можно переключать, устанавливая состояние с помощью метода CChartObjectBmpLabel::State(). А метод CElement::ChangeObjectColor() работает с цветом указанной части объекта.

В данном случае нужно, чтобы при попадании курсора в область окна изменялся цвет фона заголовка. Но также можно изменить цвет рамки заголовка или фон и рамку одновременно или даже цвета других объектов, последовательно вызывая метод CElement::ChangeObjectColor() и указывая для каждого из них в первом параметре имя объекта.

Осталось разместить вызов этого метода в локальном таймере:

//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void CWindow::OnEventTimer(void)
  {
//--- Изменение цвета объектов формы
   ChangeObjectsColor();
  }

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

Рис. 2. Тест на реакцию объектов при наведении курсора мыши.

Рис. 2. Тест на реакцию объектов при наведении курсора мыши

 

Заключение

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

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

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