English 中文 Español Deutsch 日本語 Português
Графические интерфейсы I: Подготовка структуры библиотеки (Глава 1)

Графические интерфейсы I: Подготовка структуры библиотеки (Глава 1)

MetaTrader 5Примеры | 10 декабря 2015, 13:07
15 217 41
Anatoli Kazharski
Anatoli Kazharski

Содержание

 

Введение

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

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

Также стоит упомянуть библиотеку кода Дмитрия Федосеева, с которой он поделился в серии своих статей:

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

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

Ранее я написал две простые вводные статьи, которые относятся к элементам управления:

Они выполнены в процедурном стиле и больше служат для ознакомления с языком MQL. Теперь пришло время продемонстрировать более сложную структуру на примере довольно большого проекта, который будет реализован в объектно-ориентированном виде.

Что получит читатель при прочтении этой серии статей?

  • Цель проекта — дать возможность создавать максимально интуитивно понятные интерфейсы для конечного пользователя. А тем, кто будет эти интерфейсы создавать — максимально понятную в изучении и использовании библиотеку кода, которую можно будет развивать дальше.
  • Те разработчики, которые только начинают делать первые шаги в реализации больших проектов с использованием объектно-ориентированных методов либо только начинают изучать объектно-ориентированное программирование (ООП), получат конкретный материал по этой теме для изучения с множеством примеров от начала до реализации.
  • Более опытные получат еще одну реализацию библиотеки для построения графических интерфейсов и смогут сразу приступить к реализации своих идей. Хорошо, когда есть из чего выбирать.
  • Профессионалы, которые сами могут создать и наверняка уже имеют свои подобные библиотеки, получат возможность покритиковать предложенную реализацию и, возможно, предложить более правильный на их взгляд подход в реализации подобных проектов, что будет также интересно менее опытным читателям. Подобные дискуссии иногда не менее интересны, чем сама статья.

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

 

Список элементов управления

Итак. Какой же должна быть эта библиотека? Какой должна быть ООП-структура кода этой библиотеки? С чего начать? Вопросов, на самом деле, будет еще довольно много, но для начала давайте определимся, какие элементы управления и элементы интерфейса необходимы для создания удобного в использовании MQL-приложения. У каждого есть какие-то свои потребности, и масштаб идей у всех тоже разный. Одним достаточно пары кнопок и чек-боксов, а другим нужны многооконные интерфейсы с возможностью разного рода выбора из набора данных и их управлением.

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

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

  • Форма или окно (window), к которому можно присоединять множество различных элементов управления в любом расположении и любой комбинации.
  • Главное меню (menu bar) с выпадающими списками.
  • Контекстное меню (context menu).
  • Строка состояния (status bar).
  • Кнопки:
    • Обычная кнопка (simple button).
    • Кнопка с расширенными возможностями (icon button).
    • Сдвоенная кнопка с несколькими функциями (split button).
  • Группы кнопок:
    • Группа простых кнопок (buttons group).
    • Группа радио-кнопок (radio buttons).
    • Группа кнопок с расширенными возможностями (icon buttons group).
  • Чекбокс (checkbox).
  • Поле ввода с переключателями (spin edit).
  • Чекбокс с полем ввода с переключателями (checkbox edit).
  • Полоса прокрутки (scroll):
    • Вертикальный скролл.
    • Горизонтальный скролл.
  • Списки (list view).
  • Комбобокс (combobox) с выпадающим списком (list view).
  • Комбобокс с чекбоксом (check combobox) и выпадающим списком (list view).
  • Комбобокс с выпадающим списком и полем ввода (combobox field).
  • Ползунок (slider) с полем ввода:
    • Односторонний.
    • Двухсторонний (dual slider).
  • Календарь:
    • Статический календарь (calendar).
    • Выпадающий календарь (drop calendar).
  • Всплывающая подсказка (tooltip).
  • Индикатор процесса (progress bar).
  • Таблицы:
    • Таблица из текстовых меток (labels table).
    • Таблица из полей ввода (table).
    • Таблица нарисованная на холсте (canvas table).
  • Вкладки (tabs).
  • Иерархический или древовидный список (tree view).

В представленном выше списке есть элементы управления, которые для своего функционирования вмещают в себя другие элементы из этого же списка. Например, в перечислении выше видно, что элементы управления типа комбобокс включают в себя элемент «список» (list view), а «список» (list view) в свою очередь включает в себя вертикальную полосу прокрутки (scroll). Горизонтальная и вертикальная полосы прокрутки включаются также во все варианты таблиц. Выпадающий календарь (drop calendar) содержит в себе уже готовый элемент «календарь» (calendar), который может использоваться как отдельный элемент управления. Создание каждого элемента будет подробно рассматриваться после того, как определимся со структурой проекта.

 

Базовые классы стандартной библиотеки в качестве объектов-примитивов

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

Расположение файлов с классами для работы с графическими примитивами:

  • MetaTrader 4: <каталог данных>\MQL4\Include\ChartObjects
  • MetaTrader 5: <каталог данных>\MQL5\Include\ChartObjects

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

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

Рис. 1. Общая структура взаимосвязей между классами графических объектов стандартной библиотеки.

Рис. 1. Общая структура взаимосвязей между классами графических объектов стандартной библиотеки

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

Рис. 2. Сокращенная версия структуры графических объектов стандартной библиотеки.

Рис. 2. Сокращенная версия структуры графических объектов стандартной библиотеки

Для построения графических интерфейсов нам понадобятся только несколько из этих классов:

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

  • Координаты.
  • Размер.
  • Отступ от крайней точки элемента, частью которого они будут являться.
  • Фокус при наведении курсора мыши на объект.

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

 

Производные классы объектов-примитивов с дополнительными методами

В директории <каталог данных>\MQL5\Include (для MetaTrader 4: <каталог данных>\MQL4\Include) создадим папку EasyAndFastGUI, в которой будем размещать все файлы нашей библиотеки. Чтобы найти каталог данных, в главном меню MetaTrader или MetaEditor нужно выбрать Файл > Открыть каталог данных. В папке EasyAndFastGUI все файлы, которые относятся к библиотеке для создания графических интерфейсов, будут храниться в папке Controls. Далее в папке Controls нужно создать файл Objects.mqh. В нем будут содержаться производные классы, о которых упоминалось выше.

В начале файла Objects.mqh подключим нужные файлы из стандартной библиотеки:

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

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

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

//+------------------------------------------------------------------+
//| Класс с дополнительными свойствами для объекта Rectangle Label   |
//+------------------------------------------------------------------+
class CRectLabel : public CChartObjectRectLabel
  {
protected:
   int               m_x;
   int               m_y;
   int               m_x2;
   int               m_y2;
   int               m_x_gap;
   int               m_y_gap;
   int               m_x_size;
   int               m_y_size;
   bool              m_mouse_focus;
public:
                     CRectLabel(void);
                    ~CRectLabel(void);
   //--- Координаты
   int               X(void)                      { return(m_x);           }
   void              X(const int x)               { m_x=x;                 }
   int               Y(void)                      { return(m_y);           }
   void              Y(const int y)               { m_y=y;                 }
   int               X2(void)                     { return(m_x+m_x_size);  }
   int               Y2(void)                     { return(m_y+m_y_size);  }
   //--- Отступы от крайней точки (xy)
   int               XGap(void)                   { return(m_x_gap);       }
   void              XGap(const int x_gap)        { m_x_gap=x_gap;         }
   int               YGap(void)                   { return(m_y_gap);       }
   void              YGap(const int y_gap)        { m_y_gap=y_gap;         }
   //--- Размеры
   int               XSize(void)                  { return(m_x_size);      }
   void              XSize(const int x_size)      { m_x_size=x_size;       }
   int               YSize(void)                  { return(m_y_size);      }
   void              YSize(const int y_size)      { m_y_size=y_size;       }
   //--- Фокус
   bool              MouseFocus(void)             { return(m_mouse_focus); }
   void              MouseFocus(const bool focus) { m_mouse_focus=focus;   }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CRectLabel::CRectLabel(void) : m_x(0),
                               m_y(0),
                               m_x2(0),
                               m_y2(0),
                               m_x_gap(0),
                               m_y_gap(0),
                               m_x_size(0),
                               m_y_size(0),
                               m_mouse_focus(false)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CRectLabel::~CRectLabel(void)
  {
  }

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

//--- Список классов в файле для быстрого перехода (Alt+G)
class CRectLabel;
class CEdit;
class CLabel;
class CBmpLabel;
class CButton;

Теперь, подобно тому, как это можно делать с функциями и методами, поместив курсор в название класса в этом списке и нажав Alt+G, можно быстро перейти к нужному классу в файле.

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

Рис. 3. Расширение структуры созданием производных классов для объектов-примитивов.

Рис. 3. Расширение структуры созданием производных классов для объектов-примитивов

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

 

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

Класс CElement будет находится в файле Element.mqh, а файл Objects.mqh будет подключаться к нему командой #include:

//+------------------------------------------------------------------+
//|                                                      Element.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Objects.mqh"
//+------------------------------------------------------------------+
//| Базовый класс элемента управления                                |
//+------------------------------------------------------------------+
class CElement
  {
public:
                     CElement(void);
                    ~CElement(void);
  };

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

Нужно отметить, что ни один из классов в файле Objects.mqh не будет являться базовым классом для класса CElement, а также ни один из них не будет фигурировать в качестве включаемых объектов в этом классе. Но зато они будут потом использоваться в качестве объектов во всех производных классах от класса CElement и храниться в базовом классе будут лишь как массив указателей на объекты. Подключив файл Objects.mqh к файлу Element.mqh, нам больше не нужно будет подключать его впоследствии к каким-либо другим файлам. В итоге, вместо того чтобы подключать два файла (Objects.mqh и Element.mqh), нужно будет подключить только один, то есть Element.mqh.

В папке Controls создадим еще один файл, в котором будем хранить в директивах #define некоторые общие свойства для всей программы:

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- Имя класса
#define CLASS_NAME ::StringSubstr(__FUNCTION__,0,::StringFind(__FUNCTION__,"::"))
//--- Имя программы
#define PROGRAM_NAME ::MQLInfoString(MQL_PROGRAM_NAME)
//--- Тип программы
#define PROGRAM_TYPE (ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE)
//--- Предотвращение выхода из диапазона
#define PREVENTING_OUT_OF_RANGE __FUNCTION__," > Предотвращение выхода за пределы массива."

//--- Шрифт
#define FONT      ("Calibri")
#define FONT_SIZE (8)

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

Файл Defines.mqh подключим к файлу Objects.mqh, таким образом, он будет доступен во всей цепочке подключенных друг к другу файлов (Defines.mqh -> Objects.mqh -> Element.mqh) :

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

Теперь нам нужно определить, какими общими свойствами должны обладать все элементы управления. Каждый элемент управления — это отдельный программный модуль, который будет функционировать независимо от всех остальных подобных модулей. Но поскольку некоторые элементы будут собираться в группы, то есть в более сложные (составные) элементы управления, будут возникать ситуации, когда элементы будут отсылать сообщения главному модулю программы, а также другим элементам управления. Поэтому будет возникать необходимость определить, было ли принятое сообщение от элемента управления именно нашей программы, ведь на графике может быть одновременно несколько MQL-приложений.

Также нам понадобится определять:

  • Название элемента управления, которым будет служить имя класса.
  • Тип программы (эксперт, индикатор).
  • Скрыт ли элемент управления в данный момент.
  • Является ли он частью выпадающего элемента.
  • Является ли он элементом, прикрепленным к группе вкладок.
  • Находится ли курсор мыши над элементом управления.
class CElement
  {
protected:
   //--- (1) Имя класса и (2) программы, (3) тип программы
   string            m_class_name;
   string            m_program_name;
   ENUM_PROGRAM_TYPE m_program_type;
   //--- Состояния элемента
   bool              m_is_visible;
   bool              m_is_dropdown;
   int               m_is_object_tabs;
   //--- Фокус
   bool              m_mouse_focus;
   //---
public:
                     CElement(void);
                    ~CElement(void);
   //--- (1) Получения и установка имени класса, (2) получение имени программы, 
   //    (3) получение типа программы
   string            ClassName(void)                    const { return(m_class_name);           }
   void              ClassName(const string class_name)       { m_class_name=class_name;        }
   string            ProgramName(void)                  const { return(m_program_name);         }
   ENUM_PROGRAM_TYPE ProgramType(void)                  const { return(m_program_type);         }
   //--- Состояния элемента
   void              IsVisible(const bool flag)               { m_is_visible=flag;              }
   bool              IsVisible(void)                    const { return(m_is_visible);           }
   void              IsDropdown(const bool flag)              { m_is_dropdown=flag;             }
   bool              IsDropdown(void)                   const { return(m_is_dropdown);          }
   void              IsObjectTabs(const int index)            { m_is_object_tabs=index;         }
   int               IsObjectTabs(void)                 const { return(m_is_object_tabs);       }
   //--- Фокус
   bool              MouseFocus(void)                   const { return(m_mouse_focus);          }
   void              MouseFocus(const bool focus)             { m_mouse_focus=focus;            }
  };

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

class CElement
  {
protected:
   //--- Идентификатор и индекс элемента
   int               m_id;
   int               m_index;
   //---
public:
   //--- Установка и получение идентификатора элемента
   void              Id(const int id)                         { m_id=id;                      }
   int               Id(void)                           const { return(m_id);                 }
   //--- Установка и получение индекса элемента
   void              Index(const int index)                   { m_index=index;                }
   int               Index(void)                        const { return(m_index);              }
  };

Как упоминалось ранее, все объекты графических примитивов элемента управления будут храниться в массиве типа CChartObject как указатели на эти объекты. Поэтому нам понадобится метод, с помощью которого можно будет поместить в массивы указатели на объекты после их успешного создания. Еще нам нужна возможность (1) получить указатель из массива, указав индекс, (2) узнать размер массива объектов и (3) освободить буфер массива.

class CElement
  {
protected:
   //--- Общий массив указателей на все объекты в элементе управления
   CChartObject     *m_objects[];
   //---
public:
   //--- Получение указателя объекта по указанному индексу
   CChartObject     *Object(const int index);
   //--- (1) Получение количества объектов элемента, (2) освобождение массива объектов
   int               ObjectsElementTotal(void)          const { return(::ArraySize(m_objects)); }
   void              FreeObjectsArray(void)                   { ::ArrayFree(m_objects);         }
   //---
protected:
   //--- Метод для добавления указателей объектов-примитивов в общий массив
   void              AddToArray(CChartObject &object);
  };
//+------------------------------------------------------------------+
//| Возвращает указатель объекта элемента по индексу                 |
//+------------------------------------------------------------------+
CChartObject *CElement::Object(const int index)
  {
   int array_size=::ArraySize(m_objects);
//--- Проверка размера массива объектов
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > В этом элементе ("+m_class_name+") нет объектов!");
      return(NULL);
     }
//--- Корректировка в случае выхода из диапазона
   int i=(index>=array_size)? array_size-1 : (index<0)? 0 : index;
//--- Вернуть указатель объекта
   return(m_objects[i]);
  }
//+------------------------------------------------------------------+
//| Добавляет указатель на объект в массив                           |
//+------------------------------------------------------------------+
void CElement::AddToArray(CChartObject &object)
  {
   int size=ObjectsElementTotal();
   ::ArrayResize(m_objects,size+1);
   m_objects[size]=::GetPointer(object);
  }

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

  • Перемещение элемента управления.
  • Показ элемента управления.
  • Скрытие элемента управления.
  • Сброс. Используется, когда нужно, чтобы все объекты, которые относятся к элементу управления, оказались выше тех, которые к нему не относятся.
  • Удаление всех графических объектов элемента управления.
  • Установка значений приоритетов на нажатие левой кнопкой мыши.
  • Обнуление значений приоритетов на нажатие левой кнопкой мыши.
class CElement
  {
public:
   //--- Обработчик событий графика
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) {}
   //--- Таймер
   virtual void      OnEventTimer(void) {}
   //--- Перемещение элемента
   virtual void      Moving(const int x,const int y) {}
   //--- (1) Показ, (2) скрытие, (3) сброс, (4) удаление
   virtual void      Show(void) {}
   virtual void      Hide(void) {}
   virtual void      Reset(void) {}
   virtual void      Delete(void) {}
   //--- (1) Установка, (2) сброс приоритетов на нажатие левой кнопки мыши
   virtual void      SetZorders(void) {}
   virtual void      ResetZorders(void) {}
  };

Вы уже видели, что и в классах графических примитивов в файле Objects.mqh, и в классе CElement есть свойства и методы, которые позволят нам получать границы объекта. Соответственно, будет возможность узнать, находится ли курсор мыши в зоне области всего элемента управления, а также над тем или иным объектом-примитивом в отдельности. Зачем нам это нужно? Это позволит нам сделать графический интерфейс программы максимально интуитивно понятным для пользователя.

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

Метод для расчета градиента можно сделать самому, но мы воспользуемся уже готовым классом кода, который можно скачать из Code Base, многие методы которого еще пригодятся в этом проекте в других классах. Свою версию класса для работы с цветом в базу кода выложил Дмитрий Федосеев (IncColors), но я предлагаю воспользоваться слегка отредактированной мной версией, которую можно загрузить в конце статьи (Colors.mqh).

В этом классе (CColors) много методов на все случаи жизни, и все что я изменил в нем, так это просто добавил возможность быстрой навигации, когда названия методов находятся в теле класса, а сами методы — вне тела класса. Так будет проще и быстрее находить нужный метод и перемещаться от содержания к методу и обратно с помощью клавиш Alt+G. Расположить этот файл нужно в папке EasyAndFastGUI, подключать к нашей библиотеке интерфейсов будем через файл Element.mqh в директории ..\EasyAndFastGUI\Controls. А поскольку этот файл будет находиться в директории на уровень выше, то подключать его нужно будет так, как показано ниже:

//+------------------------------------------------------------------+
//|                                                      Element.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Objects.mqh"
#include "..\Colors.mqh"

Включаем объект класса CColors в класс CElement, а также (1) добавляем переменную и метод для указания количества цветов в градиенте и (2) методы для инициализации массива градиента и изменения цвета указанного объекта:

class CElement
  {
protected:
   //--- Экземпляр класса для работы с цветом
   CColors           m_clr;
   //--- Количество цветов в градиенте
   int               m_gradient_colors_total;
   //---
public:
   //--- Установка размера градиента
   void              GradientColorsTotal(const int total)     { m_gradient_colors_total=total;  }
   //---
protected:
   //--- Инициализация массива градиента
   void              InitColorArray(const color outer_color,const color hover_color,color &color_array[]);
   //--- Изменение цвета объекта
   void              ChangeObjectColor(const string name,const bool mouse_focus,const ENUM_OBJECT_PROPERTY_INTEGER property,
                                       const color outer_color,const color hover_color,const color &color_array[]);
  };

Для инициализации массива градиента воспользуемся методом Gradient() из класса CColors. В этот метод нужно передать в качестве параметров (1) массив цветов, из которых будет рассчитываться градиент, (2) массив, в который будет помещена последовательность цветов градиента, (3) запрашиваемый размер массива, то есть сколько шагов должно быть в градиенте.

//+------------------------------------------------------------------+
//| Инициализация массива градиента                                  |
//+------------------------------------------------------------------+
void CElement::InitColorArray(const color outer_color,const color hover_color,color &color_array[])
  {
//--- Массив цветов градиента
   color colors[2];
   colors[0]=outer_color;
   colors[1]=hover_color;
//--- Формирование массива цветов
   m_clr.Gradient(colors,color_array,m_gradient_colors_total);
  }

В методе для изменения цвета объекта будут параметры, которые позволяют указать:

  • Имя объекта.
  • Находится ли курсор мыши над элементом управления.
  • Цвет какой части объекта нужно изменить (например: фон, рамка и т.д.).
  • Так как у нас градиент будет формироваться из двух цветов, то в двух параметрах для проверки нужно передавать эти цвета.
  • Массив цветов градиента, который формируется в функции InitColorArray().

Дополнительные комментарии смотрите ниже в коде метода ChangeObjectColor():

//+------------------------------------------------------------------+
//| Изменение цвета объекта при наведении курсора                    |
//+------------------------------------------------------------------+
void CElement::ChangeObjectColor(const string name,const bool mouse_focus,const ENUM_OBJECT_PROPERTY_INTEGER property,
                                 const color outer_color,const color hover_color,const color &color_array[])
  {
   if(::ArraySize(color_array)<1)
      return;
//--- Получим текущий цвет объекта
   color current_color=(color)::ObjectGetInteger(m_chart_id,name,property);
//--- Если курсор над объектом
   if(mouse_focus)
     {
      //--- Выйдем, если уже достигли указанного цвета
      if(current_color==hover_color)
         return;
      //--- Идем от первого к последнему
      for(int i=0; i<m_gradient_colors_total; i++)
        {
         //--- Если цвета не совпадают, перейдем у следующему
         if(color_array[i]!=current_color)
            continue;
         //---
         color new_color=(i+1==m_gradient_colors_total)? color_array[i] : color_array[i+1];
         //--- Изменим цвет
         ::ObjectSetInteger(m_chart_id,name,property,new_color);
         break;
        }
     }
//--- Если курсор вне области объекта
   else
     {
      //--- Выйдем, если уже достигли указанного цвета
      if(current_color==outer_color)
         return;
      //--- Идем от последнего к первому
      for(int i=m_gradient_colors_total-1; i>=0; i--)
        {
         //--- Если цвета не совпадают, перейдем у следующему
         if(color_array[i]!=current_color)
            continue;
         //---
         color new_color=(i-1<0)? color_array[i] : color_array[i-1];
         //--- Изменим цвет
         ::ObjectSetInteger(m_chart_id,name,property,new_color);
         break;
        }
     }
  }

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

class CElement
  {
protected:
   //--- Угол графика и точка привязки объектов
   ENUM_BASE_CORNER  m_corner;
   ENUM_ANCHOR_POINT m_anchor;
  }

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

Рис. 4. Подключение класса CColors для работы с цветом.

Рис. 4. Базовый класс для элементов управления CElement

 

Базовые классы для приложения с графическим интерфейсом

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

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

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

В папке Controls нужно создать файлы WndContainer.mqh и WndEvents.mqh. Класс CWndContainer на текущий момент будет совсем пустым, так как мы не создали еще ни одного элемента управления.

//+------------------------------------------------------------------+
//|                                                 WndContainer.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Класс для хранения всех объектов интерфейса                      |
//+------------------------------------------------------------------+
class CWndContainer
  {
protected:
                     CWndContainer(void);
                    ~CWndContainer(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CWndContainer::CWndContainer(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CWndContainer::~CWndContainer(void)
  {
  }
//+------------------------------------------------------------------+

Файл WndContainer.mqh нужно подключить к файлу WndEvents.mqh, так как класс CWndEvents будет производным от класса CWndContainer:

//+------------------------------------------------------------------+
//|                                                    WndEvents.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "WndContainer.mqh"
//+------------------------------------------------------------------+
//| Класс для обработки событий                                      |
//+------------------------------------------------------------------+
class CWndEvents : public CWndContainer
  {
protected:
                     CWndEvents(void);
                    ~CWndEvents(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CWndEvents::CWndEvents(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CWndEvents::~CWndEvents(void)
  {
  }
//+------------------------------------------------------------------+

Классы CWndContainer и CWndEvents будут базовыми для любого приложения на MQL, в котором нужен графический интерфейс.

Для последующих тестов этой библиотеки в процессе ее разработки создадим эксперта. Его нужно создать в отдельной папке, так как кроме главного файла программы будет еще подключаемый файл Program.mqh с классом (CProgram) нашей программы. Этот класс будет производным от класса CWndEvents.

//+------------------------------------------------------------------+
//|                                                      Program.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include <EasyAndFastGUI\Controls\WndEvents.mqh>
//+------------------------------------------------------------------+
//| Класс для создания торговой панели                               |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
public:
                     CProgram(void);
                    ~CProgram(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CProgram::~CProgram(void)
  {
  }
//+------------------------------------------------------------------+

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

class CProgram : public CWndEvents
  {
public:
   //--- Инициализация/деинициализация
   void              OnInitEvent(void);
   void              OnDeinitEvent(const int reason);
   //--- Таймер
   void              OnTimerEvent(void);
   //---
protected:
   //--- Виртуальный обработчик события графика
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
  };

Таймер и обработчик событий графика нужно создать также и на уровне выше в базовом классе CWndEvents:

class CWndEvents : public CWndContainer
  {
protected:
   //--- Таймер
   void              OnTimerEvent(void);
   //--- Виртуальный обработчик события графика
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) {}
  };

Обратите внимание, что в двух листингах кода выше и в базовом классе CWndEvents, и в производном классе CProgram обработчик событий графика (метод OnEvent) объявлен как виртуальный (virtual). При этом в классе CWndEvents этот метод представляет из себя пустышку “{}”. Это позволит нам перенаправлять поток событий из базового класса в производный, когда это будет необходимо. Виртуальные методы OnEvent() в этих классах предназначены для внутреннего использования. Для вызова в главном файле программы будет использоваться другой метод класса CWndEvents. Назовем его ChartEvent(). И создадим также вспомогательные методы для каждого типа основных событий, которые позволят сделать код более понятным и читаемым.

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

class CWndEvents : public CWndContainer
  {
public:
   //--- Обработчики событий графика
   void              ChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   //---
private:
   void              ChartEventCustom(void);
   void              ChartEventClick(void);
   void              ChartEventMouseMove(void);
   void              ChartEventObjectClick(void);
   void              ChartEventEndEdit(void);
   void              ChartEventChartChange(void);
   //--- Проверка событий в элементах управления
   void              CheckElementsEvents(void);
  };

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

class CWndEvents : public CWndContainer
  {
private:
   //--- Параметры событий
   int               m_id;
   long              m_lparam;
   double            m_dparam;
   string            m_sparam;
   //--- Инициализация параметров событий
   void              InitChartEventsParams(const int id,const long lparam,const double dparam,const string sparam);
  };
//+------------------------------------------------------------------+
//| Инициализация событийных переменных                              |
//+------------------------------------------------------------------+
void CWndEvents::InitChartEventsParams(const int id,const long lparam,const double dparam,const string sparam)
  {
   m_id     =id;
   m_lparam =lparam;
   m_dparam =dparam;
   m_sparam =sparam;
  }

Теперь в главном файле программы (1) подключим файл с классом CProgram, (2) создадим его экземпляр и (3) свяжем с главными функциями программы:

//+------------------------------------------------------------------+
//|                                                  TestLibrary.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2015, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
//--- Подключение класса торговой панели
#include "Program.mqh"
CProgram program;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(void)
  {
   program.OnInitEvent();
//--- Инициализация прошла успешно
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   program.OnDeinitEvent(reason);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(void)
  {
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer(void)
  {
   program.OnTimerEvent();
  }
//+------------------------------------------------------------------+
//| Trade function                                                   |
//+------------------------------------------------------------------+
void OnTrade(void)
  {
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int    id,
                  const long   &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   program.ChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+

Если нужно, то в классе CProgram можно создать методы и для других обработчиков событий, таких как OnTick(), OnTrade() и т.д.

 

Тест обработчиков событий библиотеки и класса приложения

Ранее упоминалось, что виртуальный метод OnEvent() класса CProgram можно вызвать из базового класса CWndEvents в методе ChartEvent(). Нужно убедиться, что это работает, и сейчас мы уже можем протестировать этот механизм. Для этого в методе CWndEvents::ChartEvent() вызовите метод CProgram::OnEvent() так, как показано ниже:

//+------------------------------------------------------------------+
//| Обработка событий программы                                      |
//+------------------------------------------------------------------+
void CWndEvents::ChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   OnEvent(id,lparam,dparam,sparam);
  }

Далее в методе CProgram::OnEvent() напишите код, который представлен ниже:

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_CLICK)
     {
      ::Comment("x: ",lparam,"; y: ",(int)dparam);
     }
  }

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

 

Заключение

Подведем промежуточный итог, изобразив все описанное выше в виде схемы:

Рис. 5. Включение в проект классов для хранения указателей и обработки событий.

Рис. 5. Включение в проект классов для хранения указателей и обработки событий

На текущий момент схема состоит из двух не связанных между собой частей. Для того чтобы настроить между ними связь, нужно в первую очередь создать главный элемент интерфейса. Главным элементом является форма или окно (window), к которому будут присоединяться все остальные элементы управления. Поэтому далее мы приступим к написанию такого класса и назовем его CWindow. К файлу Window.mqh подключим файл с классом Element.mqh, так как класс CElement будет базовым для класса CWindow.

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

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (41)
Anatoli Kazharski
Anatoli Kazharski | 6 мар. 2017 в 10:24
Stanislav Korotky:
Сейчас последняя статья переведена на Испанский, и там приложен код с русскими комментариями.
Забыли наверное код поменять. Смотрел только что файлы для японцев, так там комментарии на английском. 
Stanislav Korotky
Stanislav Korotky | 6 мар. 2017 в 11:00
Подскажите, плиз, есть ли возможность устанавливать "резиновость" элементов и их групп? Например, как сделать вверху окна панель с контролами с фиксированной высотой, а все остальное место должен занимать другой элемент (например, таблица, график и т.д.).
Anatoli Kazharski
Anatoli Kazharski | 6 мар. 2017 в 11:26
Stanislav Korotky:
Подскажите, плиз, есть ли возможность устанавливать "резиновость" элементов и их групп? Например, как сделать вверху окна панель с контролами с фиксированной высотой, а все остальное место должен занимать другой элемент (например, таблица, график и т.д.).

Вот в этих статьях можно посмотреть примеры реализации:

/‌/---

/‌/---

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

Maksxs
Maksxs | 25 окт. 2019 в 22:54

Приветствую Anatoli Kazharski. У вас в файле Element.mqh в функции ChangeObjectColor используется объект m_chart_id. Но связь как я понимаю теряется в файле Objects.mqh. У меня при компиляции выдает ошибку "m_chart_id - undeclared identifier". В статье я по этому поводу (m_chart_id) ни чего не нашел. Заранее благодарю за ответ.


P.S. Я только изучаю ООП. Разбираюсь по вашим примерам. Спасибо Вам за проделанную работу.

Vladimir Karputov
Vladimir Karputov | 3 нояб. 2019 в 08:27
Maksxs:

Приветствую Anatoli Kazharski. У вас в файле Element.mqh в функции ChangeObjectColor используется объект m_chart_id. Но связь как я понимаю теряется в файле Objects.mqh. У меня при компиляции выдает ошибку "m_chart_id - undeclared identifier". В статье я по этому поводу (m_chart_id) ни чего не нашел. Заранее благодарю за ответ.


P.S. Я только изучаю ООП. Разбираюсь по вашим примерам. Спасибо Вам за проделанную работу.

Странно. Только что скачал "EasyAndFastGUI_MQL5.zip" и скомпилировал

  • файл [data folder]\MQL5\Experts\Article01\TestLibrary.mq5 - нет ошибок
  • файл [data folder]\\MQL5\Include\EasyAndFastGUI\Controls\Element.mqh - нет ошибок
Графические интерфейсы I: Форма для элементов управления (Глава 2) Графические интерфейсы I: Форма для элементов управления (Глава 2)
В этой статье создадим первый и самый главный элемент графических интерфейсов — форму для элементов управления. К этой форме можно будет присоединять множество различных элементов управления в любом расположении и в любых комбинациях.
Изучаем класс CCanvas. Сглаживание и тени Изучаем класс CCanvas. Сглаживание и тени
Алгоритм сглаживания класса CCanvas — основа всех построений, в которых используется сглаживание. В статье рассказано о том, как работает этот алгоритм, приведены примеры визуализации его работы. Кроме того, рассмотрено рисование теней графических объектов и разработан подробный алгоритм отрисовки тени на канвасе. Для расчетов применена библиотека численного анализа ALGLIB.
Графические интерфейсы I: "Оживление" графического интерфейса (Глава 3) Графические интерфейсы I: "Оживление" графического интерфейса (Глава 3)
В предыдущей статье серии был начат процесс разработки класса формы для элементов управления. В этой статье продолжим развивать класс, наполняя его методами для перемещения формы в области графика, а также интегрируем этот элемент интерфейса в ядро библиотеки. Кроме этого, настроим всё таким образом, чтобы при наведении курсора на элементы формы изменялся их цвет.
Модуль торговых сигналов по системе Билла Вильямса Модуль торговых сигналов по системе Билла Вильямса
В статье описываются правила торговой системы Билла Вильямса, порядок использования разработанного MQL5-модуля для поиска и разметки на графике паттернов данной системы, автоматической торговли по найденным паттернам, а также представлены результаты тестирования на различных торговых инструментах.