Criando interfaces gráficas baseadas no .Net Framework e C# (Parte 2): elementos gráficos adicionais

13 junho 2019, 09:29
Vasiliy Sokolov
0
2 040

Sumário


Introdução

A partir de outubro de 2018, o MetaTrader 5 começou a suportar integração com as bibliotecas do .Net Framwork. Esse conjunto de bibliotecas é, na verdade, muito mais do que qualquer estrutura ou sistema especializado para executar uma faixa específica de tarefas, como desenhar janelas gráficas ou implementar interação de rede. No .Net Framework, há literalmente absolutamente tudo. Com ele, podem se desenvolver websites (Net Core, MVC), escrever aplicativos de sistema com uma interface unificada e profissional (Windows Forms), criar sistemas distribuídos complexos com troca de informações entre nós (Windows Communication Fundation), trabalhar com bancos de dados (Entity Framework). Afinal, o .Net Framework é uma enorme comunidade de programadores e empresas com milhares de projetos de código aberto de conteúdo muito diferente. Tudo isso, com a devida interação, hoje pode estar disponível em MQL.

Neste artigo, continuaremos a desenvolver a funcionalidade GuiController, criado na primeira parte. Essa funcionalidade é destinada a interagir com a funcionalidade gráfica do .Net Framework com base na tecnologia Windows Forms. Muito tem sido escrito sobre as possibilidades gráficas da MQL. No momento, existem muitas bibliotecas diferentes que, em diferentes graus, fazem algo semelhante, mas usando ferramentas MQL. Por isso, eu não gostaria que esse material fosse percebido pelos leitores como mais uma biblioteca para trabalhar com formulários. De fato, este material é apenas parte de uma série grande de artigos descrevendo a interação com o ,Net Framework e, passo a passo, abrindo o universo ilimitado desta plataforma de software. O Windows Forms possui apenas um dos blocos de construção, embora seja muito conveniente e abrangente, como qualquer parte da tecnologia Net. O subsistema gráfico do Windows Forms é um ótimo ponto de partida para explorar essa estrutura. Depois de estudar o Windows Forms, ele pode se usar em outras áreas de interação com o .Net Framework, bem como criar com ele painéis de negociação, janelas de configurações avançadas, indicadores gráficos avançados, sistemas de controle de robôs bastante efetivos e simples de implementar, em geral, tudo o relacionado à interação do usuário e da plataforma de negociação.

No entanto, para implementar todos esses interessantes recursos, é preciso complementar significativamente o módulo de interação entre o programa MQL e a biblioteca C#. Lembre que, na primeira parte, nosso módulo GuiController era capaz de interagir apenas com alguns elementos gráficos do WinForms, como botões (Button), rótulos de texto (Label), campos de texto para inserir texto (TextBox) e rolagem vertical. Apesar desse escasso suporte de elementos, conseguimos criar um painel gráfico completo e bastante funcional:

Fig. 1. Painel de negociação criado na primeira parte do artigo

Apesar de um resultado bastante impressionante, não vamos parar por aí e continuaremos melhorando nosso controle. Nesta parte do artigo, forneceremos elementos gráficos adicionais com os quais é possível criar a maioria dos tipos de formulários.


Testando novos elementos

Para introduzir suporte para novos elementos, é necessário criar uma espécie de parâmetro de comparação. Isso deve ser feito para depurar o trabalho com novos elementos e eliminar erros emergentes e potenciais que aparecerão durante a introdução da nova funcionalidade. Nosso parâmetro de comparação consistirá em nosso controlador, nos formulários com os conjuntos de elementos gráficos necessários e num EA que processará esses elementos. Todos os formulários estarão localizados dentro da mesma montagem DemoForm.exe. Dentro do EA, criaremos um parâmetro de usuário especial que lhe dirá exatamente qual formulário gráfico do DemoForm.exe precisa ser carregado:

 

Fig. 2 Selecionando formulário personalizado com elementos necessários

O EA de testes em si será bastante simples. Essencialmente, ele consistirá em duas partes: a função de carregamento (a função de inicialização OnInit padrão) e o manipulador de eventos gráficos (o loop de eventos na função OnTimer). Lembre-se que o trabalho com o GuiController é feito através de uma chamada para métodos estáticos. Os métodos principais são quatro:

  1. Show Form - executa um formulário a partir de um build específico;
  2. HideForm - oculta a forma;
  3. GetEvent - obtém o evento a partir do formulário;
  4. SendEvent - envia o evento ao formulário.

Na função OnInit, dependendo do elemento selecionado, carregamos a janela que precisamos. O protótipo da função é mostrado abaixo:

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

Na função OnTimer, nós manipularemos eventos que chegam a partir do formulário:

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

Assim, para cada um dos elementos gráficos, criaremos um breve exemplo de trabalho para interagir com ele. Também descreveremos em detalhes os eventos que ele suporta.


MessageBox (caixa de diálogo)

Começando com a segunda versão, o controlador suporta caixas de diálogo ou MessageBox. Este é um elemento de informações do usuário padrão. Com ele, também é possível oferecer ao usuário várias opções de ação e obter uma resposta na forma de opção escolhida por ele.

Para iniciar a exibição de caixas de diálogo, selecione a opção 'Buttons and MessageBox' no parâmetro "Windows Form Element Type" ao iniciar o EA. Depois de iniciar o EA, aparecerá um formulário solicitando escolher uma das seguintes opções:

Fig. 3. Formulário de demonstração chamando caixas de mensagem. 

Este formulário, assim como todos os subsequentes, é de demonstração, portanto, não possui lógica de negociação. No entanto, depois de pressionar qualquer um dos botões, o EA enviará uma mensagem de aviso solicitando confirmação das ações selecionadas. Por exemplo, ao pressionar o botão SELL, é exibida a seguinte caixa de mensagem:


Fig. 4. O EA solicita a confirmação da abertura de uma nova posição curta.

Depois que o usuário clica num dos botões, o evento de pressionar é lembrado e registrado no buffer de eventos GuiController. O EA pesquisa este buffer de eventos com uma frequência especificada e, assim que ele descobre que um novo evento cai no buffer, ele começa a processá-lo. Assim, o EA precisa receber o evento "pressionar botão" e reagir a ele enviando eventos que se aproximam 'MessageBox'.

for(static int i = 0; i < GuiController::EventsTotal(); i++)
   {
      int id;
      string el_name;
      long lparam;
      double dparam;
      string sparam;
      //-- Obtemos o novo evento
      GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
      //-- Sabemos seu tipo - pressionar botão
      if(id == ClickOnElement)
      {
         //-- Exibimos no console do terminal o nome do botão pressionado
         printf("You press '" + sparam + "' button");
         string msg;
         //-- Dependendo do tipo de botão pressionado, foi formada a mensagem MessageBox
         if(el_name != "btnCancelAll")
            msg = "Are you sure you want to open a new " + sparam + " position?";
         else
            msg = "Are you sure you want to close all positions?";
         //-- Enviamos o evento oposto, com o comando para exibir MessageBox
         GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OKCancel, msg);
      }
      ...
   }

Analisemos a assinatura de envio do evento:

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

Isso significa que o EA pede para exibir uma caixa de diálogo (MessageBox) com o texto principal na variável msg, mostrando dois botões OK e Cancel (OKCancel). Na primeira parte do artigo dissemos que o primeiro parâmetro do método SendEvent contém o nome do elemento gráfico do destinatário do evento que está sendo enviado. No entanto, com MessageBox, este campo funciona de forma diferente. Acontece que as caixas de diálogo não estão vinculadas a qualquer elemento ou janela gráfica em particular (embora o Windows Frorms permita fazer essa ligação). Por essa razão, o GuiController cria uma nova janela de diálogo por conta própria e não precisa do endereço de destino da mensagem. No entanto, geralmente, com a exibição da mensagem, é necessário bloquear a janela à qual se relaciona. De fato, seria estranho se, ao exibir uma mensagem, fosse possível pressionar repetidamente o botão BUY ou SELL, ignorando o MessageBox que aparece. Portanto no GuiController, o nome do primeiro parâmetro para este evento indica o nome do elemento que precisa ser bloqueado até que o usuário pressione num dos botões MessageBox. A função de bloqueio de elemento gráfico arbitrário é opcional. Ela é definida usando a variável inteira lparam: 0 - sem bloqueio de janela, 1 - existe bloqueio. No entanto, é muito mais conveniente operar com constantes do que com zeros e uns. Para este propósito, no GuiController são definidas duas constantes usando a enumeração BlockingControl:

  • LockControl; 
  • NotLockControl

A primeira bloqueia a janela antes que o usuário pressione o botão, enquanto a segunda não faz nada, permitindo que a janela gráfica permaneça acessível ao usuário.

Caixas de diálogo além do texto também podem conter várias combinações de botões. Ao clicar nestes botões, o usuário concorda com uma ou outra escolha. Conjuntos de botões são especificados usando a enumeração de sistema System.Windows.Forms.MessageBoxButtons. Elementos desta enumeração não estão disponíveis para usuários MQL, uma vez que eles são definidos numa montagem externa. Para facilitar o trabalho dos programadores MQL com o GuiController, foi introduzida uma nova enumeração — o clone System.Windows.Forms.MessageBoxButtons com os mesmos parâmetros. A definição desta enumeração é dada em IController.cs:

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

As constantes desta enumeração estão disponíveis diretamente no Editor MQL, por exemplo, através do IntelliSens, o que torna a configuração do MessgaBox bastante conveniente. Por exemplo, se, no SendEvent, a constante OKCancel for substituída por YesNoCancel, a caixa de diálogo adquirirá outro conjunto de botões:

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

Fig. 5. Combinação padrão de três botões de seleção - Yes/No/Cancel.

Além das combinações de botões, o GuiController suporta a configuração de ícones de mensagens, bem como a do título da janela. Como o método SendEvent tem um número fixo de parâmetros e é bastante problemático passar todas as configurações através dele, foi encontrada uma solução alternativa. A linha do texto da mensagem pode ser dividida em seções usando o símbolo "|" Cada seção neste caso será responsável por seu parâmetro adicional. Seções podem ser de um (sem separadores) para três (dois separadores). Consideremos alguns exemplos. Suponhamos que queiramos exibir uma mensagem simples, sem ícone ou inscrição adicional. Nesse caso, o formato da mensagem será:

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


Fig. 6. Mensagem simples sem ícones e texto adicional no título da janela.

Pode-se adicionar um ícone à mensagem usando uma constante especial na seção adicional. Suponhamos que desejamos exibir uma mensagem com o ícone de aviso "Warning", neste caso, alteramos o formato no texto da mensagem para o seguinte:

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

Fig. 7. Mensagem de aviso

O ícone pode ser definido não apenas com uma palavra chave, mas também com um ícone de pseudônimo. Por exemplo, se em vez de Warning, escrever simplesmente "?" então o efeito será o mesmo. Além do ícone Warning, podem-se definir ícones para informações, dúvidas e erros. Mostramos uma tabela de palavras-chave e um pseudônimo para esses ícones:

ÍconePalavra chavePseudônimo

Warning!

Error!!!

Infoi

Question ?


Além dos ícones, pode-se definir o nome da própria caixa de diálogo. Para isso, é preciso separar o texto usando a seção "|" e, em seguida, inserir o nome da janela. Aqui está um exemplo de uma definição completa de uma janela com uma mensagem de erro:

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

Fig. 8. Exibindo caixa de diálogo com o ícone de erro e o nome da janela

O controlador se aproxima da análise de linha intelectualmente. Assim, se for especificada a string "!!!|The operation was cancelled", será exibido o ícone do erro crítico com a mensagem correspondente. Se na string forem indicadas duas seções "The operation was cancelled|Critical Error", não será exibido esse ícone, mas o nome da janela será alterado para "Critical Error".


TabControl (guias)

As guias (Tabs) são uma ferramenta conveniente para organizar elementos em grupos:

Fig. 9. Painel com duas guias

O controle de guia suporta um único evento TabIndexChange. Com isso, pode-se sabe que o usuário muda para outra guia. No EA de teste está escrito um código que acompanha a alteração de tabulação no formulário. Mostramos um fragmento disso:

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

O evento TabIndexChange converte dois parâmetros: lparam e sparam. O primeiro é o índice de tabulação selecionado pelo usuário. O segundo é o nome da guia selecionada. Por exemplo, se o usuário selecionar a primeira guia, o EA escreverá a mensagem:

Selecet new tab. Index: 0 Name: tabPage1

As guias são um elemento gráfico extremamente útil por si só. Na verdade, nem sempre é aconselhável rastrear a alteração de tabulação. Acontece que o WindowsForm requer que todos os elementos do mesmo formulário tenham nomes exclusivos. Consequentemente, dois elementos com o mesmo tipo, mas colocados em abas diferentes, serão únicos, o que significa que, do ponto de vista dos WindForms, ele terá que ter nomes diferentes. Por outro lado, como regra geral, é necessário rastrear a pressão dos controles diretos, nem sempre é importante saber em qual guia está localizado tal elemento. No entanto, às vezes é necessário rastrear tais eventos e, por isso, o GuiController fornece a interface de interação necessária e suficiente para esse elemento.

CheckBox (sinalizador)

ChreckBox ou caixa de seleção é um dos principais elementos de qualquer interface gráfica. Apesar de sua simplicidade, ea é usada numa ampla variedade de interfaces, começando com versões mais antigas do Windows e terminando com aplicativos da Web e móveis. Ela permite que indicar intuitivamente uma opção. Ao fazer isso, ela pode ser usada para mostrar opções que, por algum motivo, não estão disponíveis — dando ao usuário a oportunidade de escolher intuitivamente opções que não se contradizem:

 

Fig. 10. Opções de seleção usando uma combinação de sinalizadores

A caixa de seleção tem três estados: marcado (Checked), desmarcado (Unchecked) e parcialmente marcado (Indeterminate). No Windows Forms, há uma estrutura System.Windows.Forms.CheckState que descreve esses estados:

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

Cada vez que um usuário clica nesse sinalizador, o GuiController transmite seu status para um EA em MQL usando o evento CheckBoxChange, por meio da variável lparam. Seus valores correspondem a uma das variantes desta enumeração: 0 — Unchecked, 1 — Checked, 2 — Indeterminate.

No exemplo, o EA rastreia a seleção dos sinalizações   'Enable Trading On EURUSD' e 'Enable Trading On GBPUSD'. Assim que um dos pontos fica disponível, ele também disponibiliza seus sub-pontos 'Allow take profit' e 'Allow stop loss'. Por outro lado, se o usuário remover o sinalizador de um dos itens principais, seus subitens se tornarão imediatamente inativos. Isso é conseguido graças a dois eventos: ElementEnable e CheckBoxChange. O código abaixo apresenta o algoritmo de trabalho do EA com sinalizadores:

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

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

Assim que o especialista recebe uma notificação de que um dos sinalizadores principais é marcado pelo usuário, ele envia para o GuiController um evento ElementEnable com valor true. Se o usuário, ao contrário, remover o sinalizador desse elemento, o evento ElementEnable será enviado com o sinalizador false. Graças a essa interação entre o EA e o formulário com a ajuda de diferentes eventos, é criado um efeito de interatividade: o formulário começa a alterar a acessibilidade dos subelementos, dependendo da escolha do próprio usuário, embora a própria lógica de controle esteja diretamente no EA.


Radio Button (botão de alternância)

Os botões de alternância são um elemento gráfico simples para selecionar o item desejado dos predefinidos:

Fig. 11. Botões de alternância

Quando o usuário altera sua escolha, o EA recebe um evento de alteração duas vezes: na primeira vez a partir do botão do qual a seleção foi removida, o segundo evento vem do botão que foi selecionado. Ambos os eventos são rastreados usando o mesmo identificador RadioButtonChange. Aqui está um exemplo de uso:

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

O parâmetro lparam contém um sinalizador indicando o que aconteceu com o botão: selecionado (plaram = true) ou desmarcado (lparam = false). Ao pressionar os botões, o EA exibe este tipo de mensagens no terminal:

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


Combo Box (lista suspensa)

A lista suspensa é um dos elementos mais comuns. Juntamente com CheckBox, ela encontrou sua aplicação tanto no desenvolvimento Web como em aplicativos móveis modernos, é necessário dizer que no Windows é também um dos elementos mais utilizados:


Fig. 12. Lista suspensa e itens de menu disponíveis

A lista é usada basicamente em dois modos. O primeiro modo, além dos itens de menu apresentados, permite que o usuário insira novos valores:

Fig. 13. Seleção de instrumentos com a possibilidade de inserir um novo. 

O segundo modo oferece ao usuário apenas os itens de menu predefinidos, sem a possibilidade de escolher opções próprias fig. 11). Existe um terceiro modo que oculta os itens de menu, mas é raramente usado, portanto, não o usaremos. 

Todos os modos de exibição da ComboBox são definidos usando suas propriedades DropDownStyle. Como regra, essa propriedade é definida uma vez, no momento do design da interface gráfica do usuário, portanto, não há nenhum evento no GuiController que permita alterar o tipo de ComboBox. No entanto, o controlador permite rastrear a seleção de item da lista e até mesmo inserir um novo valor. Assim, ComboBox suporta dois eventos: o seu próprio ComboBoxChange e TextChange. Nosso formulário de demonstração consiste em dois elementos ComboBox. O primeiro oferece escolher uma plataforma (entre o MetaTrader 4 e o MetaTrader 5), o segundo seleciona um símbolo. Por padrão, o segundo item está bloqueado. No entanto, assim que o usuário seleciona uma plataforma, fica disponível a opção de instrumento de negociação. Aqui está o código que implementa essa funcionalidade: 

for(static int i = 0; i < GuiController::EventsTotal(); i++)
{
  int id;
  string el_name;
  long lparam;
  double dparam;
  string sparam;
  GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
  if(id == ComboBoxChange)
  {
     if(el_name == "comboBox1")
        //-- Desbloqueamos a lista de símbolos assim que o usuário seleciona a plataforma:
        GuiController::SendEvent("comboBox2", ElementEnable, 1, 0.0, "");
     printf("ComboBox '" + el_name + "' was changed on " + sparam);
  }
}

Ao começar a selecionar itens nas listas suspensas, o EA de demonstração começa a exibir os parâmetros da seleção feita pelo usuário no terminal:

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


NumericUpDown (janela com listagem numérica)

A janela com listagem numérica é muito usada em sistemas analíticos que incluem painéis de negociação. É por isso que este elemento foi um dos primeiros a ser incluído no GuiController. A janela com listagem numérica permite definir um determinado valor, controlando o tipo de entrada — podem-se definir apenas números. Neste caso, pode-se ajustar o passo de alteração do valor usando uma mini-rolagem especial, bem como o número de bits do próprio número:

Fig. 14. Janela com enumerador

O GuiController suporta quatro eventos para este tipo de item:

  • NumericChange — recebe ou envia um evento contendo um novo valor de janela numérica;
  • NumericFormatChange — envia um evento que define o número de dígitos (na variável lparam) e o passo de sua mudança (na variável dparam);
  • NumericMaxChange — envia um evento que especifica o valor máximo possível do número;
  • NumericMinChange — envia um evento que define o valor mínimo possível do número.

NumericUpDown interage com o usuário com ajuda de apenas um evento NumericChange. Quando o usuário altera o valor numérico nesta janela, o EA recebe uma notificação sobre isso através deste evento. Por aqui fica, esta interação com o usuário. No entanto, o próprio EA pode configurar a janela, expondo para ela os parâmetros mais significativos: o número de dígitos, o passo da mudança, os valores máximo e mínimo permitidos. Todos esses parâmetros dependem da lógica do EA e dos tipos de dados com os quais trabalha, portanto, não é possível defini-los diretamente nas configurações do formulário, eles devem ser definidos no momento da inicialização do programa.

O EA de tests inclui uma pequena demonstração que ilustra como trabalhar com o NumericUpDown. Fornecemos o código para carregar o formulário na Figura 13. 

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

Como pode ser visto no código, no momento do carregamento, o EA obtém dados sobre o instrumento atual: seus preços Ask e Bid, número de dígitos e passo de preço e, em seguida, define esses parâmetros para os elementos de formulário NumericUpDown usando a função auxiliar especial NumericSet. Usamos seu código:

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

Este código funcionará de forma adaptativa. Dependendo do símbolo em que será iniciado, veremos um formato de preço diferente:


Fig. 15. Formatos de preços próprios para cada instrumento de negociação

DataTimePicker (janela para selecionar datas)

Esse elemento é semelhante em conceito ao NumericUpDown com a única diferença que permite selecionar datas com segurança, não números:

Fig. 16. Selecionando a hora exata no DataTimePicker

A interação com o DataTimePicker é mais fácil do que com o elemento NumericUpDown. Isso se deve ao fato de que, diferentemente do formato de um número, que depende do ambiente de negociação atual do EA, o formato da data é mais ou menos universal. Pode ser instalado no momento de projetar o formulário e nunca mais mudar. Portanto, o DataTimePicker suporta apenas um único evento DateTimePickerChange, enviando e recebendo a data exata através do parâmetro lparam. Aqui está um exemplo de como usar este elemento:

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

Ao iniciar um EA de demonstração e selecionar o DateTimePicker como o elemento de demonstração, aparece uma janela como na Figura 15. Ao começar a alterar a data e a hora, o EA começa a responder a esses eventos, exibindo a nova data e hora no log:

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

No entanto, a interação com o elemento em si é um pouco mais complicada do que parece. Acontece que o formato de hora em MQL e em C# é diferente. O MQL usa o formato de hora POSIX simplificado, resolução de 1 segundo e a menor data possível 1970.01.01, enquanto em C# é usado um formato de hora mais avançado com uma resolução de 100 nanossegundos. Assim, para interagir com sistemas diferentes, é necessário escrever um conversor de tempo que transforme um sistema de cálculo em outro. Esse conversor é usado no GuiController. Ele é executado como uma classe estática pública MtConverter:

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

No momento, ele consiste em apenas dois métodos: o primeiro, ToMqlDateTime, converte o valor de tempo DateTime no formato MQL. O segundo método faz exatamente o oposto: ele converte o valor de tempo MQL numa estrutura C# DateTime. Como os tipos datetime (mql) e DateTime(C#) são incompatíveis entre si, a conversão ocorre através do tipo geral long, que é o mesmo para todos os sistemas. Assim, ao receber o evento DateTimePickerChange, precisamos somente converter explicitamente o parâmetro lparam em datetime para obter o valor de tempo correto:

//-- Explicitamente, convertemos o valor long em datetime. Isso é completamente seguro.
printf("User set new datetime value: " + TimeToString((datetime)lparam));

De maneira semelhante, além de obter novos valores, também podemos defini-los. Por exemplo, para definir a hora atual do terminal, pode-se usar o seguinte comando:

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


ElementHide и ElementEnable — ocultar e desativar um elemento arbitrário

Existem eventos universais através dos quais se pode controlar qualquer elemento do WinForms. ElementHide e ElementEnable pertencem a esse tipo de eventos. Para ocultar um elemento, deve-se usar ElementHide, para isso, basta escrever:

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

Onde "HideGroup" é o nome do elemento a ser oculto. Para exibir este elemento, respectivamente, deve-se chamar:

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

Note o nome do elemento usado. Acontece que um único elemento no WindowsForm pode conter elementos internos. Isso é chamado de aninhamento de elementos. Esse tipo de organização permite gerenciar todos os elementos no nível do grupo. No exemplo, é usado um quandro (text box) no qual está anexado o rótulo label. Com certa periodicidade, o quadro desaparece, com todos os elementos dentro, e aparece novamente:

 

Fig. 17. Ocultando um elemento gráfico arbitrário de um EA

Além disso, cada elemento pode se tornar não disponível, não desaparecendo e fazendo com que trabalhar com ele seja impossível. Esta é uma opção útil que permite projetar interfaces mais complexas e intuitivas. Já mencionamos o trabalho com este evento na descrição dos sinalizadores. Repetimos apenas que se pode tornar um elemento indisponível enviando o seguinte evento:

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

Pode-se tornar o elemento ativo novamente alterando o sinalizador false para true:

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


AddItem — adição de subelementos

Alguns elementos podem conter outros elementos. Muitas vezes, o conteúdo desses subelementos é desconhecido antes de inciar o programa. Suponha que precisamos exibir uma lista de instrumentos de negociação para que o usuário possa escolher o que ele precisa. Para isso, é racional usar o ComboBox:


Fig. 18. Lista de símbolos predefinidos

No entanto, os símbolos não podem ser inseridos antecipadamente, no estágio de compilação de uma forma gráfica, porque as listas de instrumentos disponíveis podem diferir de corretora para corretora. Portanto, o conteúdo deste tipo deve ser formado dinamicamente. Para isso, é usado o comando AddItem. A maneira mais simples de fazer isso é listando todos os símbolos disponíveis no MarketWatch e adicioná-los como elementos do menu:

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

Como se pode ver, este comando é muito fácil de usar. Apesar de o comando ser universal, nem todos os elementos adicionam outros elementos em si mesmos. O GuiController atualmente suporta este evento apenas para o ComboBox. No entanto, para isso, basta construir interfaces gráficas eficientes. No futuro, essa lista pode ser expandida.


Exception — evento para receber exceções

Nem sempre é possível escrever um programa sem erros. Além disso, há situações em que algo dá errado e o programa é executado de acordo com um cenário imprevisto. Nesses casos, é necessário feedback com informações sobre o que acontece. Para este tipo de interação, é fornecido o evento especial Exception. Ele é gerado pelo próprio GuiController e enviado ao EA MQL. O GuiController pode criar uma mensagem de erro em dois casos:

  1. No caso de uma interceptação de exceção do sistema;
  2. Se o evento - enviado do EA - não suportar o elemento selecionado ou o próprio evento tiver valores inválidos.

Analisemos essas opções em ordem. Para apresentar a primeira opção, voltemos ao nosso código de demonstração, ou seja, à versão que exibe os elementos NumericUpDown. Nesta variante de inicialização, é chamada a função especial NumericSet, que define os intervalos de trabalho dos elementos NumericUpDown:

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

Esta função foi ligeiramente modificada, e agora o valor máximo para o elemento não está definido (a linha correspondente é comentada). Ao executar este formulário, após compilar no gráfico, o formulário de repente para de exibir os preços atuais:

Fig. 19. Valores zero como definição de preços

O que aconteceu? Para determinar isso, existe um sistema de exceções. Cada exceção pode ser recebida, como qualquer outra mensagem, via GuiController::GetEvent:

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

A exibição da mensagem pode dizer muito:

Unexpected exception: Valor '1291,32' é inválido para 'Value'. 'Value' deve estar no intervalo de 'Minimum' a 'Maximum'.
Nome do parâmetro: Value
Unexpected exception: Valor '1291,06' é inválido para 'Value'. 'Value' deve estar no intervalo de 'Minimum' a 'Maximum'.
Nome do parâmetro: Value

Acontece que NumericUpDown por padrão tem um intervalo de trabalho de 0 a 100. Por conseguinte, ao tentar estabelecer o preço atual do ouro (1292 dólares por onça troy), ocorre um erro. Para evitá-lo, é necessário expandir o intervalo de valores aceitáveis programaticamente, isso pode ser feito usando o evento NumericMaxChange, que é o que a linha comentada na função NumericSet faz.

A segunda variante de chamada de exceção é possível se for enviado um evento incorreto. Por exemplo, pode-se tentar enviar um evento com um destinatário inexistente:

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

Em resposta, obtemos:

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

Além disso, pode-se tentar enviar um evento que não suporte o elemento. Por exemplo, no campo de entrada do stop-loss (tipo de elemento NumericUpDown), tentamos adicionar texto:

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

A resposta não será menos concisa:

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

O sistema de exceções fornece assistência inestimável na criação de aplicativos gráficos complexos. Neste tipo de aplicativos, os erros são inevitáveis, e a velocidade e facilidade do desenvolvimento dependem da rapidez com que o desenvolvedor possa entender esses erros.


Tabelas de elementos gráficos e de eventos disponíveis

Para trabalhar com o GuiController é conveniente sistematizar os elementos gráficos suportados. Para esses propósitos, criamos uma tabela contendo informações resumidas sobre esses elementos e como usá-los no GuiController. Na coluna "Exemplo de uso", fornecemos um breve exemplo de código no qual é mostrado o trabalho com este elemento de um programa MQL. O código em questão deve ser considerado como parte do padrão geral do trabalho com eventos. Assim, por exemplo, o código do primeiro exemplo com MessageBox:

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

Deve ser considerado no seguinte contexto:

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

Da mesma forma, precisa-se aplicar esse padrão a outros exemplos de uso.

Elemento gráficoNome do elemento ou do eventoIdentificadores de evento principaisExemplo de uso:

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



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



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


RadioButtonRadioButtonChange

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




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


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



DateTimePickerDateTimePickerChange

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



 

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

 

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

 

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

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

Além de elementos gráficos, o GuiController suporta eventos universais para trabalhar com eles. Também sistematizamos esses eventos na forma de uma tabela:

EventoDescriçãoExemplo de uso:
ElementHide Oculta ou restaura um elemento
GuiController::SendEvent("HideGroup", ElementHide, true, 0.0, "");
ElementEnable Ativa ou desativa o elemento.
GuiController::SendEvent("HideGroup", ElementEnable, true, 0.0, "");
AddItem Adiciona um novo subelemento ao elementos selecionado. 
GuiController::ShowForm(assembly, "SendOrderForm");
for(int i = 0; i < SymbolsTotal(true); i++)
   GuiController::SendEvent("SymbolComboBox", AddItem, 0, 0, SymbolName(i, true));
Exeption  Recebe uma exceção chamada dentro do CLR 
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("Unexpected exception: " + sparam);



Fim do artigo

Analisamos os principais elementos gráficos do Windows Forms e exemplos de interação com eles. Esses elementos são poucos, mas representam a espinha dorsal de qualquer aplicativo gráfico. E apesar de não possuírem outro elemento extremamente importante no campo de negociação, já é possível criar aplicações gráficas bastante funcionais com base em tabelas.

A versão mais recente do GuiController está anexada a este artigo. Esta versão também pode ser clonada a partir do sistema de repositório do GitHub. Deixe-me lembrá-lo de que a versão desta biblioteca está localizada em: https://github.com/PublicMqlProjects/MtGuiController.git. Você também pode clonar o projeto do formulário de demonstração. Ele está localizado em https://github.com/PublicMqlProjects/GuiControllerElementsDemo.git  A primeira parte do artigo apresenta como obter a última versão da biblioteca através do sistema de controle de versão. O arquivo anexado contém o código fonte completo de todos os projetos. Há três deles: o EA que chama o formulário com os elementos, o conjunto real de formulários na compilação DemoForm.exe e GuiController na versão compilada (Source\MQL5\Libraries\GuiController.dll) e na forma de códigos fonte (Source\Sharp\GuiController). Também é preciso prestar atenção ao fato de que o EA de demonstração necessita especificar o caminho absoluto para o formulário a ser executado. No seu computador, ele será diferente do especificado no parâmetro assemble do EA, portanto, é necessário alterá-lo para o caminho real.

Traduzido do russo pela MetaQuotes Software Corp.
Artigo original: https://www.mql5.com/ru/articles/6549

Arquivos anexados |
Source.zip (50.14 KB)
Implementado OLAP na negociação (Parte 1): Noções básicas da análise de dados multidimensionais Implementado OLAP na negociação (Parte 1): Noções básicas da análise de dados multidimensionais

O artigo descreve os princípios gerais de como construir uma estrutura para analisar dados multidimensionais (OLAP) rapidamente, além disso, apresenta como implementá-la em MQL e como usá-la no ambiente MetaTrader usando um exemplo que mostra o processamento do histórico de uma conta de negociação.

Estudo de técnicas de análise de velas (parte IV): Atualizações e adições ao Pattern Analyzer Estudo de técnicas de análise de velas (parte IV): Atualizações e adições ao Pattern Analyzer

O artigo apresenta uma nova versão do aplicativo Pattern Analyzer. Esta versão fornece correções de bugs e novos recursos, bem como a interface de usuário revisada. Os comentários e sugestões do artigo anterior foram levados em conta no desenvolvimento da nova versão. A aplicação resultante é descrita neste artigo.

Implementado OLAP na negociação (Parte 2): Visualizando resultados da análise interativa de dados multidimensionais Implementado OLAP na negociação (Parte 2): Visualizando resultados da análise interativa de dados multidimensionais

O artigo discute diversos aspectos da criação de interfaces gráficas interativas de programas MQL projetados para processamento analítico online (OLAP) do histórico de contas e de relatórios de negociação. Para obter um resultado visual, são usadas janelas maximizadas e escaláveis, uma disposição adaptável de controles de borracha e um novo 'controle' para exibir diagramas. Com base nisso, é implementada uma GUI com a possibilidade de escolher indicadores ao longo dos eixos de coordenadas, funções de agregação, tipos de gráficos e classificações.

Biblioteca para desenvolvimento fácil e rápido de programas para a MetaTrader (parte V): Classes e coleções de eventos de negociação, envio de eventos para o programa Biblioteca para desenvolvimento fácil e rápido de programas para a MetaTrader (parte V): Classes e coleções de eventos de negociação, envio de eventos para o programa

Nos artigos anteriores, nós começamos a criar uma grande biblioteca multi-plataforma, simplificando o desenvolvimento de programas para as plataformas MetaTrader 5 e MetaTrader 4. Na quarta parte, nós testamos o monitoramento de eventos de negociação na conta. Neste artigo, nós vamos desenvolver classes de eventos de negociação e colocá-los nas coleções de eventos. A partir daí, eles serão enviados ao objeto base da biblioteca Engine e ao gráfico do programa de controle.