Создание графических интерфейсов на базе .Net Framework и C# (Часть 2): Дополнительные графические элементы

20 мая 2019, 19:38
Vasiliy Sokolov
21
1 968

Оглавление


Введение

С октября 2018 года MetaTrader 5 стал поддерживать интеграцию с библиотеками Net Famework. Данный набор библиотек в действительности гораздо большее, чем просто какой-либо фреймворк или специализированная система для выполнения определенного круга задач — например, отрисовка графических окон или реализация сетевого взаимодействия. В Net Framework есть буквально абсолютно всё. С его помощью можно разрабатывать сайты (Net Core, MVC), писать системные приложения с унифицированным, профессиональным интерфейсом (Windows Forms), создавать сложные распределенные системы с обменом информацией между узлами (Windows Comunication Fundation), работать с базами данных (Entity Framework). В конце концов, Net Framework — это огромное сообщество программистов и компаний с тысячами открытых проектов самого разного содержания. И всё это, при должной организации взаимодействия, может быть доступным в MQL уже сегодня.

В данной статье мы продолжим развивать функционал GuiController'а, созданного в первой части. Этот функционал направлен на взаимодействие с графическим функционалом Net Framework на базе технологии Windows Forms. О графических возможностях MQL написано немало. На сегодняшний момент существует множество различных библиотек, которые в той или иной степени делают нечто похожее, но средствами MQL. Поэтому мне не хотелось бы, чтобы данный материал воспринимался читателями как "еще одна библиотека для работы с формами". В действительности, данный материал лишь часть большой серии статей, описывающий взаимодействие с Net Framework и шаг за шагом открывающий бескрайнюю Вселенную этой программной платформы. Windows Forms в ней лишь один из кирпичиков, хотя очень удобный и всеобъемлющий, как и любая часть технологии Net. Графическая подсистема Windows Forms — отличная точка старта для знакомства с этим фреймворком. После изучения Windows Forms можно применять в других направлениях взаимодействия с Net Framework, а также создавать с ее помощью достаточно эффектные, а главное, простые в реализации торговые панели, окна настроек экспертов, продвинутые графические индикаторы, системы управления роботами, в общем всё, что так или иначе связано с взаимодействием пользователя и торговой платформы.

Однако, для реализации всех этих захватывающих возможностей необходимо существенно дополнить модуль взаимодействия между MQL-программой и C# библиотекой. Напомню, что в первой части наш модуль GuiController умел взаимодействовать лишь с несколькими графическими элементами WinForms, такими как кнопки (Button), текстовые метки (Label), текстовые поля для ввода текста (TextBox) и вертикальный скролл. Несмотря на эту скудную поддержку элементов, нам удалось создать законченную и достаточно функциональную графическую панель:

Рис. 1. Торговая панель, созданная в первой части статьи

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


Организация тестирования новых элементов

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

 

Рис. 2 Выбор загружаемой пользовательской формы с необходимыми элементами

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

  1. Show Form - Запускает форму из определенной сборки;
  2. HideForm - Скрывает форму;
  3. GetEvent - Получает событие от формы;
  4. SendEvent - Отправляет событие форме.

В функции OnInit в зависимости от выбранного элемента мы будем загружать нужное нам окно. Прототип функции показан ниже:

int OnInit()
{
   switch(ElementType)
   {
      case WINFORM_TAB:
         GuiController::ShowForm("DemoForm.exe", "tab_form");
         break;
      case WINFORM_BUTTON:
         GuiController::ShowForm("DemoForm.exe", "button_form");
         break;
      ...
   }
   ...
}

В функции OnTimer мы будем обрабатывать поступающие от формы события:

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
{   
   //-- get new events by timer
   for(static int i = 0; i < GuiController::EventsTotal(); i++)
   {
      int id;
      string el_name;
      long lparam;
      double dparam;
      string sparam;
      GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
      ...
      if(id == TabIndexChange)
         printf("Selecet new tab. Index: " + (string)lparam + " Name: " + sparam);
      else if(id == ComboBoxChange)
         printf("ComboBox '" + el_name + "' was changed on " + sparam);
      ...
   }
}

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


MessageBox (Окна сообщений)

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

Для того, что бы запустить демонстрацию окон сообщений выберите при запуске эксперта в параметре  "Windows Form Element Type" вариант 'Buttons and MessageBox'. После запуска эксперта появится форма, предлагающая выбрать один из вариантов действий:

Рис. 3. Демонстрационная форма вызывающая окна сообщений. 

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


Рис. 4. Торговый эксперт просит подтверждение на открытие новой короткой позиции.

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

for(static int i = 0; i < GuiController::EventsTotal(); i++)
   {
      int id;
      string el_name;
      long lparam;
      double dparam;
      string sparam;
      //-- Получили новое событие
      GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
      //-- Выяснили его тип - нажатие кнопки
      if(id == ClickOnElement)
      {
         //-- Вывели в консоль терминала названия нажатой кнопки
         printf("You press '" + sparam + "' button");
         string msg;
         //-- В зависимости от типа нажимаемой кнопки сформировали сообщение MessageBox
         if(el_name != "btnCancelAll")
            msg = "Are you sure you want to open a new " + sparam + " position?";
         else
            msg = "Are you sure you want to close all positions?";
         //-- Отправили обратное событие, с командой вывести MessageBox
         GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OKCancel, msg);
      }
      ...
   }

Разберем сигнатуру отправки события:

GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OKCancel, msg);

Она означает, что эксперт просит отобразить окно сообщений (MessageBox) с основным текстом в переменной msg, показывая две кнопки OK и Cancel (OKCancel). В первой части статьи мы говорили, что первый параметр метода SendEvent содержит название графического элемента получателя отправляемого события. Однако с MessageBox это поле работает иначе. Дело в том, что окна сообщений не привязаны к какому-либо конкретному графическому окну или элементу (хотя Windows Frorms позволяет сделать такую привязку). Поэтому GuiController создает новое окно сообщений самостоятельно, и адрес назначения сообщения ему не нужен. Однако, как правило с выводом сообщения требуется заблокировать окно, к которому оно относится. Действительно было бы странно, если бы при выводе сообщения, оставалась возможность повторно нажать на кнопку BUY или SELL, игнорируя при этом появившийся MessageBox. Поэтому в GuiController название первого параметра для этого события означает название элемента, которое нужно заблокировать до момента нажатия одной из кнопок MessageBox пользователем. Функция блокировки произвольного графического элемента является  опциональной. Она задается с помощью целочисленной переменной lparam: 0 - блокировки окна нет, 1 - блокировка есть. Однако куда удобней оперировать не нулями и единицами, а константами. Для этих целей в GuiController определены две константы с помощью перечисления BlockingControl:

  • LockControl; 
  • NotLockControl

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

Окна сообщений помимо текста также могут содержать различные комбинации кнопок. Нажимая на эти кнопки, пользователь соглашается с тем или иным вариантом выбора. Наборы кнопок задаются с помощью системного перечисления System.Windows.Forms.MessageBoxButtons. Элементы этого перечисления недоступны пользователям MQL, т.к. они определены во внешней сборке. Чтобы облегчить работу программистов MQL с GuiController, было введено новое перечисление — клон System.Windows.Forms.MessageBoxButtons с теми же параметрами. Определение этого перечисления дано в IController.cs:

//
// Summary:
//     Specifies constants defining which buttons to display on a System.Windows.Forms.MessageBox.
public enum MessageBoxButtons
{
    //
    // Summary:
    //     The message box contains an OK button.
    OK = 0,
    //
    // Summary:
    //     The message box contains OK and Cancel buttons.
    OKCancel = 1,
    //
    // Summary:
    //     The message box contains Abort, Retry, and Ignore buttons.
    AbortRetryIgnore = 2,
    //
    // Summary:
    //     The message box contains Yes, No, and Cancel buttons.
    YesNoCancel = 3,
    //
    // Summary:
    //     The message box contains Yes and No buttons.
    YesNo = 4,
    //
    // Summary:
    //     The message box contains Retry and Cancel buttons.
    RetryCancel = 5
}

Константы этого перечисления доступны в MQL Editor напрямую, например через IntelliSens что делает конфигурирование MessgaBox весьма удобным. Так например, если в SendEvent заменить константу OKCancel на YesNoCancel, то диалоговое окно приобретет уже другой набор кнопок:

GuiController::SendEvent("ButtonForm", MessageBox, LockControl, YesNoCancel, msg);

Рис. 5. Стандартная комбинация из трех кнопок выбора - Yes/No/Cancel.

Помимо комбинаций кнопок GuiController поддерживает настройку значков сообщений, а также текста заголовка окна. Так как метод SendEvent имеет фиксированное количество параметров, довольно проблематично все настройки передать через него, поэтому было найдено альтернативное решение. Строку текста сообщения можно разделить на секции с помощью символа "|" Каждая секция в этом случае будет отвечать за свой дополнительный параметр. Секций может быть от одной (нет разделителей) до трех (два разделителя). Рассмотрим несколько примеров. Предположим, что необходимо вывести простое сообщение, без значка и дополнительной надписи. Тогда формат отправки сообщения будет таким:

GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OK, "This is a simple message");


Рис. 6. Простое сообщение без значков и дополнительного текста в названии окна.

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

GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OK, "Warning|Your action can be dangerous");

Рис. 7. Сообщение с предупреждающим знаком

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

ЗначокКлючевое словоПсевдоним

Warning!

Error!!!

Infoi

Question ?


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

GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OK, "!!!|The operation was cancelled|Critical Error");

Рис. 8. Вывод окна сообщений с значком ошибки и названием окна

Контроллер подходит к разбору строки интеллектуально. Так, если задать строку "!!!|The operation was cancelled", то будет выведен значок критической ошибки с соответствующим сообщением. Если же в строке также указать две секции "The operation was cancelled|Critical Error", то значка выведено не будет, однако название окна будет изменено на "Critical Error".


TabControl (Вкладки)

Вкладки, или Tabs являются удобным инструментом компоновки элементов по группам:

Рис. 9. Панель с двумя вкладками

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

for(static int i = 0; i < GuiController::EventsTotal(); i++)
{
  int id;
  string el_name;
  long lparam;
  double dparam;
  string sparam;
  GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
  if(id == TabIndexChange)
     printf("Selecet new tab. Index: " + (string)lparam + " Name: " + sparam);
}

Событие TabIndexChange транслирует два параметра: lparam и sparam. В первом находится индекс вкладки, выбранный пользователем. Во втором находится имя выбранной вкладки. Так например, если пользователь выберет первую вкладку, эксперт напечатает сообщение:

Selecet new tab. Index: 0 Name: tabPage1

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

CheckBox (Флажок)

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

 

Рис. 10. Варианты выбора с помощью комбинации флагов

У флажка есть три состояния: выделен (Checked), невыделен (Unchecked) и выделен частично (Indeterminate). В Windows Forms существует структура System.Windows.Forms.CheckState, описывающая эти состояния:

namespace System.Windows.Forms
{
    //
    // Summary:
    //     Specifies the state of a control, such as a check box, that can be checked, unchecked,
    //     or set to an indeterminate state.
    public enum CheckState
    {
        //
        // Summary:
        //     The control is unchecked.
        Unchecked = 0,
        //
        // Summary:
        //     The control is checked.
        Checked = 1,
        //
        // Summary:
        //     The control is indeterminate. An indeterminate control generally has a shaded
        //     appearance.
        Indeterminate = 2
    }
}

Каждый раз, при нажатии пользователем этого флажка GuiController транслирует его состояние эксперту MQL с помощью события CheckBoxChange, через переменную lparam. Ее значения соответствуют одному из вариантов этого перечисления: 0 — Unchecked, 1 — Checked, 2 — Indeterminate.

В демонстрационном примере, эксперт отслеживает выбор флажков  'Enable Trading On EURUSD' и 'Enable Trading On GBPUSD'. Как только один из пунктов становится доступным, он делает доступными также его подпункты: 'Allow take profit'  и 'Allow stop loss'. И наоборот, если пользователь снимает флаг с одного из основных пунктов, его подпункты становятся тут же неактивными. Это достигается благодаря двум событиям: ElementEnable и CheckBoxChange. В коде ниже представлен алгоритм работы эксперта с флагами:

void OnTimer()
{   
   //-- get new events by timer
   for(static int i = 0; i < GuiController::EventsTotal(); i++)
   {
      int id;
      string el_name;
      long lparam;
      double dparam;
      string sparam;
      GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
      if(id == CheckBoxChange)
         ChangeEnableStopAndProfit(el_name, id, lparam, dparam, sparam);
   }
}

//+------------------------------------------------------------------+
//| Change enable stops and profit                                   |
//+------------------------------------------------------------------+
void ChangeEnableStopAndProfit(string el_name, int id, long lparam, double dparam, string sparam)
{
   int id_enable = ElementEnable;
   if(el_name == "EURUSDEnable")
   {
      GuiController::SendEvent("EURUSDProfit", id_enable, lparam, dparam, sparam);
      GuiController::SendEvent("EURUSDStop", id_enable, lparam, dparam, sparam);
   }
   else if(el_name == "GBPUSDEnable")
   {
      GuiController::SendEvent("GBPUSDProfit", id_enable, lparam, dparam, sparam);
      GuiController::SendEvent("GBPUSDStop", id_enable, lparam, dparam, sparam);
   }
}

Как только эксперт получает уведомление о том, что один из основных флагов пользователем выделен, он отсылает GuiController встречное событие ElementEnable со значением true. Если пользователь наоборот убирает флаг с этого элемента, то отсылается событие ElementEnable с флагом false. Благодаря такому взаимодействию эксперта и формы с помощью разных событий, создается эффект интерактивности: форма начинает менять доступность подэлементов, в зависимости от выбора самого пользователя, хотя сама логика управления находится непосредственно в эксперте.


Кнопки переключений (Radio Button)

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

Рис 11. Кнопки переключений

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

for(static int i = 0; i < GuiController::EventsTotal(); i++)
{
  int id;
  string el_name;
  long lparam;
  double dparam;
  string sparam;
  GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
  else if(id == RadioButtonChange)
  {
      if(lparam == true)
         printf("Your have selected " + sparam);
      else
         printf("Your have deselected " + sparam);
  }
}

Параметр lparam содержит флаг, указывающий что произошло с кнопкой: была она выделена (plaram = true) либо выделение с нее стало снято (lparam = false). Если понажимать кнопки, эксперт будет выводить похожие сообщения в терминал:

Your have deselected Expert
Your have selected Indicator
Your have deselected Indicator
Your have selected Script
...


Combo Box (выпадающий список)

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


Рис. 12. Выпадающий список и доступные пункты меню

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

Рис. 13. Выбор инструмента с возможностью ввести свой новый инструмент 

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

Все режимы отображения ComboBox задаются с помощью его свойства DropDownStyle. Как правило, это свойство устанавливается единожды, в момент проектирования графического интерфейса, поэтому в GuiController нет какого-либо события, позволяющего менять тип ComboBox. Однако контроллер позволяет отслеживать выбор элемента из списка и даже ввод нового значения. Таким образом ComboBox поддерживает два события: свое собственное ComboBoxChange и TextChange. Наша демонстрационная форма состоит из двух элементов ComboBox. Первый предлагает выбрать платформу между MetaTrader 4 и MetaTrader 5, второй выбирает символ. По умолчанию второй элемент заблокирован. Однако как только пользователь выбирает платформу, ему становится доступен выбор торгового инструмента. Приведем код, реализующий данный функционал: 

for(static int i = 0; i < GuiController::EventsTotal(); i++)
{
  int id;
  string el_name;
  long lparam;
  double dparam;
  string sparam;
  GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
  if(id == ComboBoxChange)
  {
     if(el_name == "comboBox1")
        //-- Разблокируем список символов, как только пользователь выберет платформу:
        GuiController::SendEvent("comboBox2", ElementEnable, 1, 0.0, "");
     printf("ComboBox '" + el_name + "' was changed on " + sparam);
  }
}

Если начать выбирать элементы из выпадающих списков, демонстрационный эксперт начнет выводить в терминал параметры выбора, сделанного пользователем:

ComboBox 'comboBox1' was changed on MetaTrader 5
ComboBox 'comboBox2' was changed on GBPUSD
ComboBox 'comboBox2' was changed on USDJPY
...


NumericUpDown (Окно с числовым перечислением)

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

Рис. 14. Окно с числовым перечислителем

GuiController поддерживает четыре события для этого типа элемента:

  • NumericChange — получает или посылает событие, содержащее новое числовое значение окна;
  • NumericFormatChange — посылает событие, задающее разрядность числа (в переменной lparam) и шаг его изменения (в переменной dparam);
  • NumericMaxChange — посылает событие, задающее максимально возможное значение числа;
  • NumericMinChange —  посылает событие, задающее минимально возможное значение числа.

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

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   if(ElementType != WINFORM_HIDE)
      EventSetMillisecondTimer(100);
   else
      EventSetMillisecondTimer(1000);
   switch(ElementType)
   {
      ...
      case WINFORM_NUMERIC:
      {
         GuiController::ShowForm(assembly, "NumericForm");
         double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
         double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);
         double price_step = NormalizeDouble(SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_SIZE), Digits());
         long digits = (long)Digits();
         GuiController::SendEvent("NumericForm", TextChange, 0, 0.0, "NumericForm (" + Symbol() + ")");
         NumericSet("StopLoss", Digits(), ask, (double)LONG_MAX, 0.0, price_step);
         NumericSet("TakeProfit", Digits(), bid, (double)LONG_MAX, 0.0, price_step);
         break;
      }
      ...
   }
   return(INIT_SUCCEEDED);
}

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

//+------------------------------------------------------------------+
//| Set NumericUpDownParameter                                       |
//| name - name of NumericUpDown element                             |
//| digits - digits of symbol                                        |
//| init - init double value                                         |
//| max - max value                                                  |
//| min - min value                                                  |
//| step - step of change                                            |
//+------------------------------------------------------------------+
void NumericSet(string name, long digits, double init, double max, double min, double step)
{
   int id_foramt_change = NumericFormatChange;
   int id_change = NumericChange;
   int id_max = NumericMaxChange;
   int id_min = NumericMinChange;
   long lparam = 0;
   double dparam = 0.0;
   string sparam = "";
   GuiController::SendEvent(name, id_max, lparam, max, sparam);
   GuiController::SendEvent(name, id_min, lparam, min, sparam);
   GuiController::SendEvent(name, id_change, lparam, init, sparam);
   GuiController::SendEvent(name, id_foramt_change, digits, step, sparam);
}

Данный код будет работать адаптивно. В зависимости от символа, на котором он будет запущен, мы увидим разный формат цен:


Рис. 15. Собственные форматы цен для каждого торгового инструмента

DataTimePicker (Окно выбора даты)

Данный элемент похож по своей концепции на NumericUpDown с той лишь разницей, что позволяет безопасно выбирать даты, а не числа:

Рис. 16. Выбор точного времени в элементе DataTimePicker

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

for(static int i = 0; i < GuiController::EventsTotal(); i++)
{
  int id;
  string el_name;
  long lparam;
  double dparam;
  string sparam;
  GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
  if(id == DateTimePickerChange)
     printf("User set new datetime value: " + TimeToString((datetime)lparam));
}

Если запустить демонстрационный эксперт и выбрать в качестве элемента демонстрации DateTimePicker, появится окно как на рисунке 15. Если начать изменять дату и время различными способами, эксперт начнет реагировать на эти события, выводя новое значение даты и времени в лог:

User set a new datetime value: 2019.05.16 14:21
User set a new datetime value: 2019.05.16 10:21
User set a new datetime value: 2021.05.16 10:21

Однако само взаимодействие с элементом происходит несколько сложней, чем это может показаться на самом деле. Дело в том, что формат времени в MQL и в C# разный. В MQL используется упрощенный POSIX формат времени, с разрешающей способностью 1 секунда и минимально возможной датой 1970.01.01, в то время как в C# используется более продвинутый формат времени с разрешающей способностью 100 наносекунд. Таким образом что бы взаимодействовать с разными системами, необходимо написать конвертор времени, переводящий одну систему исчисления в другую. Такой конвертор используется в GuiController. Он выполнен в виде публичного статического класса MtConverter:

/// <summary>
/// System Converter MetaTrader - C# 
/// </summary>
public static class MtConverter
{
    /// <summary>
    /// Convert C# DateTime format to MQL (POSIX) DateTime format.
    /// </summary>
    /// <param name="date_time"></param>
    /// <returns></returns>
    public static long ToMqlDateTime(DateTime date_time)
    {
        DateTime tiks_1970 = new DateTime(1970, 01, 01);
        if (date_time < tiks_1970)
            return 0;
        TimeSpan time_delta = date_time - tiks_1970;
        return (long)Math.Floor(time_delta.TotalSeconds);
    }
    /// <summary>
    /// Convert MQL (Posix) time format to sharp DateTime value.
    /// </summary>
    /// <param name="mql_time">MQL datetime as tiks</param>
    /// <returns></returns>
    public static DateTime ToSharpDateTime(long mql_time)
    {
        DateTime tiks_1970 = new DateTime(1970, 01, 01);
        if (mql_time <= 0 || mql_time > int.MaxValue)
            return tiks_1970;
        TimeSpan time_delta = new TimeSpan(0, 0, (int)mql_time);
        DateTime sharp_time = tiks_1970 + time_delta;
        return sharp_time;
    }
}

В данный момент он состоит всего из двух методов: первый, ToMqlDateTime, конвертирует значение времени DateTime в формат MQL. Второй метод делает ровно наоборот, конвертирует значение MQL времени в C# структуру DateTime. Так как типы datetime (mql) и DateTime(C#) являются несовместимыми друг с другом, конвертация происходит через общий тип long, который является одинаковым для всех систем. Таким образом, получив событие  DateTimePickerChange, нам достаточно явно привести параметр lparam к datetime что бы получить корректное значение времени:

//-- Явно приводим значение long к datetime. Это полностью безопасно
printf("User set new datetime value: " + TimeToString((datetime)lparam));

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

GuiController::SendEvent("DateTimePicker", DateTimePickerChange, ((long)TimeCurrent()), 0.0, "");


ElementHide и ElementEnable — скрытие и деактивация произвольного элемента

Существуют универсальные события, через которые можно управлять любым элементом WinForms. К таким событиям относятся ElementHide и ElementEnable. Чтобы скрыть элемент необходимо использовать ElementHide, для чего достаточно написать:

GuiController::SendEvent("HideGroup", ElementHide, true, 0.0, "");

Где "HideGroup" — название элемента, который нужно скрыть. Для показа этого элемента соответственно следует вызвать:

GuiController::SendEvent("HideGroup", ElementHide, false, 0.0, "");

Обратите внимание на название используемого элемента. Дело в том, что один элемент в WindowsForm может содержать внутренние элементы. Это называется вложенностью элементов. Такая организация позволяет управлять всеми элементами на уровне группы. В демонстрационном примере используется рамка (text box) в которую вложена надпись label. С заданной периодичностью рамка исчезает, со всеми элементами внутри нее, а затем появляется вновь:

 

Рис. 17. Демонстрация скрытия произвольного графического элемента из торгового эксперта

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

GuiController::SendEvent("element", ElementEnable, false, 0.0, "");

Сделать элемент снова активным можно просто поменяв флаг false на true:

GuiController::SendEvent("element", ElementEnable, true, 0.0, "");


AddItem — добавление подэлементов

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


Рис. 18. Список предустановленных символов

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   EventSetMillisecondTimer(100);
   GuiController::ShowForm(assembly, "SendOrderForm");
   for(int i = 0; i < SymbolsTotal(true); i++)
   {
      GuiController::SendEvent("SymbolComboBox", AddItem, 0, 0, SymbolName(i, true));
   }
   return(INIT_SUCCEEDED);
}

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


Exception — событие получения исключений

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

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

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

void NumericSet(string name, long digits, double init, double max, double min, double step)
{
   int id_foramt_change = NumericFormatChange;
   int id_change = NumericChange;
   int id_max = NumericMaxChange;
   int id_min = NumericMinChange;
   long lparam = 0;
   double dparam = 0.0;
   string sparam = "";
   // GuiController::SendEvent(name, id_max, lparam, max, sparam);
   GuiController::SendEvent(name, id_min, lparam, min, sparam);
   GuiController::SendEvent(name, id_change, lparam, init, sparam);
   GuiController::SendEvent(name, id_foramt_change, digits, step, sparam);
}

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

Рис. 19. Нулевые значения в форме установки цен

Что же случилось? Для определения этого и существует система исключений. Каждое такое исключение можно получить, как и любое другое сообщение, через GuiController::GetEvent:

//-- get new events by timer
for(static int i = 0; i < GuiController::EventsTotal(); i++)
{
  int id;
  string el_name;
  long lparam;
  double dparam;
  string sparam;
  GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
  ...
  if(id == Exception)
     printf("Unexpected exception: " + sparam);
  
}

Вывод сообщения уже о многом может сказать:

Unexpected exception: Значение '1291,32' недопустимо для 'Value'. 'Value' должно лежать в диапазоне от 'Minimum' до 'Maximum'.
Имя параметра: Value
Unexpected exception: Значение '1291,06' недопустимо для 'Value'. 'Value' должно лежать в диапазоне от 'Minimum' до 'Maximum'.
Имя параметра: Value

Дело в том, что NumericUpDown по умолчанию имеет рабочий диапазон от 0 до 100. Соответственно при попытки установить текущую цену золота (1292 доллара за тройскую унцию) возникает ошибка. Чтобы ее избежать, необходимо программно расширить диапазон допустимых значений, сделать это можно с помощью события NumericMaxChange, чем и занимается закомментированная строка в функции NumericSet.

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

GuiController::SendEvent("empty", ElementEnable, 0, 0.0, "");

В ответ мы получим:

Unexpected exception: SendEvent: element with name 'empty' not find

Также можно попытаться отправить событие, которое не поддерживает элемент. Например, в поле ввода уровня стоп-лосса (тип элемента NumericUpDown) попытаемся добавить текст:

GuiController::SendEvent("StopLoss", AddItem, 0, 0.0, "New Text");

Ответ будет не менее лаконичным:

Unexpected exception: Element 'StopLos' doesn't support 'Add Item' event

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


Сводная таблицы доступных графических элементов и событий

Для работы с GuiController удобно систематизировать поддерживаемые графические элементы. Для этих целей создадим таблицу, содержащую сводную информацию об этих элементах и способах их использования в GuiController. В колонке "Пример использования" приведем краткий пример кода, в котором бы показывалась работа с этим элементом из MQL-программы. Рассматриваемый код необходимо рассматривать как часть общего паттерна работы с событиями. Так, например, код первого примера с MessageBox:

string msg = "!!!|The operation was cancelled|Critical Error";
GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OK, msg);

Необходимо рассматривать в следующем контексте:

void OnTimer
{
   //...
   //-- get new events by timer
   for(static int i = 0; i < GuiController::EventsTotal(); i++)
   {
     int id;
     string el_name;
     long lparam;
     double dparam;
     string sparam;
     GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
     if(id == MessageBox)
     {
        string msg = "!!!|The operation was cancelled|Critical Error";
        GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OK, msg);
     }
  }
}

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

Графический ЭлементИмя Элемента или событияИдентификаторы ключевых событийПример использования

MessageBoxMessageBox
string msg = "!!!|The operation was cancelled|Critical Error";
GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OK, msg);



TabsTabIndexChange
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("Selecet new tab. Index: " + (string)lparam + " Name: " + sparam);



CheckBoxCheckBoxChange
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("Checked " + sparam + " " + lparam);


RadioButtonRadioButtonChange

GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam); if(lparam == true)    printf("Your have selected " + sparam); else    printf("Your have deselected " + sparam);




ComboBoxComboBoxChange
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("ComboBox '" + el_name + "' was changed on " + sparam);


NumericUpDownNumericChange
NumericFormatChange
NumericMaxChange
NumericMinChange
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("Numeric '" + el_name + "' was changed, new value: " + DoubleToString(dparam, 4));



DateTimePickerDateTimePickerChange

GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam); printf("User set new datetime value: " + TimeToString((datetime)lparam));



 

Vertical Scroll ScrollChange 
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("Vertical Scroll has new value: " + (string)lparam);

 

 TextBoxTextChange  
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("new value entered: " + sparam);

 

 Button ClickOnElement 
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("Button " + sparam + " is pressed");

 LabelTextChange  
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("Label has new text: " + sparam);

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

СобытиеОписаниеПример использования
ElementHide Скрывает либо восстанавливает элемент
GuiController::SendEvent("HideGroup", ElementHide, true, 0.0, "");
ElementEnable Активирует либо диактивирует элемент
GuiController::SendEvent("HideGroup", ElementEnable, true, 0.0, "");
AddItem Добавляет новый подэлемент в выбранный элемент 
GuiController::ShowForm(assembly, "SendOrderForm");
for(int i = 0; i < SymbolsTotal(true); i++)
   GuiController::SendEvent("SymbolComboBox", AddItem, 0, 0, SymbolName(i, true));
Exeption  Получает исключение вызванное внутри CLR 
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("Unexpected exception: " + sparam);



Заключение

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

Последняя версия GuiController прикреплена во вложении к данной статье. Также эту версию можно склонировать с системы репозитория GitHub. Напомню, что версия этой библиотеки находится по адресу: https://github.com/PublicMqlProjects/MtGuiController.git. Также, Вы можете клонировать проект демонстрационной формы. Он находится по адресу https://github.com/PublicMqlProjects/GuiControllerElementsDemo.git  О том как получить последнюю версию библиотеки через систему контроля версий написано в первой части статьи. В прикрепленном файле находятся полные исходные коды всех проектов. Их три: эксперт, вызывающий форму с элементами, собственно набор форм в сборке DemoForm.exe и GuiController в скомпилированной версии (Source\MQL5\Libraries\GuiController.dll) и в виде исходников (Source\Sharp\GuiController). Также необходимо обратить внимание на то, что демонстрационному эксперту требуется указать абсолютный путь к запускаемой форме. На Вашем компьютере он будет отличаться от того, что указан в параметре assemble эксперта, поэтому его нужно поменять на Ваш фактический путь.

Прикрепленные файлы |
Source.zip (50.14 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (21)
fxsaber
fxsaber | 11 июн 2019 в 17:21
Vasiliy Sokolov:

Я пробовал делать через стандартный form.Dispose(). Т.е. когда вызывается GuiController::HideForm() идет вызов Dispose соответствующей формы.  В тестовом приложении на C# прекрасно все работало. Но в MT это не катит. Зато если перед удалением эксперта ручками закрываем форму - все отлично работает.

Здесь выгружается мгновенно.

Vasiliy Sokolov
Vasiliy Sokolov | 11 июн 2019 в 18:17
fxsaber:

Здесь выгружается мгновенно.

Да, видел Вашу панель. С помощью какой функции выгружаетесь?

Igor Makanu
Igor Makanu | 11 июн 2019 в 18:23
Vasiliy Sokolov:

Я пробовал делать через стандартный form.Dispose(). Т.е. когда вызывается GuiController::HideForm() идет вызов Dispose соответствующей формы.  В тестовом приложении на C# прекрасно все работало. Но в MT это не катит. Зато если перед удалением эксперта ручками закрываем форму - все отлично работает.

однозначно гуглить нужно, какой то процесс висит

как вариант, все равно нужно Form.Close() вызывать, чтобы Виндовс приложение закрыла

https://stackoverflow.com/questions/3097364/c-sharp-form-close-vs-form-dispose

Not calling Close probably bypasses sending a bunch of Win32 messages which one would think are somewhat important though I couldn't specifically tell you why...

Close has the benefit of raising events (that can be cancelled) such that an outsider (to the form) could watch for FormClosing and FormClosed in order to react accordingly.

I'm not clear whether FormClosing and/or FormClosed are raised if you simply dispose the form but I'll leave that to you to experiment with.


Vasiliy Sokolov:

Да, видел Вашу панель. С помощью какой функции выгружаетесь?

при вызове .dll создаю 2 потока и в них запускаю через ShowDialog() каждую форму, убиваю формы банальным вызовом метода Close() и освобождаю память на всякий случай - фиг его знает как там в .Net работает сборщик мусора )))
public static class FormsMT5
    {
        private static Form1 MainForm;
        private static Form2 OrderForm;
	private static Thread ThreadMainform, ThreadOrderForm;
....................
 

public static void FormDeinit(int reason)
        {
            if (reason == 3 || reason == 5) return;
            if (MainForm != null)
            {
                MainForm.Close();
            }
            if (OrderForm != null)
            {
                OrderForm.Close();
            }
            if (ThreadMainform != null) ThreadMainform.Join();
            if (ThreadOrderForm != null) ThreadOrderForm.Join();
            MainForm = null;
            OrderForm = null;
            ThreadMainform = null;
            ThreadOrderForm = null;
        }
fxsaber
fxsaber | 11 июн 2019 в 18:37
Vasiliy Sokolov:

Да, видел Вашу панель. С помощью какой функции выгружаетесь?

Это панель Игоря. Я только опубликовал.

Vasiliy Sokolov
Vasiliy Sokolov | 11 июн 2019 в 21:29
Igor Makanu:

однозначно гуглить нужно, какой то процесс висит

как вариант, все равно нужно Form.Close() вызывать, чтобы Виндовс приложение закрыла

https://stackoverflow.com/questions/3097364/c-sharp-form-close-vs-form-dispose


при вызове .dll создаю 2 потока и в них запускаю через ShowDialog() каждую форму, убиваю формы банальным вызовом метода Close() и освобождаю память на всякий случай - фиг его знает как там в .Net работает сборщик мусора )))

Там 100% дело в потоках или в маршалинге или во взаимодействии между этими потоками. Короче не все рецепты одинаковы полезны. Можно поиграться с потоками, но GuiController не хотелось бы только из-за этого переписывать.

Библиотека для простого и быстрого создания программ для MetaTrader (Часть IX): Совместимость с MQL4 - Подготовка данных Библиотека для простого и быстрого создания программ для MetaTrader (Часть IX): Совместимость с MQL4 - Подготовка данных

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

Применение OLAP в трейдинге (Часть 2): Визуализация результатов интерактивного анализа многомерных данных Применение OLAP в трейдинге (Часть 2): Визуализация результатов интерактивного анализа многомерных данных

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

Библиотека для простого и быстрого создания программ для MetaTrader (Часть X): Совместимость с MQL4 - События открытия позиции и активации отложенных ордеров Библиотека для простого и быстрого создания программ для MetaTrader (Часть X): Совместимость с MQL4 - События открытия позиции и активации отложенных ордеров

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

Оценка индекса фрактальности, показателя Херста и возможность предсказания финансовых временных рядов Оценка индекса фрактальности, показателя Херста и возможность предсказания финансовых временных рядов

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