Скачать MetaTrader 5

Управление терминалом MetaTrader с помощью DLL

21 июля 2015, 16:32
Galina Bobro
8
10 537

Постановка задачи

Имеется список MetaQuotesID, который составляет более четырех адресов для рассылки. Как известно, функция SendNotification использует только ID, указанные в окне настроек на вкладке "Уведомления". Таким образом, средствами MQL можно делать рассылку только на указанные ранее ID и не более четырех за раз. Попробуем исправить данную ситуацию.

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

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


1. Создание DLL

В данной статье мы сосредоточимся в основном на работе с WinApi, поэтому в сокращенном варианте рассмотрим построение динамической библиотеки на Delphi.

library Set_Push;

uses
  Windows,
  messages,
  Commctrl,
  System.SysUtils;

var
   windows_name:string;
   class_name:string;
   Hnd:hwnd;


{$R *.res}
{$Warnings off}
{$hints on}

function FindFunc(h:hwnd; L:LPARAM): BOOL; stdcall;
begin
  ...
end;

function FindFuncOK(h:hwnd; L:LPARAM): BOOL; stdcall;
begin
  ...
end;

function Find_Tab(AccountNumber:integer):Hwnd;
begin
  ...
end;


function Set_Check(AccountNumber:integer):boolean; export; stdcall;
var
  HndButton, HndP:HWnd;
  i:integer;
  WChars:array[0..255] of WideChar;
begin
   ...
end;

function Set_MetaQuotesID(AccountNumber:integer; Str_Set:string):boolean; export; stdcall;
begin
  ...
end;

//--------------------------------------------------------------------------/
Exports Set_Check, Set_MetaQuotesID;

begin
end.

Как можно увидеть из кода, функции Set_Check и Set_MetaQuotesID будут экспортированы, остальные функции — для внутреннего использования. FindFunc используется для поиска нужного окна (рассмотрим далее), а Find_Tab — для поиска нужной вкладки. Для использования WinApi подключены библиотеки Windows, Messages, Commctrl.


1.1. Используемые инструменты

Основной принцип решения данной задачи заключается в использовании WinApi в среде Delphi XE4. При желании можно использовать и С++, так как синтаксис WinApi практически идентичен. Поиск наименований компонент и их классов можно осуществлять с использованием утилиты Spy++, которая включена в поставку продукта Visual Studio, или путем обычного перебора, что будет рассмотрено ниже.


1.2. Нахождение окон MetaTrader

Найти окно любой программы можно по его заголовку (см. рис. 1).

Рис. 1. Заголовок окна

Рис. 1. Заголовок окна

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

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

function FindFunc(h:hwnd; L:LPARAM): BOOL; stdcall;
var buff: ARRAY [0..255] OF WideChar;
begin
   result:=true;
   Hnd := 0;
   GetWindowText(h, buff, sizeof(buff));
   if((windows_name='') or (pos(windows_name, StrPas(buff))> 0)) then begin
      GetClassName(h, buff, sizeof(buff));
      if ((class_name='') or (pos(class_name, StrPas(buff))> 0)) then begin
         Hnd := h;
         result:=false;
      end;
   end;
end;

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

С помощью получаемого хэндла мы можем посмотреть заголовок окна (GetWindowText) и название его класса (GetClassName), скопировав это название в буфер. Далее, сверяем заголовок окна и его класс с искомыми. Если совпадение есть, то, самое главное, запоминаем хэндл и выходим из перечисления, то есть возвращаем false.

Теперь рассмотрим сам вызов функции EnumWindows.

windows_name:=IntToStr(AccountNumber);
class_name:='MetaTrader';
EnumWindows(@FindFunc, 0);

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

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

hnd:=FindWindow(nil, 'Настройки');

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


1.3. Работа с меню

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

Тут необходимо сделать важное замечание: количество пунктов меню в терминале изменяется в зависимости от того, развернуто окно графика или нет (см. рис. 2). Нумерация пунктов меню начинается с 0.

Рис. 2. Изменение количества пунктов меню

Рис. 2. Изменение количества пунктов меню

При изменении количества пунктов меню, соответственно, изменяется и порядковый номер пункта "Сервис". Поэтому при работе учитываем общее количество пунктов с помощью функции GetMenuItemCount(Hnd:HMenu), в которую передается хэндл меню.

Рассмотрим на примере:

function Find_Tab(AccountNumber:integer; Language:integer):Hwnd;
var
  HndMen :HMenu;
  idMen:integer;
  ...
begin
   ...
   //_____работа в меню________
   HndMen:=GetMenu(Hnd);
   if (GetMenuItemCount(HndMen)=7) then
      HndMen:=GetSubMenu(HndMen,4)
   else
      HndMen:=GetSubMenu(HndMen,5);
   idMen:=GetMenuItemID(HndMen,6);
   if idMen<>0 then begin
      PostMessage(Hnd,WM_COMMAND,idMen,0);
      ...

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

for i := 0 to 10000 do 
   hnd:=FindWindow(nil, 'Настройки');

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


1.4. Нахождение компонентов

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

windows_name:='ОК';
class_name:='Button';
EnumChildWindows(HndParent, @FindFunc, 0);

или так

Hnd:=FindWindowEx(HndParent, 0, 'Button', 'OK');

Таким образом, мы рассмотрели основные примеры поиска компонентов. На этом этапе кажется, что здесь нет ничего сложного, кроме измененных имен функций и дополнительной передачи хэндла родителя. Сложности возникают из-за необходимости учитывать особенности компонент и необходимости узнать заголовки или классы компонентов, по которым будет вестись поиск. Помочь в этом может утилита Spy++ или перебор всех компонентов у окна-родителя с последующим выводом значений. Для этого необходимо немного изменить передаваемую функцию (FindFunc): прописать возвращаемое значение всегда true и сделать вывод, например, в файл названий окон и их классов.

Рассмотрим одну из особенностей поиска компонент: кнопка "ОК" является системной, то есть ее текст в англоязычной Windows будет написан латиницей, а в русской версии — кириллицей. Поэтому данное решение никак не может претендовать на универсальность.

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

function FindFuncOK(h:hwnd; L:LPARAM): BOOL; stdcall;
var buff: ARRAY [0..255] OF WideChar;
begin
   result:=true;
   Hnd := 0;
   GetClassName(h, buff, sizeof(buff));
   if (pos('Button', StrPas(buff))> 0) then begin
      GetWindowText(h, buff, sizeof(buff));
      if(Length(StrPas(buff))=2) then  begin
         Hnd := h;
         result:=false;
      end;
   end;
end;

Соответственно, поиск кнопки "ОК" будет выглядеть следующим образом:

EnumChildWindows(HndParent, @FindFuncOK, 0);


1.5. Работа с компонентами

В итоге всех манипуляций мы должны получить следующее открытое окно (рис. 3):

Рис. 3. Окно настроек

Рис. 3. Окно настроек

TabControl

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

Hnd:=FindWindowEx(Hnd, 0, 'SysTabControl32', nil);

Далее необходимо отправить этому компоненту сообщение на смену вкладки:

SendMessage(Hnd, TCM_SETCURFOCUS, 5, 0);

В данном примере 5 — это номер необходимой вкладки, то есть "Уведомления". Теперь можно производить поиск необходимой вкладки:

Hnd:=GetParent(Hnd);
Hnd:=FindWindowEx(Hnd, 0, '#32770', 'Уведомления');

Для активной вкладки родителем будет окно "Настройки" и, поскольку у нас был хэндл TabControl, мы берем хэндл его родителя, то есть окна, после чего выполняем поиск нужной вкладки. Соответственно, класс вкладки — это "#32770".


CheckBox

Как видно, в окне настроек есть опция разрешения отправки Push-сообщений и, разумеется, мы не будем надеяться, что пользователь указал все правильно. Компонент, который отвечает за разрешение или запрет, имеет класс "Button", то есть это кнопка, однако есть сообщения, предназначенные именно для этого вида компонент.

Для начала осуществляем поиск компонента, родителем для него будет вкладка "Уведомления". Далее, если успешно нашли компонент, проверяем, разрешены ли сообщения, то есть поставлена ли галочка, и если нет, то устанавливаем. Все действия с объектом производим посредством отправки сообщений.

Hnd:=FindWindowEx(Hnd, 0, 'Button', 'Разрешить Push-уведомления');
if(Hnd<>0) then begin
   if (SendMessage(Hnd,BM_GETCHECK,0,0)<>BST_CHECKED) then
      SendMessage(Hnd,BM_SETCHECK,BST_CHECKED,0);
         ...


Edit

Этот компонент представляет собой поле для ввода адресов MetaQuotes ID. Его родителем будет все та же вкладка "Уведомления", класс — "Edit". Схема работы такая же — находим и отправляем сообщение.

Hnd:=FindWindowEx(Hnd, 0, 'Edit', nil);
if (Hnd<>0) then begin
   SendMessage(Hnd, WM_Settext,0,Integer(Str_Set));

где Str_Set — это список адресов типа string.


Button

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

EnumChildWindows(HndParent, @FindFuncOK, 0);
I:=GetDlgCtrlID(HndButton);
if I<>0 then begin
   SendMessage(GetParent(HndButton),WM_Command,MakeWParam(I,BN_CLICKED),HndButton);
   ...


2. Создание скрипта на MQL4

Результатом нашей предыдущей работы стала DLL c двумя внешними функциями Set_Check и Set_MetaQuotesID, которые, соответственно, устанавливают разрешение на отправку Push-сообщений и заполняют поле адресами MetaQuotes ID из списка. Если все окна и компоненты терминала были найдены в функциях, то они возвращают true. Покажем пример их использования в скрипте.

//+------------------------------------------------------------------+
//|                                                    Send_Push.mq4 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict
#property show_inputs

#import "Set_Push.dll"
bool Set_Check(int);
bool Set_MetaQuotesID(int,string);
#import

extern string text="test";
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart()
  {
   if(StringLen(text)<1)
     {
      Alert("Error: Нет текста для отправки"); return;
     }
   if(!Set_Check(AccountNumber()))
     {
      Alert("Error: Не удалось разрешить отправку push. Проверьте язык терминала"); return;
     }
   string str_id="1C2F1442,2C2F1442,3C2F1442,4C2F1442";
   if(!Set_MetaQuotesID(AccountNumber(),str_id))
     {
      Alert("Error: Ошибка выполнения dll! Возможно, вмешательство в процесс"); return;
     }
   if(!SendNotification(text))
     {
      int Err=GetLastError();
      switch(Err)
        {
         case 4250: Alert("Waiting: Ошибка отправления ", str_id); break;
         case 4251: Alert("Err: Неверный текст сообщения ", text); return; break;
         case 4252: Alert("Waiting: Неверный список ID ", str_id); break;
         case 4253: Alert("Err: Слишком частые запросы! "); return; break;
        }
     }
  }
//+------------------------------------------------------------------+

Заключение

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

Прикрепленные файлы |
Send_Push.mq4 (2.36 KB)
set_push.zip (3.96 KB)
Sergey Dzyublik
Sergey Dzyublik | 22 июл 2015 в 02:10
Игорь Герасько:

По поводу самого решения возникли мысли и вопросы: 

1. Для вызова окна "Настройки" можно было бы использовать более универсальный способ: эмуляция нажатия  Ctrl + O. Вариант с меню не настолько уж и универсальный, т. к. зависит от расположения пунктов главного меню. Да, можно сказать, что и Ctrl+O никто не гарантирует. Тем не менее, изменение горячих клавиш в процессе развития приложения происходит достаточно редко и для этого должны быть веские причины.

а что 
 PostMessageW(hParentWnd, WM_COMMAND, 33265, 0);

Больше не работает ? 

Andrey Osorgin
Andrey Osorgin | 22 июл 2015 в 09:26
Игорь Герасько:
Имелось в виду часть предложения: "пользователь может его закрыть окно". Возможно, здесь лишнее "его". А может и что-то другое.
Исправлено, спасибо за замечание.
Ihor Herasko
Ihor Herasko | 22 июл 2015 в 19:43
Sergey Dzyublik:
а что 
 PostMessageW(hParentWnd, WM_COMMAND, 33265, 0);

Больше не работает ? 

Не проверял. Но к подобного рода решениям отношусь с осторожностью, т. к. они недокументированы. Следовательно, могут в любой момент "поломаться" и нигде об этом никто ничего не напишет. В то же время программист будет долго ломать голову, прежде чем найдет возникшую ошибку. А вот Ctrl+O на данный момент является документированной возможностью терминала. Поэтому ее можно смело использовать. При изменениях в поведении терминала от разработчиков обязательно последуют изменения документации.
Dmitry Orlov
Dmitry Orlov | 11 авг 2015 в 21:51

вот это я называю mad skills !

способ поражает одновременно и простотой и изощрённостью ;)

Denis Sartakov
Denis Sartakov | 5 мар 2017 в 23:21
Dmitry Orlov:

вот это я называю mad skills !

способ поражает одновременно и простотой и изощрённостью ;)


интересно, кто-нибудь это все проверял ?

п‌ереписываю все на С++, здесь вот что-то странное:

HWND cSetPush::FindTab(int i_AccountNumber,int i_Language)
{
  TCHAR t_Buffer[255];
  HWND  h_Button;
  HWND  h_Parent;
  HMENU h_Menu;
  int i;
  UINT ui_MenuItemID;

  _itow(i_AccountNumber,gt_MT_WindowName,10);

  wcscpy(gt_MT_WindowClassName,TEXT("MetaTrader"));

  EnumWindows(&FindFunc, 0);

  if (gh_MT_Window != NULL)
  {
          SetForegroundWindow(gh_MT_Window);

          h_Menu = GetMenu(gh_MT_Window);

          if (GetMenuItemCount(h_Menu) == 7)
          h_Menu = GetSubMenu(h_Menu,4);
          else
          h_Menu = GetSubMenu(h_Menu,5);

          ui_MenuItemID = GetMenuItemID(h_Menu,6);

h_Menu - это handle для Tools

Tools содержит

0‌ - New Order

1‌ - History Center

2 - Global Variables

3 - MetaEditor

4 - Options

‌надо наверное так:

ui_MenuItemID = GetMenuItemID(h_Menu,4);

Price Action. Автоматизация торговли по внутреннему бару Price Action. Автоматизация торговли по внутреннему бару

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

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

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

Оценка эффективности торговых систем путем анализа их компонентов Оценка эффективности торговых систем путем анализа их компонентов

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

Price Action. Автоматизация торговли по паттерну "Поглощение" Price Action. Автоматизация торговли по паттерну "Поглощение"

В статье описывается создание советника для MetaTrader 4, торгующего по паттерну "Поглощение", включая принцип нахождения паттерна, правила установки отложенных и стоп-ордеров. Приведены результаты тестирования и оптимизации.