Manejar el Terminal de MetaTrader mediante DLL

Sofiia Butenko | 10 mayo, 2016

Planteamiento de la tarea

Tenemos una lista MetaQuotes ID que contiene más de cuatro direcciones de entrega. Como se sabe, la función SendNotification usa únicamente el ID establecido en la pestaña "Notificaciones" de la ventana "Opciones". MQL sólo permite enviar notificaciones push al ID establecido anteriormente y no más de cuatro a la vez. Vamos a tratar de arreglar esto.

Se puede solucionar esta limitación de dos maneras; podemos desarrollar la función de entrega de notificaciones push desde cero o cambiar la configuración del terminal y utilizar la función estándar. La primera opción consume mucho tiempo y no es muy versátil. Así que he optado por la segunda opción. Además, se puede cambiar la configuración del terminal de distintas maneras. Según mi experiencia, se puede hacer esto mediante la interfaz de usuario o sustituyendo valores en la memoria del proceso. El trabajo con la memoria parece mucho mejor ya que permite a los usuarios evitar las ventanas emergentes. Pero el más mínimo error puede interferir en el funcionamiento del todo el terminal. Y lo peor que puede pasar si trabajamos con la interfaz de usuario es que desaparezca una ventana o un botón.

En este artículo voy a tratar de manejar el terminal a través de la interfaz de usuario y mediante una librería DLL adicional. En particular, voy a considerar el cambio de configuración. Se llevará a cabo la comunicación con el terminal como se suele hacer habitualmente, es decir mediante sus ventanas y elementos. No hay ninguna interferencia con el terminal durante el proceso. Este método puede utilizarse también para resolver otros problemas.


1. Crear una librería DLL

Nos vamos a centrar principalmente en trabajar con WinAPI. Así que vamos a analizar de forma breve cómo se puede desarrollar una librería de enlace dinámico en 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.

Como podemos ver, las funciones Set_Check y Set_MetaQuotesID tienen salidas, mientras que las otras son para un uso interno. Se usa FindFunc para buscar la ventana requerida (está descrita más adelante) y Find_Tab para encontrar la pestaña requerida. Las librerías Windows, Messages y Commctrl están habilitadas para poder utilizar WinAPI.


1.1. Herramientas utilizadas

El principio básico para solucionar este problema es el uso de WinAPI en el entorno Delphi XE4. También se puede utilizar C++, ya que la sintaxis de WinAPI es casi idéntica. Se puede llevar a cabo la búsqueda de los nombres de los elementos y las clases mediante la utilidad Spy++ incluida en Visual Studio, o mediante la simple enumeración que se describe a continuación.


1.2. Búsqueda de las ventanas de MetaTrader

Se puede encontrar la ventana de cualquier programa por su título (ver la figura 1).

Fig. 1 Título de la ventana

Fig. 1 Título de la ventana

Como se puede observar, el título de la ventana de MetaTrader incluye el número de cuenta, y este título cambia en función del símbolo seleccionada y del período de tiempo. Por lo tanto, la búsqueda se llevará a cabo mediante el número de cuenta sólo. También tenemos que encontrar la ventana "Opciones" que aparece después. Su título no cambia.

En el primer caso usaremos la función EnumWindows que nos permite enumerar todas las ventanas disponibles. Se envía la función para procesar las ventanas enumeradas como un parámetro EnumWindows. En nuestro caso, es la función 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;

Veamos esta función más de cerca. El encabezamiento de la función tiene que ser el mismo, excepto los nombres de las funciones y las variables. Al detectarse una nueva ventana, la función EnumWindows llama a la función especificada y le pasa el identificador de la ventana. Si la función especificada devuelve true, la enumeración continúa. De lo contrario, se detiene.

Mediante el identificador recibido, podemos examinar el título de la ventana (GetWindowText) y el nombre de la clase (GetClassName) mediante su copia en el buffer. A continuación, comparamos el título de la ventana y la clase con el título y la clase requeridos. Si coinciden, almacenamos el identificador (que es lo más importante) y salimos de la enumeración devolviendo false.

Vamos a ver ahora la llamada de la función EnumWindows.

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

Tenemos que asignar la clase requerida y parte de los valores del título de la ventana. Vamos a llamar ahora a la función para la enumeración de todas las ventanas disponibles. Como resultado, recibimos el identificador de la ventana principal en la variable global Hnd.

Mirando hacia el futuro, vamos a ver otra función de búsqueda de ventanas. Puesto que queremos modificar la configuración del terminal, tendremos sin duda que tratar con la ventana "Opciones" que aparece después de seleccionar la opción adecuada del menú. Hay otra manera de encontrar la ventana.

hnd:=FindWindow(nil, 'Options');

Los parámetros de la función son el nombre de la clase y el título de la ventana, mientras que el valor devuelto es el identificador requerido o 0 si no se encuentra ninguno. A diferencia del caso anterior, esta función busca la coincidencia exacta de los nombres en lugar de la coincidencia parcial con la cadena.


1.3. Trabajar con un menú

Al igual que con los demás elementos, el trabajo con el menú empieza después de encontrar el identificador raíz (una ventana concreta). Después, tenemos que encontrar la opción del menú o submenú correspondiente y hacer la selección.

Tenga en cuenta que: la cantidad de elementos del menú en el terminal cambia dependiendo de si la ventana del gráfico está expandida o no (ver figura 2). La enumeración de los elementos empieza desde 0.

Fig. 2 Cambio de la cantidad de elementos del menú

Fig. 2 Cambio de la cantidad de elementos del menú

Si la cantidad de elementos del menú cambia, el número de índice del elemento "Herramientas" (Tools) cambia también. Por lo tanto, hay que tener en cuenta el número total de elementos mediante la función GetMenuItemCount(Hnd:HMenu) que recibe el identificador del menú.

Veamos el siguiente ejemplo:

function Find_Tab(AccountNumber:integer; Language:integer):Hwnd;
var
  HndMen :HMenu;
  idMen:integer;
  ...
begin
   ...
   //_____working in the menu________
   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);
      ...

En este ejemplo, encontramos el identificador del menú principal mediante su identificador raíz. Luego encontramos el submenú apropiado mediante el identificador del menú. Se usa el número de índice del submenú como un segundo parámetro de la función GetSubMenu. Después de esto encontramos el elemento del submenú apropiado. Para hacer la selección, tenemos que enviar el mensaje apropiado. Después de enviar el mensaje, tenemos que esperar a que aparezca la ventana "Opciones".

for i := 0 to 10000 do 
   hnd:=FindWindow(nil, 'Options');

No se recomienda usar un bucle infinito, ya que puede causar un fallo en el terminal después de cerrar la ventana a pesar del funcionamiento rápido del programa.


1.4. Búsqueda de los elementos

Ya hemos conseguido la ventana "Opciones" y ahora tenemos que solicitar sus elementos o sus ventanas secundarias (mediante WinAPI). Pero primero tenemos que encontrarlos mediante el identificador. El término "ventanas secundarias" no es casual, puesto que se hace su búsqueda del mismo modo que las ventanas.

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

o

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

Así que hemos observado los principales ejemplos de búsqueda de los elementos. No hemos encontrado grandes dificultades en esta etapa, excepto con el cambio de nombre de las funciones y el envío de los parámetros adicionales del identificador raíz. Las dificultades surgen cuando hay que tener en cuenta las características de los elementos y conocer sus títulos o clases, mediante los cuales se hace la búsqueda. En este caso, la utilidad Spy++ puede ser de gran ayuda, así como la enumeración de todos los elementos de la ventana principal, seguida de una pantalla con todos los valores. Para conseguirlo, tenemos que modificar ligeramente la función enviada (FindFunc), fijar el valor devuelto en true en todos los casos y almacenar los nombres y las clases de las ventanas (por ejemplo, escribirlos en un archivo).

Vamos a analizar una de las funciones de búsqueda del elemento: ОК es un botón del sistema. Es decir que el texto del botón se escribe en caracteres latinos en la versión inglesa de Windows, mientras que en la versión rusa se escribe en caracteres cirílicos. Por lo tanto, la solución no es universal.

Se basa la búsqueda en la longitud del nombre (por lo menos en los lenguajes que usan los caracteres latino y cirílico), en este caso, está compuesto de dos caracteres. Esto hace que la librería sea más versátil. La función de búsqueda para este caso sería así:

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;

En consecuencia, la búsqueda del botón "OK" se hará de la siguiente manera:

EnumChildWindows(HndParent, @FindFuncOK, 0);


1.5. Uso de los elementos

Como resultado de las operaciones anteriores, obtendremos la siguiente ventana (figura 3):

Fig. 3 La ventana "Opciones"

Fig. 3 La ventana "Opciones"

TabControl

Esta ventana contiene muchas pestañas y no podemos estar seguros si ha sido seleccionada la pestaña requerida. El elemento que se encarga de seleccionar las pestañas es TabControl, o en este caso, SysTabControl32, como se indica en su clase. Vamos a buscar su identificador. Se usa "Opciones" como su ventana raíz.

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

Después enviamos un mensaje de cambio de pestaña a este elemento:

SendMessage(Hnd, TCM_SETCURFOCUS, 5, 0);

En el ejemplo anterior, 5 es el número de índice de la pestaña requerida (Notificaciones). Ahora podemos buscar la pestaña requerida:

Hnd:=GetParent(Hnd);
Hnd:=FindWindowEx(Hnd, 0, '#32770', 'Notifications');

Se usa "Opciones" como ventana raíz de una pestaña activa. Mientras no tenemos el identificador de TabControl usaremos el identificador de su raíz (la ventana). A continuación se lleva a cabo la búsqueda de la pestaña requerida. En consecuencia, la clase de la pestaña es "#32770".


CheckBox

Como se puede observar, la ventana "Opciones" tiene la opción "Permitir notificaciones push", y por supuesto, no hay que esperar a que el usuario haya configurado todo correctamente. El elemento responsable de activar y/o desactivar esta opción es de la clase Button, y existen mensajes diseñados especialmente para este tipo de elementos.

Primero, vamos a buscar el elemento. Su raíz es la pestaña "Notificaciones". Si no se encuentra el elemento, comprobamos si se permiten las notificaciones o no (si está marcada la casilla o no). Si no lo está, la marcamos. Se llevan a cabo todas las operaciones con los objetos mediante el envío de mensajes.

Hnd:=FindWindowEx(Hnd, 0, 'Button', 'Enable Push Notifications');
if(Hnd<>0) then begin
   if (SendMessage(Hnd,BM_GETCHECK,0,0)<>BST_CHECKED) then
      SendMessage(Hnd,BM_SETCHECK,BST_CHECKED,0);
         ...


Edit

Este elemento es el campo de entrada de las direcciones de MetaQuotes ID. Su raíz es la pestaña "Notificaciones" también y es de la clase Edit. El principio de funcionamiento es el mismo; encontrar el elemento y enviar un mensaje.

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

Donde Str_Set es una lista de direcciones de tipo string (cadena).


Button

Veamos ahora el botón estándar "OK" (Aceptar) en la parte inferior de la ventana "Opciones". Este elemento no pertenece a ninguna pestaña, lo que significa que su raíz es la propia ventana. Después de completar todas las operaciones necesarias, le tenemos que enviar un mensaje de presión del botón.

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


2. Crear un script en MQL4

El resultado de nuestro trabajo es una DLL con dos funciones externas Set_Check y Set_MetaQuotesID que permiten enviar notificaciones push y rellenar el campo con las direcciones MetaQuotes ID a partir del listado correspondiente. Se encuentran todas las ventanas y elementos del terminal en las funciones, estas devuelven true. Veamos ahora cómo se pueden utilizar en el script.

//+------------------------------------------------------------------+
//|                                                    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: No text to send"); return;
     }
   if(!Set_Check(AccountNumber()))
     {
      Alert("Error: Failed to enable sending push. Check the terminal language"); return;
     }
   string str_id="1C2F1442,2C2F1442,3C2F1442,4C2F1442";
   if(!Set_MetaQuotesID(AccountNumber(),str_id))
     {
      Alert("Error: dll execution error! Possible interference with the process"); return;
     }
   if(!SendNotification(text))
     {
      int Err=GetLastError();
      switch(Err)
        {
         case 4250: Alert("Waiting: Failed to send ", str_id); break;
         case 4251: Alert("Err: Invalid message text ", text); return; break;
         case 4252: Alert("Waiting: Invalid ID list ", str_id); break;
         case 4253: Alert("Err: Too frequent requests! "); return; break;
        }
     }
  }
//+------------------------------------------------------------------+

Conclusión

Hemos visto los principios básicos para manejar las ventanas del terminal mediante DLL, lo que nos permitirá utilizar todas las funciones del terminal de manera más eficiente. Sin embargo, hay que tener en cuenta que este método tiene que ser el último recurso cuando no se pueden solucionar los problemas mediante los métodos convencionales, ya que tiene varios inconvenientes, como la dependencia del lenguaje del terminal seleccionado, la intervención del usuario y la complejidad de implementación. Un uso erróneo puede provocar errores fatales e incluso un fallo general del programa.