Пользовательские графические элементы управления. Часть 1. Создание простого элемента управления

Dmitry Fedoseev | 11 июля, 2011


Введение

Язык MQL5 обеспечивает разработчика большим набором программно управляемых графических объектов: кнопка, текстовая метка, поле ввода, графическая метка (рис. 1), различные графические аналитические инструменты (рис. 2).


Рис. 1. Графические объекты: кнопка, текстовая метка, поле ввода, графическая метка


Рис 2. Некоторые графические аналитические инструменты: эллипс, веер Фибоначчи, расширение Фибоначчи

Всего в терминале MetaTrader 5 насчитывается более сорока графических объектов. Все эти объекты могут использоваться по отдельности, самостоятельно, но, чаще всего, используется несколько взаимосвязанных графических объектов. Например, при использовании поля ввода (OBJ_EDIT), почти всегда используются объект текстовая метка (OBJ_LABEL), в котором указывается назначение поля ввода.

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

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

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

 
Рис. 3. Форма с наиболее распространенными стандартными элементами управления

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

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

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


1. Каким должен быть графический элемент управления

1.1. Общие требования и принципы

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

  1. Должна обеспечиваться возможность быстрого создания элемента управления на этапе разработки. Эта задача решается использованием объектно-ориентированного программирования. Один графический элемент управления оформляется в виде одного программного объекта.
     
  2. Элемент управления должен быть гибким, т.е. должна быть возможность изменения его свойств: размеров, положения, цвета и т.п.
     
  3. Элемент управления должен быть прост в использовании - должен быть наделен только необходимым, но достаточным набором свойств и методов, назначение которых должно быть очевидно из назначения элемента управления и названий методов. Здесь разделим свойства элементов управления на несколько категорий:
     
    1. Неуправляемые свойства. К этим свойствам отнесем цветовую схему. Все элементы управления, используемые в одной программе должны иметь общий стиль оформления, поэтому установка цветов каждому создаваемому элементу управления по отдельности будет довольно утомительным занятием.
      К тому же, подбор цветов для некоторых элементов управления является довольно сложной задачей, на которую не хотелось бы отвлекаться на этапе разработки программы. Например, полоса прокрутки, некоторые вебмастера могли сталкиваться с такой интересной задачей.
       
    2. Свойства, устанавливаемые только на этапе создания элемента управления или редко изменяемые свойства. Например, размеры элемента управления. Гармоничное и удобное расположение всех элементов управления используемых в программе, это отдельная творческая и довольно сложная задача решаемая на этапе конструирования интерфейса.
      В связи с этим, размеры элементов управления, как правило, не меняются в процессе работы программы. Однако, в некоторых случаях, может потребоваться менять и такие свойства, поэтому должна обеспечиваться возможность изменять эти свойства в процессе работы программы.
       
    3. Основные рабочие свойства. Свойства, которые будут наиболее часто меняться программно и для работы с которыми, собственно, и предназначен элемент управления. Здесь свойства можно разделить на две категории:
       
      1. Свойства с автоматическим обновлением отображения элемента управления. Например, поле ввода. После программной установки значения, желательно, чтобы оно сразу отображалось на экране, что бы при программировании ограничиться только одной строкой кода.
         
      2. Свойства требующие принудительного обновления отображения. Например, список. Списки предполагают работу с массивами данных, поэтому, не желательно автоматическое обновление вида списка после работы с одним элементом списка. Здесь лучше выполнять принудительное обновление после завершения всей работы с его элементами, что значительно повысит быстродействие программы.
         
  4. Должна обеспечиваться возможность простого и быстрого скрытия и отображения элемента управления. При включении видимости элемента управления не должно требоваться снова устанавливать ему свойства отображения, доступ к свойствам объекта должен быть независим от видимости графических объектов этого элемента управления, т.е. программный объект должен содержать все свойства элемента управления в себе, а не пользоваться свойствами графических объектов.
      
  5. Должна быть организована обработка событий отдельных графических объектов, входящих в состав элемента управления, так, чтобы на выходе иметь соответствующие элементу управления события.

1.2. Порядок использования элемента управления и обязательные методы 

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

  1. Инициализация элемента управления с одновременной установкой ему редко изменяемых свойств. Этот метод будет иметь имя Init(), у метода будет несколько параметров: 1-ый обязательный - имя элемента управления, этот параметр будет являться приставкой для имен всех графических объектов входящих в состав элемента управления, затем, при необходимости, параметры определяющие размеры элемента управления и прочие параметры (в зависимости от назначения элемента управления).
     
  2. Элементы управления могут находиться на графике на постоянных местах, а может потребоваться обеспечить возможность их перемещения, поэтому, для определения координат элементов управления будут использоваться отдельные методы: SetPosLeft() - установка координаты X, SetPosTop() - установка координаты Y, Эти методы должны иметь по одному параметру. Очень часто приходиться одновременно менять обе координаты, поэтому будет полезен метод SetPos() с двумя параметрами, позволяющий одновременно установить координаты X и Y.
    Для вычисления расположения одного элемента управления может потребоваться получить информацию о размерах и расположении другого элемента управления, для этого будут использоваться методы: Width() - ширина, Height() - высота, Left() - координата X, Top() - координата Y. На этом шаге работы вычисляются координаты элемента управления и вызываются методы для их установки.
     
  3. Сразу после создания или на каком-то этапе работы программы потребуется включить видимость элемента управления. Для этого будет использоваться метод Show(). Для скрытия элемента управления будет использоваться метод Hide().
     
  4. Как уже упоминалось ранее, в процессе работы программы может потребоваться изменение размеров элемента управления, поэтому у программного объекта должны быть отдельные методы для установки размеров SetWidth() и/или SetHeght(). Поскольку это редко изменяемые свойства, для того, чтобы их изменения вступили в действие, потребуется вызывать метод для обновления отображения - Refresh().

  5. События отдельных графических объектов будут обрабатываться в методе Event(), который будет возвращать значение соответствующее определенному событию элемента управления. Метод Event() будет должен вызываться из функции OnChartEvent() и будет иметь такой же набор параметров как функция OnChartEvent().

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

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

Далее в этой статье попробуем воплотить вышеизложенные принципы на практике - создадим элемент управления для ввода пользователем текста или числа.

Предварительно обеспечим себя средствами быстрой и удобной работы с графическими объектами.


2. Как работать с графическими объектами быстро и удобно

Для работы с графическими объектами в MQL5 существуют следующие основные функции: ObjectCreate(), ObjectDelete(), ObjectSetDouble(), ObjectSetInteger(), ObjectSetString(), ObjectGetDouble(), ObjectGetInteger(), ObjectGetString(). Можно и непосредственно пользоваться этими функциями, однако, процесс программирования при этом будет очень трудоемким и длительным - у функций длинные имена, в функции надо передавать идентификаторы, которые тоже достаточно длинные и их большое количество.

Для того чтобы работа с графическими объектами была более комфортной, можно воспользоваться готовым классом для работы с графическими объектами, входящим в комплект терминала MetaTrader 5 (класс CChartObject в файле MQL5/Include/ChartObjects/ChartObject.mqh) или же написать собственный подобный класс и наделить его всеми желаемыми для себя методами.

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


Рис. 4. Список свойств и методов объекта

Возможно два варианта управления графическим объектом посредством вспомогательного класса:

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

  2. Используется один экземпляр класса. При необходимости управления каким-нибудь графическим объектом, графический объект присоединяется к классу. Будем использовать этот вариант.

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


3. Универсальный класс для управления графическими объектами

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

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

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

По умолчанию оба необязательных параметра будут иметь значения 0 (график цены на "своем" чарте). Смотрим в справочном руководстве список типов графических объектов. Для каждого из этих тиов создадим метод Create.

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

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

Для возможности использования класса по второму варианту (для управления любым графическим объектом), наделим его методом для присоединения графического объекта (метод Attach()), у метода будет один обязательный параметр - имя графического объекта и один необязательный параметр - идентификатор чарта. Не исключено, что может потребоваться узнать имя и идентификатор присоединенного графического объекта, для этого наделим объект методами Name() и ChartID().

Получилась такая "заготовка" класса: 

class CGraphicObjectShell
  {
protected:
   string            m_name;
   long              m_id;
public:
   void Attach(string aName,long aChartID=0)
     {
      m_name=aName;
      m_id=aChartID;
     }
   string Name()
     {
      return(m_name);
     }    
   long ChartID()
     {
      return(m_id);
     }
  };

Добавляем вышеупомянутые методы создания графическим объектов, имена всех этих методов будут начинаться со слова "Create".

Читателям не обязательно все это повторять, в приложении к статье в файле IncGUI.mqh находится полностью готовый класс CGraphicObjectShell.

Для примера, один метод создания графического объекта вертикальная линия (OBJ_VLINE):

void CreateVLine(string aName,int aSubWindow=0,long aChartID=0)
  {
   ObjectCreate(m_id,m_name,OBJ_VLINE,aSubWindow,0,0);
   Attach(aName,aChartID);
  }

Теперь открываем справочное руководство со списком свойств графических объектов и для каждого свойства пишем метод установки значения свойства посредством функций ObjectSetDouble(), ObjectSetInteger(), ObjectSetString(). Имена методов будут начинаться со слова "Set". Затем, пишем методы чтения свойств посредством функций ObjectGetDouble(),ObjectGetInteger(). ObjectGetString().

Для примера по одному методу для установки и получения цвета:

void SetColor(color aColor)
  {
   ObjectSetInteger(m_id,m_name,OBJPROP_COLOR,aColor);
  }
color Color()
  {
   return(ObjectGetInteger(m_id,m_name,OBJPROP_COLOR));
  }

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

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

Наделим класс еще двумя вариантами всех методов установки/чтения свойств.

Один - по имени на "своем" чарте:

void SetColor(string aName,color aColor)
  {
   ObjectSetInteger(0,aName,OBJPROP_COLOR,aColor);
  }
color Color(string aName)
  {
   return(ObjectGetInteger(0,aName,OBJPROP_COLOR));
  }

Второй - по имени и идентификатору чарта:

void SetColor(long aChartID,string aName,color aColor)
  {
   ObjectSetInteger(aChartID,aName,OBJPROP_COLOR,aColor);
  }
color Color(long aChartID,string aName)
  {
   return(ObjectGetInteger(aChartID,aName,OBJPROP_COLOR));
  }

Кроме функций ObjectGet... и ObjectSet... существует еще несколько функций для работы с графическими объектами: ObjectDelete(), ObjectMove(), ObjectFind(), ObjectGetTimeByValue(), ObjectGetValueByTime(), ObjectsTotal(), их тоже можно добавить в класс и также с тремя вариантами вызова каждой функции.

Наконец, в этом же файле, объявим класс CGraphicObjectShell с простым и коротким именем "g". 

CGraphicObjectShell g;

Теперь, для того чтобы начать работать с графическими объектами, достаточно подключить файл IncGUI.mqh и в нашем распоряжении будет класс "g", при помощи которого будет очень легко и просто управлять всеми возможными графическими объектами.


4. Заготовки элементов управления

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

  1. Прямоугольная метка (OBJ_RECTANGLE_LABEL),
  2. Текстовая метка (OBJ_LABEL),
  3. Поле ввода (OBJ_EDIT),
  4. Кнопка (OBJ_BUTTON).

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

Для работы элементов управления потребуется обработка событий чарта, события других чартов не доступны, значит, ограничимся работой только на своем чарте - в параметрах методов класса CWorkPiece не будет параметра идентификатора чарта, везде будет использоваться 0 - "свой" чарт.

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

Основой многих элементов управления будет графический объект прямоугольная метка, добавим в класс CWorkPiece метод для создания этого графического объекта, метод будет называться Canvas():

void Canvas(string aName="Canvas",
             int aSubWindow=0,
             int aLeft=100,
             int aTop=100,
             int aWidth=300,
             int aHeight=150,
             color aColorBg=clrIvory,
             int aColorBorder=clrDimGray)
  {
   g.CreateRectangleLabel(aName,aSubWindow); // Создание прямоугольной метки
   g.SetXDistance(aLeft);                    // Установка координаты X
   g.SetYDistanse(aTop);                        // Установка координаты Y
   g.SetXSize(aWidth);                          // Установка ширины
   g.SetYSize(aHeight);                         // Установка высоты
   g.SetBgColor(aColorBg);                   // Установка цвета фона
   g.SetColor(aColorBorder);                 // Установка цвета рамки
   g.SetCorner(CORNER_LEFT_UPPER);             // Установка точки привязки
   g.SetBorderType(BORDER_FLAT);             // Установка типа рамки
   g.SetTimeFrames(OBJ_ALL_PERIODS);            // Установка видимости на всех таймфреймах
   g.SetSelected(false);                        // Отключения выделения
   g.SetSelectable(false);                   // Отключение возможности выделять объект
   g.SetWidth(1);                               // Установка толщины рамки
   g.SetStyle(STYLE_SOLID);                  // Установка стиля рамки
  }

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

Подобно методу Canvas() напишем методы для создания текстовой метки, кнопки и поля ввода: Label(), Button() и Edit(). В приложении к статье в файле IncGUI.mqh находится полностью готовый класс CWorkPiece. Кроме вышеупомянутых методов, в классе есть еще несколько методов, среди них: Frame() и DeleteFrame() - методы для создания и удаления рамки (рис. 5). Рамка представляет собой графический прямоугольную метку с надписью в левом верхнем углу.

Использование рамок планируется при группировании элементов управления на форме.


Рис. 5. Рамка с надписью. 

В приложении к статье находится список всех методов класса CWorkPiece.

Так же, как и с классом CGraphicObjectShell, заранее объявим класс CWorkPiece с коротким именем "w", чтобы он был готов к использованию сразу после подключения файла IncGUI.mqh.

CWorkPiece w;

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


5. Создание элемента управления "поле ввода"

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

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

Значит, при инициализации элемента управления, ему будет указываться режим работы: текстовый или числовой, указание режима работы будет выполняться параметром aDigits, значение от нуля и выше означает числовой режим с указанным количеством знаков после запятой, отрицательное значение - текстовый режим.

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

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

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

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

Класс будет называться CInputBox. Получается следующий набор переменных класса (располагаются в секции private):

string m_NameEdit;    // Имя графического объекта Edit
string m_NameLabel;   // Имя графического объекта Label
int m_Left;           // Координата Х
int m_Top;            // Координата Y
int m_Width;           // Ширина
int m_Height;          // Высота
bool m_Visible;        // Флаг видимости элемента управления
int m_Digits;          // Количество знаков после запятой у числа double или -1 при работе в текстовом режиме
string m_Caption;      // Надпись
string m_Value;        // Значение
double m_ValueMin;     // Минимальное значение
double m_ValueMax;     // Максимальное значение
color m_BgColor;       // Цвет фона 
color m_TxtColor;     // Цвет текста
color m_LblColor;      // Цвет надписи
color m_WarningColor; // Предупреждающий цвет фона
bool m_Warning;        // Флаг предупреждения
int m_SubWindow;       // Подокно
string m_Tag;           // Тэг

Первый метод, который будет вызываться при использовании элемента управления - метод Init().

В этом методе подготавливаем значения всех ранее определенных переменных:

// Метод инициализации
void Init(string aName="CInputBox",
           int aWidth=50,
           int aDigits=-1,
           string aCaption="CInputBox")
 { 
   m_NameEdit=aName+"_E";  // Подготовка имени текстового поля
   m_NameLabel=aName+"_L"; // Подготовка имени надписи
   m_Left=0;                 // Координата Х
   m_Top=0;                  // Координата Y
   m_Width=aWidth;          // Ширина
   m_Height=15;             // Высота
   m_Visible=false;         // Видимость
   m_Digits=aDigits;       // Режим работы и количество знаков после десятичного разделиеля
   m_Caption=aCaption;     // Текст надписи
   m_Value="";              // Значение в текстовом режиме
   if(aDigits>=0)m_Value=DoubleToString(0,m_Digits); // Значение в числовом режиме
   m_ValueMin=-DBL_MAX;                   // Минимальное значение
   m_ValueMax=DBL_MAX;                  // Максимальное значение
   m_BgColor=ClrScheme.Color(0);       // Цвет фона текстового поля
   m_TxtColor=ClrScheme.Color(1);      // Цвет текста и рамки текстового поля
   m_LblColor=ClrScheme.Color(2);      // Цвет надписи
   m_WarningColor=ClrScheme.Color(3); // Предупреждающий цвет
   m_Warning=false;                      // Режим: предупреждающий, нормальный
   m_SubWindow=0; // Номер подокна
   m_Tag=""; // Тэг
 }

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

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

// Установка координаты X и Y
void SetPos(int aLeft,int aTop)
{ 
   m_Left=aLeft;
   m_Top=aTop;
}

После этого уже можно включить отображение элемента управления (метод Show()):

// Включение отображения на заранее установленной позиции
void Show()
{ 
   m_Visible=true; // Регистрация видимости 
   Create();       // Создание графических объектов
   ChartRedraw();   // Обновление чарта
}

Из метода Show() вызывается функция Create() в которой выполняется создание графических объектов (функция располагается в секции private) и выполняется обновление чарта (ChartRedraw()). Ниже код функции Create():

// Функция создания графических объектов
void Create(){ 
   color m_ctmp=m_BgColor;  // Нормальный цвет фона
      if(m_Warning){ // Установлен режим предупреждения
         m_ctmp=m_WarningColor; // Текстовое поле будет окрашено предупреждающим цветом
      }
    // Создание текстового поля
   w.Edit(m_NameEdit,m_SubWindow,m_Left,m_Top,m_Width,m_Height,m_Value,m_ctmp,m_TxtColor,7,"Arial"); 
      if(m_Caption!=""){ // Есть надпись
          // Создание надписи
         w.Label(m_NameLabel,m_SubWindow,m_Left+m_Width+1,m_Top+2,m_Caption,m_LblColor,7,"Arial"); 
      } 
}   

При создании графических объектов в функции Create(), в зависимости от значения переменной m_Warning, текстовому полю устанавливается соответствующий цвет фона. Если переменная m_Caption имеет значение, создается надпись (можно создавать элемент управления без надписи).

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

// Установка координаты X и Y
void SetPos(int aLeft,int aTop){ 
   m_Left=aLeft;
   m_Top=aTop;
}

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

Для этого используется метод Hide():

// Скрытие (удаление графических объектов)
void Hide()
{ 
   m_Visible=false; // Регистрация состояния невидимости
   Delete();        // Удаление графических объектов
   ChartRedraw();    // Обновление чарта
}  

Метод Hide() выполняет вызов функции Delete() в которой выполняется удаление графических объектов и вызов функции ChartRedraw() для обновления отображения чарта. Функция Delete() располагается в секции private:

// Функция удаления графических объектов
void Delete()
{ 
   ObjectDelete(0,m_NameEdit);  // Удаление текстового поля
   ObjectDelete(0,m_NameLabel); // Удаление надписи
}   

Поскольку уже фигурировал метод, в котором выполнялась только установка значений свойств без изменения отображения элемента управления (метод SetPos()), напрашивается появление метода принудительного обновления элемента управления - метода Refresh(): 

// Обновление отображения (удаление и создание)
void Refresh()
{ 
   if(m_Visible)
   {   // Включена видимость
      Delete();     // Удаление графических объектов
      Create();     // Создание графических объектов
      ChartRedraw(); // Перерисовка чарта 
   }            
}   

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

С расположением элемента управления разобрались, теперь установка значения - метод SetValue(). Поскольку элемент управления может работать в двух режимах, будет два варианта метода SetValue(): с параметром типа string и double. В текстовом режиме значение используется как есть:

// Установка текстового значения
void SetValue(string aValue)
{ 
   m_Value=aValue; // Присвоение значения переменной для хранения значения
      if(m_Visible)
      { // Включена видимость элемента управления
          // Присоединения текстового поля к объекту для управления графическими объектами
         g.Attach(m_NameEdit); 
         g.SetText(m_Value); // Установка значения в текстовое поле
         ChartRedraw();        // Обновление отображения чарта
      }
} 

Полученный аргумент метода присваивается переменной m_Value и, если элемент управления видим, выполняется вывод значения в текстовое поле.

В числовом режиме полученный аргумент нормализуется по значению m_Digits, корректируется по максимальному и минимальному значениями (m_MaxValue, m_MinValue), преобразуется в строку и вызывается первый метод SetValue().

// Установка числового значения
void SetValue(double aValue)
{ 
   if(m_Digits>=0)
   {  // В числовом режиме
       // Нормализация числа по установленному количеству знаков после запятой
      aValue=NormalizeDouble(aValue,m_Digits);
      // "Выравнивание" значения по минимально допустимому значению
      aValue=MathMax(aValue,m_ValueMin); 
       // "Выравнивание" значения по максимально допустимому значению
      aValue=MathMin(aValue,m_ValueMax); 
       // Установка полученного значения как текстового значения
      SetValue(DoubleToString(aValue,m_Digits)); 
   }
   else
   { // В текстовом режиме
      SetValue((string)aValue); // Присвоение значения переменной для хранения значения как есть
   }            
}

Пишем два метода получения значений: один для получения значения типа string, второй - для double:

// Получение текстового значения
string ValueStrind()
{ 
   return(m_Value);
}

// Получение числового значения
double ValueDouble()
{ 
   return(StringToDouble(m_Value));
}

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

// Установка максимально допустимого значения
void SetMaxValue(double aValue)
{ 
   m_ValueMax=aValue; // Регистрация нового максимально допустимого значения
      if(m_Digits>=0)
     { // Элемент управления работает в числовом режиме
         if(StringToDouble(m_Value)>m_ValueMax)
         { /* Текущее значение элемента управления больше нового максимально допустимого значения*/
            SetValue(m_ValueMax); // Установка нового значения равного максимально допустимому значению
         }
      }         
}

// Установка минимально допустимого значения
void SetMinValue(double aValue)
{ 
   m_ValueMin=aValue; // Регистрация нового минимально допустимого значения     
      if(m_Digits>=0)
      { // Элемент управления работает в числовом режиме
         if(StringToDouble(m_Value)<m_ValueMin)
         { /* Текущее значение элемента управления меньше нового минимально допустимого значения*/
            SetValue(m_ValueMin); // Установка нового значения равного минимально допустимому значению 
         }
      }
}

// Получение максимально допустимого значения
double MaxValue()
{ 
   return(m_ValueMax); 
}

// Получение минимально допустимого значения
double MinValue()
{ 
   return(m_ValueMin);
}

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

Теперь займемся вводом значения пользователем, методом Event(). Контроль данных введенных пользователем будет выполняться по событию CHARTEVENT_OBJECT_ENDEDIT. При работе в текстовом режиме, если введенное пользователем значение не равно значению переменной m_Value, переменной m_Value будет присваиваться новое значение и присваивается значение 1 переменной m_event возвращаемой из метода Event().

При работе в числовом режиме запомним в переменной m_OldValue предыдущее значение m_Value, заменим запятую на точку, преобразуем строку в число и отдадим функции SetValue(), затем, в случае неравенства m_Value и m_OldValue "сгенерируем" событие (установим переменной m_event значение 1).

// Обработка событий
int Event(const int id,
           const long & lparam,
           const double & dparam,
           const string & sparam)
{ 
   bool m_event=0; // Переменная для события этого элемента управления
      if(id==CHARTEVENT_OBJECT_ENDEDIT)
      { // Произошло событие окончания редактирования текстового поля
         if(sparam==m_NameEdit)
         { // Выполнялось редактирование текстового поля с именем m_NameEdit
            if(m_Digits<0)
            { // В текстовом режиме
               g.Attach(m_NameEdit); // Присоединение текстового поля для управления
                  if(g.Text()!=m_Value)
                  { // В текстовом поле новое значение
                     m_Value=g.Text(); // Присвоение значения переменной для хранения значения
                     m_event=1;         // Произошло событие
                  }
            }
            else
            { // В числовом режиме
               string m_OldValue=m_Value; // Переменная с предыдущим значением элемента управления
               g.Attach(m_NameEdit);      // Присоединение текстового поля для управления
               string m_stmp=g.Text();     // Получение текста из текстового поля, введенное пользователем
               StringReplace(m_stmp,",",".");       // Замена запятой на точку
               double m_dtmp=StringToDouble(m_stmp); // Преобразование в число
               SetValue(m_dtmp);                     // Установка нового числового значения
                     // Сравнение нового значения с предыдущим
                  if(StringToDouble(m_Value)!=StringToDouble(m_OldValue))
                  { 
                     m_event=1; // Произошло событие 
                  }
            }
         }
      }               
   return(m_event); // Возвращаем событие. 0 - нет события, 1 - есть событие
}

Обеспечим работу элемента управления в подокнах. Для этого добавим метод SetSubWindow(), который должен вызываться из функции OnChartEvent() в случае события CHARTEVENT_CHART_CHANGE. Если планируется применение элемента управления только на графике цены, не требуется вызывать этот метод. 

Переменная m_SubWindow уже объявлена, имеет значение по умолчанию 0 и уже передается в методы Edit() и Label() класса "w" при создании графических объектов элемента управления. В метод SetSubWindowName() будет будет передаваться номер подокна, если номер меняется - меняем значение переменной m_SubWindow и выполняем метод Refresh().

// Установка подокна по номеру
void SetSubWindow(int aNumber)
{ 
   int m_itmp=(int)MathMax(aNumber,0); /* Если номер подокна отрицательный, будет использоваться 0 - график цены*/
      if(m_itmp!=m_SubWindow)
      { /* Указанный номер окна не соответствует номеру в котором находится элемент управления*/
         m_SubWindow=m_itmp; // Регистрация нового номера подокна
         Refresh(); // Пересоздание графических объектов
      }
} 

Возможно, будет удобней передавать в функцию не номер подокна, а имя подокна, добавим еще один вариант метода SetSubWindow():

// Установка подокна по имени подокна
void SetSubWindow(string aName)
{ 
   SetSubWindow(ChartWindowFind(0,aName)); // Определение номера подокна по имени и установка подокна по номеру
}

Дополним класс элемента управления остальными недостающими методами в соответствии с определенной в начале статьи концепцией.

Поскольку имеется метод SetPos() позволяющий одновременно устанавливать обе координаты элемента управления, добавим методы для установки координат по отдельности:

// Установка координаты X 
void SetPosLeft(int aLeft)
{ 
   m_Left=aLeft;
}      

// Установка координаты Y
void SetPosTop(int aTop)
{ 
   m_Top=aTop;
}  

 Метод установки ширины:

// Установка ширины 
void SetWidth(int aWidth)
{ 
   m_Width=aWidth;
}

Методы получения координат и размеров:

// Получение координаты X
int Left()
{ 
   return(m_Left);
}

// Получение координаты Y
int Top()
{ 
   return(m_Top);
}

// Получение ширины
int Width()
{ 
   return(m_Width);
}

// Получение высоты
int Height()
{
   return(m_Height); 
}

Методы для работы с тэгом:

// Установка тэга
void SetTag(string aValue)
{ 
   m_Tag=aValue;
}

// Получение тэга
string Tag()
{ 
   return(m_Tag);
}  

Методы предупреждения:

// Установка режима предупреждения
void SetWarning(bool aValue)
{ 
      if(m_Visible)
      { // Включена видимость
         if(aValue)
         { // Нужно включить режим предупреждения
            if(!m_Warning)
            { // Режим предупреждения не был включен
               g.Attach(m_NameEdit);         // Присоединение текстового поля для управления 
               g.SetBgColor(m_WarningColor); // Установка предупреждающего цвета фона текстового поля
            }
         }
         else
         { // Нужно отключить режим предупреждения
            if(m_Warning)
            { // Включен режим предупреждения
               g.Attach(m_NameEdit);    // Присоединение текстового поля для управления 
               g.SetBgColor(m_BgColor); // Установка нормального цвета фона                
            }
         }
      }
   m_Warning=aValue; // Регистрация установленного режима
}

// Получение предупреждающего режима
bool Warning()
{ 
   return(m_Warning);
}

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

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

Осталось одно свойство m_Digits, и для него добавим методы чтения/установки значения: 

// Установка числа знаков после запятой
void SetDigits(int aValue)
{ 
   m_Digits=aValue; // Регистрация нового значения
      if(m_Digits>=0)
      { // Числовой режим
         SetValue(ValueDouble()); // Переустановка текущего значения
      }
}  

// Получение значения m_Digits
int Digits()
{ 
   return(m_Digits);
}  

С самым интересным закончили, осталось самое красивое.


6. Цветовые схемы

Наборы цветов цветовых схем будут храниться в переменных класса CСolorSchemes.

Класс будет заранее объявлен в файле IncGUI.mqh с именем ClrScheme. Для выбора цветовой схемы надо будет вызвать метод SetScheme() с указанием в параметрах номера цветовой схемы. Если не вызывать метод SetScheme(), будут использоваться цвета из схемы с индексом 0.

Для получения цвета будет используется метод Color() с указанием номера цвета из схемы. Пишем класс CСolor Schemes с секциями private и public, в секции private объявляем переменную m_ShemeIndex для хранения индекса цветовой схемы. В секции public пишем метод SetScheme():

// Установка номера цветовой схемы
void SetScheme(int aShemeIndex)
{ 
   m_ShemeIndex=aShemeIndex;
}

Метод Color(). В методе объявлен двухмерный массив: первое измерение - номер цветовой схемы, второе - номер цвета в схеме. В зависимости от установленного номера цветовой схемы, возвращается цвет по номеру указанному в параметрах метода.

color Color(int aColorIndex)
{
   color m_Color[3][4];  // Первое измерение - количество цветовых схем, второе - номер цвета в схеме
   // default
   m_Color[0][0]=clrSnow;
   m_Color[0][1]=clrDimGray;
   m_Color[0][2]=clrDimGray;
   m_Color[0][3]=clrPink;
   // желто-коричневая
   m_Color[1][0]=clrLightYellow;
   m_Color[1][1]=clrBrown;
   m_Color[1][2]=clrBrown;
   m_Color[1][3]=clrPink;
   // голубая
   m_Color[2][0]=clrAliceBlue;
   m_Color[2][1]=clrNavy;
   m_Color[2][2]=clrNavy;
   m_Color[2][3]=clrPink;
   return(m_Color[m_ShemeIndex][aColorIndex]); // Возвращение значение по номеру схемы и номеру цветы в схеме
}

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

Чтобы было легко выбрать подходящий цвет из схемы или принять решение о необходимости добавления в схему нового цвета, в классе имеется метод позволяющий просматривать цвета - метод Show() (рис. 6). Соответственно, имеется метод Hide() для удаления цветовых образцов с графика.


Рис. 6. Просмотр цветовых схем методом Show()

В приложении к статье находится файл ColorSchemesView.mq5 - эксперт для просмотра цветовых схем (ColorSchemesView.mq5).

Немного отредактируем класс CInputBox, метод Init() - заменим его цвета на цвета из класса ClrScheme:

m_BgColor=ClrScheme.Color(0);       // Цвет фона текстового поля
m_TxtColor=ClrScheme.Color(1);      // Цвет текста и рамки текстового поля
m_LblColor=ClrScheme.Color(2);     // Цвет надписи
m_WarningColor=ClrScheme.Color(3); // Предупреждающий цвет

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


7. Использование элемента управления

Создаем эксперта с именем GUITest, подключаем файл IncGUI.mqh:  

#include <IncGUI.mqh>
Объявляем класс CInputBox с именем ib:
CInputBox ib;

В функции OnInit() эксперта вызываем метод Init() объекта ib:

ib.Init("InpytBox",50,4,"ввод");

Включаем отображение элемента управления с установкой позиции:

ib.Show(10,20);

В функции OnDeinit() эксперта удаляем элемент управления:

ib.Hide(); 

Компилируем, присоединяем эксперта к графику и видим наш элемент управления (рис. 7).


Рис. 7. Элемент управления InputBox

Добавим в эксперт возможность менять цветовую схему.

На данный момент у нас есть три цветовые схемы, напишем перечисление для выбора типа цветовой схемы и внешнюю переменную для этого: 

enum eColorScheme
  {
   DefaultScheme=0,
   YellowBrownScheme=1,
   BlueScheme=2
  };

input eColorScheme ColorScheme=DefaultScheme;

В самое начало функции OnInit() эксперта добавим установку цветовой схемы:

ClrScheme.SetScheme(ColorScheme);

Теперь в окне свойств эксперта можно выбрать одну из трех цветовых схем (рис. 8).




Рис. 8. Различные цветовые схемы

Для обработки события ввода нового значения, в эксперт в функцию OnChartEvent() добавляем код:

if(ib.Event(id,lparam,dparam,sparam)==1)
  {
   Alert("Введено значение "+ib.ValueStrind());
  }

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

Добавим в эксперт возможность создавать элемент управления в подокне.

Сначала создадим тестовый индикатор с именем TestSubWindow (в приложении файл TestSubWindow.mq5). При создании индикатора, в мастере создания надо указать, что индикатор должен работать в отдельном окне. В функцию OnChartEvent() эксперта добавим такой код:

if(CHARTEVENT_CHART_CHANGE)
  {
   ip.SetSubWindow("TestSubWindow");
  }

Теперь, если индикатора нет на графике, элемент управления создается на графике цены, если прикрепить индикатор на график, элемент управления перепрыгнет в подокно (рис. 9), если удалить индикатор, элемент управления вернется на график цены.

 
Рис. 9. Элемент управления в подокне

Заключение

В результате проведенной работы имеем подключаемый файл IncGUI.mqh с классами: CGraphicObjectShell (создание и управление графическими объектами), CWorkPiece (быстрое создание некоторых графических объектов с установкой свойств через параметры), CColorSchemes (для установки цветовой схемы и получения цветов установленной цветовой схемы) и один класс элемента управления CInputBox.

Классы CGraphicObjectShell, CWorkPiece, CColorSchemes уже объявлены в файле с именами "g", "w", "ClrScheme", то есть, уже готовы к использованию сразу после подключения файла IncGUI.mqh.

Повторим порядок использования класса CInputBox:

  1. Подключить файл IncGUI.mqh.
  2. Объявить класс типа CInputBox.
  3. Вызвать метод Init().
  4. Установить координаты методом SetPos(), по мере необходимости включить отображение методом Show(). Второй вариант: включить отображение методом Show() с указанием координат.
  5. По мере необходимости или по завершению работы эксперта скрыть элемент управления методом Hide().
  6. В функцию OnChartEvent() добавить вызов метода Event().
  7. При необходимости создания элемента управления в подокне, в функцию OnChartEvent() добавить вызов метода SetSubWindow() при событии CHARTEVENT_CHART_CHANGE.
  8. Для использования цветовых схем, перед вызовом метода Init(), вызвать метод SetScheme() класса ClrScheme. 


Приложение