Создание графических интерфейсов для экспертов и индикаторов на базе .Net Framework и C#

12 февраля 2019, 07:43
Vasiliy Sokolov
80
1 778

Введение

С октября 2018 года MQL5 стал нативно поддерживать интеграцию с библиотеками Net Framwork. Нативная поддержка означает что типы, методы и классы, размещенные в библиотеке .Net теперь, доступны из MQL5 программы напрямую, без предварительной декларации вызывающих функций и их параметров, а также сложного приведения типов двух языков друг к другу. Это действительно может считаться определенным прорывом, т.к. теперь гигантская кодовая база .Net Framework и мощь языка C# доступна практически "из коробки" всем пользователям MQL5.

Возможности Net Framework не ограничиваются только ей самой. Благодаря интегрированной, условно бесплатной, среде разработки VisualStudio, создание многих вещей становится гораздо проще. Например, с ее помощью в режиме drag-n-drop можно создать полноценное приложение windows, с формой и элементами на ней, которые будут вести себя привычным образом как и любое другое графическое windows-приложение. Это то, чего так не хватало в MQL.

Конечно, за годы существования этого языка, появилась ни одна и не две библиотеки, существенно облегчающие построение графического приложения внутри MQL программы. Однако, все эти библиотеки, как бы хороши они не были, представляют из себя набор кода, принцип работы с которым нужно понимать, а также уметь его интегрировать с кодом своих советников и индикаторов. Иными словами, пользователи, не знакомые с программированием, едва ли могли использовать эти библиотеки на практике. Разрыв между простотой создания форм в Visual Studio и сложностью конфигурирования графических библиотек в MQL, сохранялся бы и поныне, однако благодаря новой интеграции с библиотеками .Net Framework, простое создание форм становится реальностью. 

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

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

Особый упор в статье сделан на простоту предложенного подхода. Основная задача сделать как можно более простое взаимодействие с кодом, написанным на C#. При этом само взаимодействие организовать таким образом, чтобы код, написанный на C#, создавался автоматически, без участия пользователя! Благодаря развитым языковым средствам C#, а также богатым возможностям Visual Studio, это возможно.

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


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

.Net — это патентованное название общеязыковой платформы от компании Microsoft. Она была создана в 2002 году как альтернатива популярной в то время, впрочем как и сейчас, платформе Java. Основу платформы составляет так называемая общеязыковая среда исполнения  Common Language Runtime или CLR. В отличии от классической программы, которая компилируется непосредственно в машинный код, и может быть запущена на компьютере напрямую, программа написанная для .Net запускается на виртуальной машине CLR.  Таким образом .Net — это некая среда, с помощью которой программа, написанная на языке высокого уровня, выполняется на машине пользователя.

Язык программирования C# является основным языком программирования в Net. Когда говорят о C# подразумевают Net, и напротив — Net четко ассоциируется с C#. Упрощенно можно сказать, что Net — это среда исполнения программ, написанных как правило на C#. Наша статья не будет исключением. Весь код, предложенный в статье, будет написан на этом языке.

После того, как программа для платформы .Net написана, она компилируется в некий промежуточный байт код низкоуровневого языка CIL (Common Intermediate Language), который исполняет виртуальная машина CLR. Сам код упаковывается в стандартные сущности windows-программ: исполняемые модули exe или динамические библиотеки dll. Скомпилированный код для виртуальной машины Net имеет высокоуровневую структуру, его свойства легко исследовать, можно понять какие типы данных он содержит. Эту замечательную особенность используют последние версии компилятора MQL. Компилятор, на этапе компиляции, загружает динамическую библиотеку Net, и читает общедоступные статические методы, определенные в ней. Помимо открытых статических методов, компилятор MQL понимает базовые типы данных языка программирования C#. К этим типам данным относятся:

  • Все целочисленные типы данных: long/ulong, int/uint, byte, short/ushort;
  • Вещественные числа с плавающей запятой float/double;
  • Символьный тип данных char (в отличии от MQL, где char и uchar означают байтовый тип данных, в С# данный тип используется для определения символа);
  • Строковые типы string;
  • Простые структуры, содержащие в качестве своих полей базовые типы перечисленные выше.

Помимо перечисленных типов компилятор MQL видит массивы C#. Однако на текущий момент получить стандартный доступ к элементам массива по индексатору '[]' в программе MQL пока невозможно. Можно сказать уверено, что в будущем поддержка типов будет расширяться, однако сегодняшних возможностей уже вполне достаточно для организации полноценного взаимодействия.

В нашем проекте мы будем создавать формы с помощью технологии Windows Forms. Это довольно простой набор API, который позволяет быстро, а главное просто нарисовать графический интерфейс даже неподготовленному пользователю. Его особенностью является событийно-ориентированный подход. Это значит, что когда пользователь нажимает на кнопку или вводит текст в окне ввода — в этот момент генерируется соответствующее событие. Обработав такое событие, программа написанная на C# определяет, что тот или иной графический элемент формы был изменен пользователем. Работа с событиями достаточно сложный процесс для пользователя, незнакомого с C#, поэтому необходимо написать специальный промежуточный код, который будет обрабатывать события происходящие в форме и передавать их MQL-программе, запущенной в терминале MetaTrader 5.

Таким образом, в нашем проекте будет существовать три независимых объекта, которые будут взаимодействовать друг с другом:

  • Программа в виде эксперта или индикатора написанная на MQL (файл EX5), которая будет получать события от графического окна или передавать их ему с помощью специального контроллера;
  • Контроллер в виде динамической библиотеки Net (DLL-файл), к которой будет обращаться MQL-программа;
  • Графическое окно, созданное пользователем с помощью C#, в виде независимой программы (EXE) или динамической библиотеки (DLL), события которого будет анализировать контроллер.

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



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

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


Установка и настройка Visual Studio

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

Visual Studio представляет из себя профессиональную среду разработки в самых разнообразных областях программирования. Данный комплекс представлен в нескольких редакциях. Нас будет интересовать редакция Community. Данная версия является условно-бесплатной. После тридцати дней использования, ее необходимо бесплатно зарегистрировать, для чего пройти стандартную процедуру верификации с помощью одного из сервиса Microsoft. Мы покажем основные шаги по скачиванию, установке и регистрации платформы, чтобы начинающие пользователи могли безболезненно начать использовать ее функционал в кратчайшие сроки. 

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

Первое, что необходимо сделать — это перейти на официальный сайт Visual Studio visualstudio.microsoft.com и выбрать подходящий дистрибутив. Напоминаю, что нам необходимо выбрать версию Community:

 

Рис. 2. Выбор дистрибутива VisualStudio.


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

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

Рис. 3. Согласие на продолжение установки

Далее начнется процесс скачивания необходимых файлов для установки. Он может занять некоторое время, в зависимости от пропускной способности Вашего канала связи. После того, как процесс будет выполнен, появится окно конфигурации установки. Из предложенных компонентов, нам необходимо выбрать опцию ".Net desktop development": 


Рис. 4. Выбор компонентов

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

Рис. 5. Процесс инсталяции.

После завершения инсталляции Visual Studio запустится автоматически. Если этого не произошло, запустите ее вручную. При первом запуске Visual Studio попросит войти в Ваш аккаунт или создать новый. Если у Вас нет аккаунта, лучше зарегистрировать его сейчас, для чего нажать ссылку "Create One":


Рис. 6. Создание новой учетной записи


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

Если регистрация Вам по каким-то причинам не подходит, просто пропустите этот шаг, нажав на ссылку "Not now, maybe later". Однако помните, что через тридцать дней Visual Studio потребует регистрацию, иначе прекратит свою работу.


Создание первой формы. Быстрый старт.

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

Итак, необходимо создать новый проект. Для этого выберем в меню  File -> New -> Project. Откроется диалоговое окно выбора типа проекта:

Рис 7.

Нам необходимо выбрать тип "Windows Form App (.Net Framework)". В поле Name необходимо ввести имя проекта. Переименуем стандартное название, выдаваемое по умолчанию, назовем наш проект GuiMT. После чего нажмем кнопку "OK". После этого в Visual Studio автоматически отобразить визуальный конструктор с автоматически созданной формой:

 

Рис. 8. Создание графической формы в окне Visual Studio.


В окне Solution Explorer содержится структура нашего созданного проекта. Обратите внимание на название Form1.cs — это файл, содержащий программный код, создающий графическое представление формы, которое мы видим в окне графического конструктора Form1.cs[Disign]. Запомните это название, оно нам еще понадобится.

Визуальный конструктор с помощью мыши позволяет менять размер формы. Также на форме можно размещать пользовательские элементы. Сейчас этого будет вполне достаточно для наших первых экспериментов. Откроем вкладку Toolbox, на боковых табах слева главного окна и в разделе All Windows Form выберем элемент Button:

Рис. 9. Выбор кнопки

Перетащим его с помощью мышки на основную поверхность нашей формы Form1:

Рис. 10. Первая форма

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

Рис 11. Кнопка запуска приложения в режиме отладки.  

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

Рис. 11. Кнопка остановки отладки

Итак, наше первое приложение готово. Последнее что нам необходимо сделать, это выяснить абсолютный путь к запускаемой программе, которую мы только что создали. Самый простой способ — это просто посмотреть на путь в поле Project Folder окна Properties, при этом в окне Solution Explorer должен быть выделен проект GuiMT:

Рис. 12. Абсолютный путь к проекту приложения в графе Project Folder

Путь в этом окне относится к самому проекту. Конкретная сборка нашей программы будет располагаться в одном из подкаталогов, в зависимости от режима компиляции. В нашем случае это будет .\bin\debug\<Название_нашего_проекта.exe>. Таким образом, полный путь к нашей программе будет: C:\Users\<Имя_пользователя>\source\repos\GuiMT\GuiMT\bin\debug\GuiMT.exe. После того, как мы выяснили путь, его нужно будет где-то записать или запомнить, потому что позже его необходимо будет вставить в наш код на MQL.


Получение последней версии GuiController.dll. Работа с GitHub

В файлах, прикрепленных к данной статье, содержится библиотека GuiController.dll. Эту библиотеку необходимо разместить в каталоге \MQL5\Libraries. Однако часто бывает так, что библиотека продолжает обновляться и развиваться, в то время как к статье прикреплен уже давно устаревший архив. Чтобы избежать такой и многих других подобных ситуаций, лучше всего использовать системы контроля версий, когда новый код автоматически становится доступен для получателей. Наш проект не является исключением. Для того, чтобы гарантировано  получить самую последнюю версию GuiController, воспользуемся открытым депозитарием хранения открытых кодов GitHub.com. Исходный код этого контроллера уже содержится в данном репозитории, все что необходимо сделать — это скачать его проект и скомпилировать контроллер в динамическую библиотеку. Если по каким-то причинам Вы не можете воспользоваться этой системой или просто не хотите, то просто пропустите этот раздел. Вместо этого просто скопируйте файл GuiController.dll в каталог MQL5\Libraries. 

Итак, если у Вас еще открыто текущее решение, закройте его, выполнив команду File -> Solution. Теперь перейдем на вкладку Team Explorer и нажмем ссылку Clone. В желтой графе  введите адрес, по которому  располагается проект:

https://github.com/PublicMqlProjects/MtGuiController

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

Рис. 13. Подключение к удаленному репозиторию исходного кода

Теперь, когда все готово, нажмем кнопку Clone. Через некоторое время у Вас по указанному адресу появится проект с последней версией MtGuiController. Откройте его через команду в меню File -> Open -> Project/Solution. После того как проект загружен и открыт, его необходимо скомпилировать, для чего можно нажать клавишу "F6" или выбрать в меню команду Build -> Build Solution. Найдите скомпилированный файл MtGuiController.dll в папке MtGuiController\bin\debug и скопируйте его в каталог библиотек MetaTrader 5:  MQL5\Libraries.

Если по каким-то причинам Вам не удалось получить последнюю версию через github, не отчаивайтесь. В этом случае скопируйте контроллер из архива, прикрепленный к данной статье.


Интеграция первого приложения с MetaTrader 5

Теперь, когда у нас есть первое приложение и контроллер, который будет транслировать сигналы графического окна в MetaTrader, нам осталось выполнить заключительную часть: написать программу на MQL в виде эксперта, которая бы получала события от нашего окна через контроллер. Создадим новый эксперт в MetaEditor с именем GuiMtController со следующим содержимым:

//+------------------------------------------------------------------+
//|                                              GuiMtController.mq5 |
//|                        Copyright 2019, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2018, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#import  "MtGuiController.dll"
string assembly = "С:\\Users\\Bazil\\source\\repos\\GuiMT\\GuiMT\\bin\\Debug\\GuiMT.exe";
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- create timer
   EventSetMillisecondTimer(200);
   GuiController::ShowForm(assembly, "Form1");
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- destroy timer
   GuiController::HideForm(assembly, "Form1");
   EventKillTimer();   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
//---
}
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//---
   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("Click on element " + el_name);
   }
  }
//+------------------------------------------------------------------+

Напоминаю, что для того чтобы скомпилировать данный код, в каталоге MQL5\Libraries должна быть размещена библиотека MtGuiController.dll. Также абсолютный путь, указанный в строке

string assembly = "С:\\Users\\Bazil\\source\\repos\\GuiMT\\GuiMT\\bin\\Debug\\GuiMT.exe";

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

Рис. 14. Эксперт с интегрированным графическим приложением на C#.

Обратите внимание, что если нажать на кнопку button1, то эксперт выведет на вкладке Experts надпись "Click on element button1", сигнализируя о том, что он получил событие нажатие кнопки. 


Взаимодействия MQL-программы с GuiController, событийна модель

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

Итак, первое что мы видим — это деректива import и строка assembly:

#import  "MtGuiController.dll"
string assembly = "C:\\Users\\Bazil\\source\\repos\\GuiMT\\GuiMT\\bin\\Debug\\GuiMT.exe";

Первая строка говорит компилятору о том, что будет использованы вызовы к открытым статическим методам класса, располагающимся в MtGuiController.dll. Обратите внимание, что указывать к каким именно методам мы будет обращаться в этой сборке не нужно. Эту работу выполнит за нас компилятор в автоматическом режиме.

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

Далее содержится стандартный код процедуры инициализации советника OnInit:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- create timer
   EventSetMillisecondTimer(200);
   GuiController::ShowForm(assembly, "Form1");
//---
   return(INIT_SUCCEEDED);
  }

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

GuiController::ShowForm(assembly, "Form1");

В C# функции не могут существовать отдельно от классов. Таким образом у каждой функции (метода) есть свой класс, в котором она определена. В MtGuiController.dll определен единственный класс GuiController. Он содержит статические методы, с помощью которых можно управлять тем или иным окном. Других классов в MtGuiController.dll нет, а значит все управление централизовано происходит через этот класс. Это очень удобно, т.к. пользователь работает с одним единственным интерфейсом взаимодействия и не ищет нужную ему функцию из набора разрозненных определений.

Первое что делается в блоке инициализации — это вызов метода ShowForm. Как нетрудно догадаться из его названия, он занимается тем, что запускает процесс отображения формы. Первый параметр метода задает абсолютный путь к файлу, в котором определена форма, а второй задает имя самой формы. Дело в том, что в одном файле может быть опредено сразу несколько форм. Поэтому и необходимо указать какую именно форму в файле мы хотим запустить. Названием формы в данном случае является название класса формы, которое Visual Studio назначила по-умолчанию форме созданной нами. Если открыть наш ранее созданный проект в Visual Studio и открыть файл Form1.Designer.cs в режиме просмотра кода, то мы увидим название класса, которое нам нужно:

partial class Form1
{
        /// <summary>
        /// Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;
        ...
}

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

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

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("Click on element " + el_name);
   }

Событие, с точки зрения контроллера, это любое действие пользователя над формой. Например, когда пользователь нажимает на кнопку или вводит текст в текстовое окно, контроллер получает соответствующее событие и помещает его в список событий. Количество событий в списке транслируется статическим методом GuiController::EventsTotal(), который может вызвать наша MQL-программа.

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

  • Событие нажатия кнопки;
  • Событие окончания вода текста;
  • Событие горизонтального скролла.

В дальнейшем этот список планируется расширить, но для целей данной статьи предложенного списка пока достаточно.

Итак, после того как произойдет событие, которое поддерживает наш GuiController, оно будет им обработано и добавлено в список событий. Обработка события заключается в создании данных, получив которые MQL-программа могла бы относительно легко определить тип события и его параметры. Именно поэтому формат данных каждого события имеет очень схожую структуру с событийной моделью функции OnChartEvent. Благодаря этому сходству, пользователю, работающему с GuiController не нужно учить формат новой событийном модели. Конечно, в представленном подходе есть и свои трудности, например, непростые события вроде того же скролла крайне сложно уместить в предложенный формат, но и эти проблемы легко решаемы с помощью языковых средств C# и его развитой объектно-ориентированной модели программирования. Сейчас же предложенной модели будет вполне достаточно для решения наших задач.

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

public static void GetEvent(int event_n, ref string el_name, ref int id, ref long lparam, ref double dparam, ref string sparam)

Опишем его параметры по порядку: 

  • event-n — порядковый номер события, который необходимо получить. Благодаря возможности указать порядковый номер события становится легко контролировать новые события, в каком бы количестве они не поступали;
  • el_name — имя элемента, который сгенерировал данное событие;
  • id — Тип события.
  • lparam — целочисленное значение, которое имеет данное событие;
  • dparam — вещественное значение, которое имеет данное событие;
  • sparam — строковое значение, которое имеет данное событие.

Как видите, событийная модель GuiController сильно напоминает событийную модель OnChartEvent. Любое событие в GuiController всегда имеет порядковый номер и источник (имя элемента), которое это событие сгенерировал. Остальные параметры являются опциональными. Так некоторые события, вроде нажатия кнопки, вообще не имеют дополнительных параметров (lparam, dparam, sparam), другие же их содержат. Например событие окончание текста в параметре sparam содержит текст, который был введен в поле пользователем.

Ниже приведена таблица, которая содержит события и их параметры, подерживаемые на текущий момент:

Название события Идентификатор (id)  Параметры
Exception  0 sparam - содержит сообщение, вызвавшего исключения
ClickOnElement  1 -
TextChange  2 sparam - новый текст, введенный пользователем 
ScrollChange  3

lparam - предыдущий уровень скролла

dparam - текущий уровень скролла

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

GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);

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

if(id == ClickOnElement)
   printf("Click on element " + el_name);

Обратите внимание, что id сравнивается с константой ClickOnElement, которая нигде в MQL-коде программы не определена. Действительно, данная константа является частью перечисления определенного в самом GuiController на C#^

/// <summary>
/// Type of gui event
/// </summary>
public enum GuiEventType
{
    Exception,
    ClickOnElement,
    TextChange,
    ScrollChange
}

Как видите, компилятор понимает внешние перечисления определенные в Net-библиотеках и умеет с ними работать. 

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

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

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

//-- Цикл запоминает последний индекс события и при следующем запуске функции начинает работать с него
for(static int i = 0; i < GuiController::EventsTotal(); i++)

События можно как получать с помощью метода GuiController::GetEvent, так и отправлять их с помощью GuiController::SendEvent. Второй метод используется, кода нужно отправить какие-нибудь данные в окно и изменить его содержимое. Он имеет тот же прототип, что и GetEvent, единственная разница, что он не содержит порядковый номер события, так как в этом случае такой номер бессмысленен. Мы не будет останавливаться на нем подробно, однако покажем работу с ним на примере, который будет представлен в заключительной части статьи.

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

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

  1. Отобразить окно при запуске программы;
  2. Получать новые события от окна;
  3. Скрыть окно при выходе из программы;

Более простую схему сложно было бы придумать. Обратите внимание также на сам код формы, которую мы создали. Хотя форма окна содержит этот самый код, сами мы не написали ни строчки на C#. Всю работу за нас сделали развитые средства автогенерации кода в Visual Studio и собственно GuiController. Именно так проявляется мощь технологий Net, ведь конечной цель мощных сред как раз и является простота.


Под капотом у GuiController

Если Вы недостаточно хорошо разбираетесь в C#, данный раздел Вы можете смело пропустить. Он будет интересен тем, кто хочет разобраться как устроен GuiController и как происходит доступ к отдельным, изолированным приложениям Net.

GuiController представляет из себя разделяемый класс, состоящий из двух частей: статической и части экземпляра. В статической части класса сосредоточены открытые статические методы для взаимодействия с MetaTrader. Эта часть класса реализует интерфейс между MetaTrader 5 и самим контроллером. Вторая часть экземплярная — это значит, что данные и методы этой части существуют только на уровне экземпляра. В их задачу входит взаимодействие с независимыми сборками Net, в которых располагаются графические окна. Само графическое окно в Windows Forms — это класс, унаследованный от базового класса Form. Таким образом, с каждым пользовательским окном можно работать на более высоком и абстрактном уровне класса Form.

Внутри сборок Net, таких как DLL или EXE, содержатся типы Net, которые являются открытыми по своей сути. Получить доступ к ним, их свойствам и даже методам довольно просто. Это можно сделать с помощью механизма, называемого в Net  рефлексией или отражением. Благодаря этому механизму, каждый файл вроде DLL или EXE, созданный в Net, можно исследовать на предмет наличия нужного нам элемента. Именно этим и занимается класс GuiController. Когда в его метод ShowForm передают абсолютный путь к сборке в Net, то контроллер загружает эту сборку с помощью специального механизма, после чего находит в ней графическое окно, которое необходимо отобразить. Приведем код метода GetGuiController, который выполняет эту работу:

/// <summary>
/// Create GuiController for windows form
/// </summary>
/// <param name="assembly_path">Path to assembly</param>
/// <param name="form_name">Windows Form's name</param>
/// <returns></returns>
private static GuiController GetGuiController(string assembly_path, string form_name)
{
    //-- Загрузить указанную сборку
    Assembly assembly = Assembly.LoadFile(assembly_path);
    //-- Найти в ней указанную форму
    Form form = FindForm(assembly, form_name);
    //-- Назначить найденной форме управляющий контроллер
    GuiController controller = new GuiController(assembly, form, m_global_events);
    //-- Вернуть управляющий контроллер вызывающему методу
    return controller;
}

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

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

/// <summary>
/// Find needed form
/// </summary>
/// <param name="assembly">Assembly</param>
/// <returns></returns>
private static Form FindForm(Assembly assembly, string form_name)
{
    Type[] types = assembly.GetTypes();
    foreach (Type type in types)
    {
        //assembly.CreateInstance()
        if (type.BaseType == typeof(Form) && type.Name == form_name)
        {
            object obj_form = type.Assembly.CreateInstance(type.FullName);
            return (Form)obj_form;
        }
    }
    throw new Exception("Form with name " + form_name + " in assembly " + assembly.FullName + "  not find");
}

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

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

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

За запуск и удаление окна отвечают соответствующие методы контроллера:

/// <summary>
/// Пользовательскую форму, вызванную из MetaTrader необходимо запускать в асинхронном режиме,
/// что бы обеспечить отзывчивость интерфейса.
/// </summary>
public static void ShowForm(string assembly_path, string form_name)
{
    try
    {
        GuiController controller = GetGuiController(assembly_path, form_name);
        string full_path = assembly_path + "/" + form_name;
        m_controllers.Add(full_path, controller);
        controller.RunForm();
    }
    catch(Exception e)
    {
        SendExceptionEvent(e);
    }
}
        
/// <summary>
/// После того, как эксперт закончит работу с формой, необходимо завершить процесс ее выполнения.
/// </summary>
public static void HideForm(string assembly_path, string form_name)
{
    try
    {
        string full_path = assembly_path + "/" + form_name;
        if (!m_controllers.ContainsKey(full_path))
            return;
        GuiController controller = m_controllers[full_path];
        controller.DisposeForm();
    }
    catch(Exception ex)
    {
        SendExceptionEvent(ex);
    }
}

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

/// <summary>
/// Subscribe on supported events
/// </summary>
/// <param name="form">Windows form</param>
private void SubscribeOnElements(Form form)
{
    Dictionary<Type, List<HandlerControl>> types_and_events = new Dictionary<Type, List<HandlerControl>>();
    types_and_events.Add(typeof(VScrollBar), new List<HandlerControl>() { vscrol => ((VScrollBar)vscrol).Scroll += OnScroll });
    types_and_events.Add(typeof(Button), new List<HandlerControl>()  { button => ((Button)button).Click += OnClick });
    types_and_events.Add(typeof(Label), new List<HandlerControl>());
    types_and_events.Add(typeof(TextBox), new List<HandlerControl>() { text_box => text_box.LostFocus += OnLostFocus, text_box => text_box.KeyDown += OnKeyDown });
    foreach (Control control in form.Controls)
    {
        if (types_and_events.ContainsKey(control.GetType()))
        {
            types_and_events[control.GetType()].ForEach(el => el.Invoke(control));
            m_controls.Add(control.Name, control);
        }
    }
}

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


Торговая панель на основе графических интерфейсов

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

Рис. 15. Встроенная торговая панель MetaTrader 5.

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

Можно поступить по-разному, например, начать создавать такую панель с чистого нуля. Однако описывать работу визуального конструктора будет излишним в этой статье. Поэтому мы поступим проще и загрузим проект, содержащий эту панель в Visual Studio. Сделать это можно двумя способами: либо скопировать проект из архива и открыть его в Visual Studio, либо скачать его из удаленного репозитория Git по следующему адресу: 

https://github.com/PublicMqlProjects/TradePanelForm

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

После того, как проект будет загружен и открыт, Вы должны будете увидеть следующую форму:

Рис. 16. Окно TradePanel в визуальном конструкторе Visual Studio

Проект содержит макет торговой панели. В реальных проектах вроде этого нам потребуется постоянно получать доступ к элементам, размещенным на этой форме, а также отправлять им события. Для этих целей будет необходимо обращаться к каждому элементу по его имени. Поэтому имена элементов должны быть осмысленными и запоминающимися. Давайте посмотрим как называются элементы, которые мы будем использовать. Чтобы посмотреть имя каждого элемента нужно найти свойство Name в окне Properties, предварительно выделив этот элемент. Так например кнопка с надписью Buy будет обладать именем ButtonBuy:

Рис. 17. Имя элемента в окне Properties.

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

Приведем список элементов, которые содержит наша торговая панель:

  • Основное графическое окно (Form) c именем TradePanelForm, на котором располагаются все остальные элементы управления.
  • Красная текстовая метка (Label), с именем AskLabel. Данная метка будет отображать цену Ask текущего инструмента;
  • Cиняя текстовая метка (Label), с именем BidLabel. Данная метка будет отображать цену Bid текущего инструмента;
  • Поле ввода текста (TextBox) с именем CurrentVolume. В данное поле пользователь будет вводить нужный объем сделки;
  • Вертикальный Скролл (VScrollBar) с именем IncrementVol. Данный скролл будет увеличивать или уменьшать объем на один шаг. Величина шага будет определятся MQL-программой, на основе текущего торгового окружения.
  • Кнопка покупки с именем ButtonBuy. Нажав на эту кнопку пользователь купит заданный им объем по цене Ask — той, что отображается на красной текстовой метке.
  • Кнопка продажи с именем ButtonSell. Нажав на эту кнопку пользователь продаст заданный им объем по цене Bid, которая будет отображаться на синей текстовой метке.

Хотя элементов, которые мы используем, совсем немного, комбинируя их, мы получаем довольно продвинутый интерфейс. Заметим, что как и в предыдущем примере, наше решение не содержит ни одной строки кода на C# которую пришлось бы добавлять. Все нужные свойства элементов выставлены в окне Properties, а расположение и размер элементов установлены с помощью drag-n-drop техники, т.е. с помощью мышки!


Интеграция графического окна с кодом эксперта

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

//+------------------------------------------------------------------+
//|                                                   TradePanel.mq5 |
//|                        Copyright 2019, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#import  "MtGuiController.dll"
#include <Trade\Trade.mqh>
string assembly = "c:\\Users\\Bazil\\source\\repos\\TradePanel\\TradePanel\\bin\\Debug\\TradePanel.dll";
string FormName = "TradePanelForm";
double current_volume = 0.0;

//-- Trade module for executing orders
CTrade Trade;  
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
//--- create timer, show window and set volume
   EventSetMillisecondTimer(200);
   GuiController::ShowForm(assembly, FormName);
   current_volume = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN);
   GuiController::SendEvent("CurrentVolume", TextChange, 0, 0.0, DoubleToString(current_volume, 2));
//---
   return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Dispose form
   EventKillTimer();
   GuiController::HideForm(assembly, FormName);
//---
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
//--- refresh ask/bid   
   double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
   double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);
   GuiController::SendEvent("AskLabel", TextChange, 0, 0.0, DoubleToString(ask, Digits()));
   GuiController::SendEvent("BidLabel", TextChange, 0, 0.0, DoubleToString(bid, Digits()));
//---
}

//+------------------------------------------------------------------+
//| 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 == TextChange && el_name == "CurrentVolume")
         TrySetNewVolume(sparam);
      else if(id == ScrollChange && el_name == "IncrementVol")
         OnIncrementVolume(lparam, dparam, sparam);
      else if(id == ClickOnElement)
         TryTradeOnClick(el_name);
   }
//---
}
//+------------------------------------------------------------------+
//| Validate volume                                                  |
//+------------------------------------------------------------------+
double ValidateVolume(double n_vol)
{
   double min_vol = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN);
   double max_vol = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MAX);
   //-- check min limit 
   if(n_vol < min_vol)
      return min_vol;
   //-- check max limit
   if(n_vol > max_vol)
      return max_vol;
   //-- normalize volume
   double vol_step = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_STEP);
   double steps = MathRound(n_vol / vol_step);
   double corr_vol = NormalizeDouble(vol_step * steps, 2);
   return corr_vol;
}
//+------------------------------------------------------------------+
//| Set new current volume from a given text                         |
//+------------------------------------------------------------------+
bool TrySetNewVolume(string nstr_vol)
{
   double n_vol = StringToDouble(nstr_vol);
   current_volume = ValidateVolume(n_vol);
   string corr_vol = DoubleToString(current_volume, 2);
   GuiController::SendEvent("CurrentVolume", TextChange, 0, 0.0, corr_vol);
   return true;
}
//+------------------------------------------------------------------+
//| Execute trade orders                                             |
//+------------------------------------------------------------------+
bool TryTradeOnClick(string el_name)
{
   if(el_name == "ButtonBuy")
      return Trade.Buy(current_volume);
   if(el_name == "ButtonSell")
      return Trade.Sell(current_volume);
   return false;
}
//+------------------------------------------------------------------+
//| Increment or decrement current volume                            |
//+------------------------------------------------------------------+
void OnIncrementVolume(long lparam, double dparam, string sparam)
{
   double vol_step = 0.0;
   //-- detect increment press
   if(dparam > lparam)
      vol_step = (-1.0) * SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_STEP);
   //-- detect decrement press
   else if(dparam < lparam)
      vol_step = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_STEP);
   //-- detect increment press again
   else if(lparam == 0)
      vol_step = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_STEP);
   //-- detect decrement press again
   else
      vol_step = (-1.0) * SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_STEP);
   double n_vol = current_volume + vol_step;
   current_volume = ValidateVolume(n_vol);
   string nstr_vol = DoubleToString(current_volume, 2);
   GuiController::SendEvent("CurrentVolume", TextChange, 0, 0.0, nstr_vol);
}
//+------------------------------------------------------------------+

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

Первое, что делается в функции OnInit — это устанавливается таймер с разрешением 200 миллисекунд. Затем отображается окно с помощью метода ShowForm:

GuiController::ShowForm(assembly, FormName);

где assembly — путь к сборке, в которой находится наше окно, а FormName — имя класса нашей формы.

Сразу после того, как окно запущено, мы устанавливаем минимальный объем в текстовое поле "CurrentVolume":

GuiController::SendEvent("CurrentVolume", TextChange, 0, 0.0, DoubleToString(current_volume, 2));

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

При закрытии советника, окно формы закрывается. Делается это в функции OnDeinit, с помощью метода GuiController::HideForm; 

Функция OnTick реагирует на изменение текущей цены Ask/Bid. Таким образом, если получать текущие цены в этой функции и передавать их в соответствующие текстовые метки формы, панель будет оперативно отображать все изменения текущей цены.

//-- Получить цену Ask
double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
//-- Получить цену Bid
double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);
//-- Изменить текст в текстовой метке AskLabel на текущую цену Ask переведенную в строку:
GuiController::SendEvent("AskLabel", TextChange, 0, 0.0, DoubleToString(ask, Digits()));
//-- Изменить текст в текстовой метке BidLabel на текущую цену Bid переведенную в строку:
GuiController::SendEvent("BidLabel", TextChange, 0, 0.0, DoubleToString(bid, Digits()));

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

  • Ввести новый объем в текстовую метку CurrentVolume;
  • Нажать на кнопку увеличения или уменьшения шага объема выполненную в виде скролла;
  • Нажать на кнопку Buy или Sell, тем самым отправляя приказ на совершения сделки.

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

Событие скролла в текущей событийной модели состоит из двух параметров lparam и dparam. Первый параметр содержит условную величину, которая характеризует сдвиг каретки относительно нулевого уровня до нажатия кнопок скролла пользователем. Второй параметр содержит эту же величину, но уже после нажатия. Сам скролл имеет определенный диапазон работы, например от 0 до 100. Так если значение lparam равно 30, а значение dparam равно 50, то это значит, что вертикальный скролл был передвинут вниз с 30 до 50% (горизонтальный скролл соответственно передвинется не вниз, а вправо на аналогичную величину). В нашей панели определять где именно находится скролл не требуется. Нам необходимо лишь знать по какой кнопке нажал пользователь. Для этого нужно проанализировать предыдущее и текущее значение. Именно этим и занимается функция OnIncrementVolume. Определив тип нажатия скролла, она увеличивает или уменьшает текущий объем на минимальный шаг объема, который узнает с помощью системной функции SystemInfoDouble.

Устанавливать новый торговый объем можно не только с помощью стрелок скролла. Также его можно вводить в текстовую метку напрямую. Когда пользователь вводит новый символ, windows forms генерирует соответствующее событие. Однако нам важно проанализировать конечную строку, а не каждый символ по отдельности. Поэтому GuiController реагирует на нажатие клавиши 'Enter' или на смену фокуса текстовой метки. Именно эти события считаются окончанием ввода текста. Когда происходит одно из них, сформированный текст передается в очередь событий, которую последовательно читает наш эксперт. Добравшись до события изменения текста в метке, MQL-программа разбирает его новое значение и устанавливает новый объем согласно заданному. Анализ осуществляется с помощью функции ValidateVolume. Она контролирует следующие параметры введенного объема:

  • Объем должен быть между минимально и максимально допустимым;
  • Величина объема должна быть кратна его шагу. Так если шаг 0.01 лота, а пользователь введет цифру 1.0234, то она будет скорректирована до 1.02;

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

Давайте запустим торговую панель на графике и попробуем совершить несколько сделок с ее помощью:


Рис. 18. Работа панели в режиме реального времени. 

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


Работа графических интерфейсов в тестере стратегий

Тестер стратегий в MetaTrader 5 имеет ряд особенностей, которые должен учитывать разработчик графических интерфейсов на языке программирования MQL. Главной особенностью является тот факт, что функция обработки графических событий OnChartEvent не вызывается вовсе. Эта особенность логична, т.к. графическая форма подразумевает работу с пользователем в режиме реального времени. Однако есть тип панелей, который было бы крайне интересно реализовать именно в тестере. Это так называемые торговые плееры, с помощью которых люди могли бы тестировать свои торговые стратегии в ручном режиме. Например, тестер стратегий в ускоренном режиме генерировал текущие рыночные цены, а пользователь нажимал бы на кнопки купить или продать и тем самым симулировал свои торговые действия на истории. Именно к такому типу панелей можно отнести созданную нами TradePanel. Не смотря на свою простоту, она вполне может быть простым плеером торговли с самым необходимым функционалом. 

Но давайте подумаем, как наша панель будет работать в тестере стратегий MetaTrader 5. Графическое окно панели TradePanel существует в виде независимой сборки Net. Следовательно оно никак не зависит от текущего окружения MetaTrader 5 и даже самого терминала. Строго говоря, его можно запустить из любой другой программы, а сборки размещенные в exe-контейнере может запустить даже сам пользователь.

Таким образом, нашей программе не требуется вызов OnChartEvent. Более того, обновлять данные в окне и получать новые приказы от пользователей можно в любой функции-обработчике событий, которая регулярно запускается в тестере стратегий. К таким функциям относятся OnTick и OnTimer. Именно через них и работает наша панель. Следовательно, разработанная для работы в режиме реального времени, наша панель будет также хорошо работать и в тестере стратегий. Делать какие-либо изменения для этого не потребуется. Давайте проверим данное утверждение, запустив нашу панель в тестере и совершим несколько сделок в ней:


Рис. 19. Работа панели в режиме симуляции, в тестере стратегий.

Получается что разработка графических интерфейсов с помощью C# дает нам неожиданный бонус при работе в тестере стратегий. Для приложения Windows Forms тестер стратегий не накладывает никаких ограничений. Особенности работы событийной модели в нем не затрагивают ни панели ни способы работы с ними. Переделывать программу под работу в тестере стратегий также не нужно. 


Заключение

В статье был предложен подход с помощью которого быстро, а главное — легко, можно создать свою собственную визуальную форму. Данный подход разделяет графическое приложение на три независимые части: MQL-программу, адаптер GuiController и собственно визуальную панель. Все части приложения не зависят друг от друга. MQL-программа работает в торговом окружении MetaTrader и выполняет торговые или аналитические функции исходя из тех параметров, что она получает от панели через GuiController. Сам GuiController является назависимой программой, которую не требуется изменять при изменении формы или ее элементов. Наконец, графическая панель создается самим пользователем, с помощью развитых визуальных средств Visual Studio. Благодаря этому, для создания довольно сложной формы может даже не потребоваться знание языка программирования C#.

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

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

У предложенного подхода, как и у любой технологии, есть свои недостатки. Основной из них это невозможность работы в Маркете, т.к. вызов сторонних DLL запрещен. Во-вторых, запуск незнакомых DLL или EXE может быть небезопасным, т.к. эти модули могут содержать вредоносные функции. Однако данная проблема решается открытостью проекта. Пользователь знает, что созданная им программа априори не содержит иных элементов, кроме заданных им самим, а GuiController — это публичный проект с открытым исходным кодом. Еще одним недостатком является то, что межпрограммное взаимодействие достаточно сложный процесс. Такое взаимодействие может вызвать зависание или непредвиденное завершение работы программы. Многое зависит от разработчика, который будет разрабатывать интерфейс и взаимодействие с ним. Такую систему легче вывезти из строя, чем монолитную, написанную на чистом MQL5.

Сейчас проект находится в стадии становления. Многие пользователи не увидели в этой статье нужных им элементов управления, а текущие возможности по взаимодействию с графическими окнами, прямо скажем, еще очень ограничены. Все это верно. Однако главная задача статьи была выполнена. Мы показали, что создавать windows forms и взаимодействовать с ними проще, чем кажется. Если данный материал окажется полезным MQL-сообществу, то мы обязательно продолжим развивать это направление.

 

Прикрепленные файлы |
Source.zip (29.6 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (80)
Maxim Kuznetsov
Maxim Kuznetsov | 13 апр 2019 в 22:14
Renat Akhtyamov:

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

так?

ну да..и автор статьи этого не скрывает и наверное не против дополнений/допиливаний

в статье довольно оригинальный способ отделить логику от вида и вынести формы отдельно. но со своими проблемами - зато можно быстро визуально сваять и отдельно разрабатывать/поддерживать форму :-)

применение не освобождает от необходимости изучать С# и разбирать механику такой работы. Хорошая штука, но не "серебрянная пуля"

Vasiliy Sokolov
Vasiliy Sokolov | 14 апр 2019 в 16:23
Renat Akhtyamov:

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

так?

От Ваших вопросов хоть стой хоть падай. При чем здесь костыли, если цель была показать взаимосвязь формы с торговым экспертом? Если нужны расчеты внутри c# библиотеки - передавайте данные для расчета через свою функцию. В статье не будет 100500 функций для каждой отдельной задачи. Если свою функцию написать знаний не хватает - передавайте через string с последующей конвертацией в double - это почти элементарно.

Renat Akhtyamov
Renat Akhtyamov | 14 апр 2019 в 16:46
Vasiliy Sokolov:

От Ваших вопросов хоть стой хоть падай. При чем здесь костыли, если цель была показать взаимосвязь формы с торговым экспертом? Если нужны расчеты внутри c# библиотеки - передавайте данные для расчета через свою функцию. В статье не будет 100500 функций для каждой отдельной задачи. Если свою функцию написать знаний не хватает - передавайте через string с последующей конвертацией в double - это почти элементарно.

да есть у меня все

и так и так могем

просто этот гуи-контроллер очень сырой

а если у меня есть своя функция по передаче информации в любом виде туда и обратно, то зачем мне гуи?

получается что достаточно иметь функцию обмена с МТ и больше ничего не надо.

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

Igor Makanu
Igor Makanu | 14 апр 2019 в 17:36
Renat Akhtyamov:

да есть у меня все

и так и так могем

просто этот гуи-контроллер очень сырой

а если у меня есть своя функция по передаче информации в любом виде туда и обратно, то зачем мне гуи?

получается что достаточно иметь функцию обмена с МТ и больше ничего не надо.

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

что то Вы выкрутили свои хотелки с задунаперед )))

статья про то как в буквальном смысле слова 2 клика "прикрутить кнопки, чекбоксы, поля ввода...", т.е. графический интерфейс к своему коду на MQL5, статья с этим справилась на 100%

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

Renat Akhtyamov
Renat Akhtyamov | 14 апр 2019 в 18:05
Igor Makanu:

что то Вы выкрутили свои хотелки с задунаперед )))

статья про то как в буквальном смысле слова 2 клика "прикрутить кнопки, чекбоксы, поля ввода...", т.е. графический интерфейс к своему коду на MQL5, статья с этим справилась на 100%

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

с этой точки зрения согласен
Исследование методов свечного анализа (Часть I): Проверка существующих паттернов Исследование методов свечного анализа (Часть I): Проверка существующих паттернов

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

Утилита для отбора и навигации на MQL5 и MQL4: добавляем автоматичекий поиск паттернов с показом найденных символов Утилита для отбора и навигации на MQL5 и MQL4: добавляем автоматичекий поиск паттернов с показом найденных символов

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

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

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

Синтаксический анализ MQL средствами MQL Синтаксический анализ MQL средствами MQL

Статья описывает препроцессор, сканер и парсер для синтаксического анализа исходных кодов на MQL. Реализация на MQL прилагается.