Gerenciando o Terminal MetaTrader Via DLL

Sofiia Butenko | 4 novembro, 2015

Definição de Tarefas

Temos uma lista de ID MetaQuotes contendo mais de quatro endereços de entrega. Como sabemos, a função SendNotification usa apenas as IDs definidas na guia "Notificações" da janela "Opções". Assim, você pode enviar quatro notificações Push de cada vez às IDs especificadas anteriormente por meio da MQL. Vamos tentar corrigir isso.

O problema pode ser resolvido de duas maneiras - nós podemos desenvolver uma função para entrega de notificação push a partir do zero, ou alterar as configurações do terminal e usar a função padrão. A primeira opção é bastante demorada e carece de universalidade. Portanto, eu selecionei a segunda opção. As configurações do terminal podem também ser alterada de várias maneiras, de acordo com minha experiência, isso pode ser feito através da interface de usuário ou substituindo os valores na memória do processo. Trabalhando com a memória parece muito melhor, uma vez que permite aos usuários que evitem as janelas que ficam piscando. Porém pode perturbar o funcionamento de todo o terminal no menor erro. A pior coisa que pode acontecer quando se trabalha via UI é o desaparecimento de uma janela ou um botão.

Neste artigo vamos olhar para a gestão do terminal através da interface de usuário usando uma biblioteca DLL auxiliar. Em particular, vamos considerar a alteração das configurações. A interação com o terminal vai ser realizada da maneira habitual, ou seja, utilizando as janelas e os componentes, Não poderá acontecer nenhuma interferência com o processo do terminal. Este método pode ser aplicado para resolver outros problemas também.


1. Criação de uma DLL

Aqui vamos nos concentrar principalmente sobre o trabalho com WinAPI. Então vamos examinar de maneira breve como uma biblioteca dinâmica pode ser desenvolvido em 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 as funções Set_Check e Set_MetaQuotesID devem ser exportadas, enquanto outras são destinadas para uso interno. FindFunc olha para uma janela necessária (descrito abaixo), enquanto Find_Tab olha para uma guia necessária. As bilbiotecas Windows, Mensagens e Commctrl estão habilitadas para usar o WinAPI.


1.1. Ferramentas Aplicadas

O princípio básico de resolver esta tarefa é usando o WinAPI em ambiente Delphi XE4. С++ podem também ser utilizado, desde que a sintaxe WinAPI seja quase idêntica. A busca de nomes e classes de componentes pode ser realizada utilizando o utilitário Spy++, incluído na entrega do Visual Studio ou por uma simples enumeração descrita abaixo.


1.2. Procurando por Janelas MetaTrader

Qualquer janela do programa pode ser encontrada pelo seu título (ver Fig. 1).

Fig. 1. Título da Janela

Fig. 1. Título da Janela

Como podemos ver o título de uma janela MetaTrader contém o número da conta, enquanto o próprio título é alterado dependendo do símbolo e timeframe selecionado. Assim a pesquisa será realizada apenas pelo número da conta. Nós também devemos encontrar a janela Opções que aparece posteriormente, ela tem um título permanente.

Para o primeiro caso, vamos usar a função EnumWindows que nos permite enumerar todas as janelas disponíveis. A função para processar as janelas enumeradass é transmitida como o parâmetro EnumWindows. No nosso caso, é a função 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;

Vamos considerar essa função em mais detalhes. O cabeçalho da função permanece inalterado, com exceção da função e dos nomes das variáveis. Quando uma nova janela é detectada, a função EnumWindows chama a função específica e transmite o "handle" da janela para si. Se a função especificada retornar verdadeiro, a enumeração continua. Caso contrário, ele é completa.

Usando o "handle" recebido, podemos examinar o título da janela (GetWindowText) e nome da classe (GetClassName) copiando-o para o buffer. Em seguida devemos comparar o título da janela e classe com os necessários. Se houver uma correspondência, relembramos o "handle" (que é a coisa mais importante) e saímos da enumeração, devolvendo falso.

Agora, vamos considerar a função chamada EnumWindows.

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

Aqui nós atribuímos a classe necessária e os valores parciais do título da janela. Agora vamos chamar a função para enumerar todas as janelas disponíveis. Como resultado recebemos o "handle" da janela principal na variável global Hnd.

Olhando para frente, vamos examinar ainda uma outra janela para pesquisa de função. Uma vez que precisamos alterar as configurações do terminal, nós certamente teremos de enfrentar a nova janela Opções que aparece após a opção de menu apropriado ter sido selecionada. Não há outra maneira de encontrar a janela.

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

Nome da classe e título da janela são usados ​​como parâmetros de função, enquanto o valor retornado é um "handle" necessário ou 0 (zero) se nenhum for encontrado. Ao contrário do caso anterior, a função procura exatamente nomes correspondentes em vez de uma ocorrência de string.


1.3. Trabalhando com um Menu

Tal como todos os outros componentes, a trabalho com um menu começa após um "parent handle" (uma certa janela) ter sido encontrado. Então devemos encontrar o item e sub-item correspondente ao menu e realizar uma seleção.

Observe: a quantidade dos itens do menu do terminal altera conforme um gráfico de janela é expandido ou não (ver Fig. 2). Enumeração de item começa a partir do 0 (zero).

Fig. 2. Alterando a quantidade de itens do menu

Fig. 2. Alterando a quantidade de itens do menu

Se a quantidade de itens do menu é alterada, o número de índice de itens nas Ferramentas é alterado também. Portanto devemos considerar a quantidade total de pontos usando a função GetMenuItemCount (HND: HMEnu), transmitidos pelo "handle" de menu.

Vamos examinar o seguinte exemplo:

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

Neste exemplo, encontramos o "handle" do menu principal, através do seu "parent". Então encontramos o sub-menu apropriado pela "handle" de menu. O número de índice do sub-menu é utilizado como o segundo parâmetro da função GetSubMenu. Depois disso encontramos o item do sub-menu apropriado. Para executar uma seleção, temos de enviar uma mensagem adequada. Depois de enviar a mensagem, nós temos que esperar pela janela Opções.

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

Não é recomendado definir um loop infinito, pois pode causar a queda do terminal depois de fechar a janela, apesar da rápida operação do programa.


1.4. Procurando por Componentes

Nós obtivemos a janela de opções e agora temos de abordar seus componentes ou "child windows" (usando termos WinAPI), mas primeiro devemos encontrá-los usando o "handle". O termo "child window" é utilizado por uma razão, nós procuramos pelos componentes da mesma forma que olhamos para as janelas.

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

ou

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

Assim observamos os principais exemplos de busca dos componentes. Nesta fase não enfrentamos quaisquer complicações específicas, além de nomes de funções alteradas e transmissão adicional do "parent handle". As dificuldades surgem normalmente se você necessita considerar as peculiaridades dos componentes e conhecer quais os títulos ou classes dos componentes, para isto uma pesquisa é realizada. Nesse caso o utilitário Spy++ pode servir, bem como a enumeração de todos os componentes da janela "parent" seguidos pela exibição de todos os valores. Para isto necessitamos mudar um pouco a função transmitida (FindFunc) - definindo o valor de retorno para "true" em todos os casos e salvando os nomes das janelas e suas classes (por exemplo, escrevendo-as num arquivo).

Vamos examinar um dos recursos da pesquisa de componentes: ОК é um botão do sistema. Isso significa que o texto do botão é escrito em caracteres latinos no Sistema Operacional Windows em inglês, já na versão russa é escrito em caracteres cirílicos, ou seja, esta solução não é universal.

A pesquisa baseia-se no fato de que o comprimento do nome (pelo menos para idiomas utilizando caracteres Latinos e Cirílicos) é constituído por dois caracteres. Isto já torna a biblioteca mais versátil. A função de busca neste caso é a seguinte:

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;

Por conseguinte, a busca pelo botão OK é realizada da seguinte forma:

EnumChildWindows(HndParent, @FindFuncOK, 0);

1.5. Trabalhando com Componentes

Devemos receber a seguinte janela como resultado de todas as nossas ações (Fig. 3):

Fig. 3. Janela Opções

Fig. 3. Janela Opções

TabControl

A janela contém várias guias e não temos certeza da solicitação ter sido selecionada. O componente responsável pelo conjunto das guias é o TabControl ou SysTabControl32, como indicado na sua classe. Vamos procurar pelo seu "handle". A janela Opções é usada como seu "parent":

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

Então enviamos uma mensagem para mudança da guia neste componente:

SendMessage(Hnd, TCM_SETCURFOCUS, 5, 0);

No exemplo acima o número 5 é um índice da guia necessária (Notificações). Agora procuramos a guia necessária:

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

A janela Opções é usada como um "parent" para uma guia ativa, desde que nó temos o "handle" TabControl, tomamos o "handle" de seu "parent" (a janela). Após isto é realizada a busca da guia necessária, ou seja, a classe da guia é "#32770".


CheckBox

Como verificamos a janela Opções possue o item "Ativar Notificações Push". Claro que não esperamos a configuração correta de tudo por um usuário. O componente responsável pela ativação/desativação tem a classe Button e existem mensagens projetadas especificamente para este tipo de componente.

Primeiro vamos procurar o componente, a guia Notificações atua como a principal. Se o componente for encontrado, então verificamos se as notificações são permitidas (se a opção foi selecionada ou não). Se não estiver selecionada, verificamos. Todas as ações com o objeto são realizadas pelos envio de mensagens.

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);
         ...


Editar

Este componente é um campo para a entrada dos endereços das ID MetaQuotes. Sua guia principal é Notificações, enquanto a classe é Edit. O princípio de funcionamento é o mesmo - encontrar o componente e enviar uma mensagem.

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

Onde Str_Set é uma lista de endereços dea string.


Botão

Agora, vamos examinar o botão padrão OK na parte inferior da janela Opções. Este componente não pertence a qualquer guia, então a sua guia principal é a janela em si. Após concluir todas as ações necessárias devemos enviar uma mensagem.

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


2. Criando um script em MQL4

O resultado do nosso trabalho é uma DLL com as duas funções externas Set_Check e Set_MetaQuotesID que permitem o envio de notificações Push e preenchem o campo com endereços de IDs MetaQuotes da lista em conformidade. Se todas as janelas e componentes do terminal são encontradas nas funções, elas retornam verdadeiro. Agora veremos como podem ser usadas no 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;
        }
     }
  }
//+------------------------------------------------------------------+


Conclusão

Observamos os princípios básicos da gestão das janelas do terminal via DLL que permitem utilizar todas as funcionalidades dos terminais de forma mais eficiente. Este método deve ser usado apenas como um último recurso no caso de um problema que não possa ser resolvido através dos métodos convencionais, pois tem várias desvantagens, incluindo a dependência da linguagem do terminal selecionada, a intervenção do usuário e a complexidade de implementação. Se mal utilizado poderá causar erros fatais ou mesmo quebrar o programa.