English Русский 中文 Español Deutsch 日本語
preview
Criando uma Interface Gráfica de Usuário Interativa em MQL5 (Parte 2): Adicionando Controles e Responsividade

Criando uma Interface Gráfica de Usuário Interativa em MQL5 (Parte 2): Adicionando Controles e Responsividade

MetaTrader 5Negociação |
387 5
Allan Munene Mutiiria
Allan Munene Mutiiria

Introdução

Em nosso artigo anterior, estabelecemos a base ao montar os elementos gráficos do nosso painel de interface gráfica de usuário (GUI) em MetaQuotes Language 5 (MQL5). Se você se lembra, a iteração foi uma montagem estática dos elementos da GUI - uma simples captura congelada no tempo, sem responsividade. Era estático e imutável. Agora, vamos descongelar essa captura e dar-lhe vida. Nesta aguardada continuação, estamos levando nosso painel para o próximo nível. Prepare-se enquanto exploramos como dar vida à nossa interface:

  • Layout e Responsividade: Esqueça os componentes estáticos! Vamos adotar o posicionamento relativo, layouts flexíveis e componentes clicáveis, responsivos e editáveis, tornando nosso painel responsivo às interações dos usuários.
  • Atualizações Dinâmicas: Dados em tempo real são o coração de qualquer aplicação de negociação. Vamos explorar como buscar feeds de preços ao vivo e garantir que nosso painel reflita as informações mais recentes do mercado.
  • Mobilidade dos Componentes: Imagine elementos painéis arrastáveis que respondem ao toque do usuário. Vamos explorar como tornar certos componentes móveis, melhorando a experiência do usuário.

Os seguintes tópicos nos guiarão sobre como alcançar um painel responsivo e interativo:

  1. Ilustração dos Elementos a Serem Automatizados
  2. Automação de GUI em MQL5
  3. Conclusão


Ilustração dos Elementos a Serem Automatizados

Sete componentes serão automatizados. O primeiro componente é o fechamento do painel quando o botão de fechamento for clicado. Nosso objetivo é excluir todos os elementos do painel quando este botão for clicado. Em segundo lugar, quando os botões de gerenciamento de posição forem clicados, eles fecharão suas respectivas posições e ordens conforme instruído. Por exemplo, quando clicamos no botão ou rótulo "Lucro", fechamos todas as posições que estão em lucro. A terceira automação será no componente de volume de negociação. Assim que a entidade for clicada, uma lista suspensa de opções será criada para o usuário escolher uma opção de negociação.

A quarta automação será nos botões de aumento ou diminuição ao lado dos respectivos botões de negociação para incrementar ou diminuir os valores nos campos de edição, em vez de apenas digitá-los. Caso o usuário queira inserir os valores desejados diretamente, o campo de edição precisará capturar os valores inseridos, e isso torna-se o nosso quinto passo de automação. Então, o sexto passo será a criação de um efeito de hover no botão sobre o qual o mouse passar. Ou seja, quando o mouse estiver dentro da área do botão em destaque, o botão crescerá, indicando que o mouse está dentro da proximidade do botão. Quando o mouse se afastar da área do botão, o botão será redefinido para as características padrão. Por fim, atualizaremos as cotações de preço para valores em tempo real a cada variação de preço. 

Para facilitar o entendimento desses processos e componentes de automação, abaixo está uma descrição detalhada deles, destacando o marco anterior.

REPRESENTAÇÃO DOS PASSOS

Com a visão do que faremos, vamos começar a automação imediatamente. Por favor, consulte o artigo anterior onde criamos a montagem estática dos elementos da GUI, caso ainda não tenha lido, para que você esteja no caminho certo conosco. Vamos fazer isso.


Automação de GUI em MQL5

Vamos de processos simples para complexos para que nossa estrutura esteja organizada em ordem cronológica. Assim, atualizaremos os preços a cada variação de preço ou cotação. Para isso, precisaremos do manipulador de evento OnTick, uma função interna do MQL5 que é normalmente chamada quando as cotações de preço mudam. A função é do tipo void, o que significa que ela lida com execuções diretamente e não precisa retornar nenhum valor. Sua função deve se parecer com isso, conforme mostrado abaixo.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){

  ...

}
//+------------------------------------------------------------------+

Este é o manipulador de evento responsável pelas atualizações de preço e, portanto, o coração da nossa lógica. Vamos adicionar a lógica de controle a essa função como mostrado abaixo:

    // --- Update price quotes ---
    
    // Set the text of the "SELL PRICE" label to the current Bid price
    ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, Bid());

Usamos o ObjectSetString para definir a propriedade do objeto, texto neste caso, pois precisamos alterar a entrada de texto do rótulo do botão. Fornecemos o ID do gráfico da janela como 0 para o gráfico atual, ou você também pode fornecer "ChartID()", uma função que fornecerá o índice de identificação do gráfico para a janela do gráfico atual. Em seguida, fornecemos "LABEL_SELL_PRICE" como o nome do objeto alvo para atualizar o rótulo do botão de venda, e "OBJPROP_TEXT", para indicar que a propriedade do objeto que estamos atualizando é o valor da string de texto do objeto. Por fim, fornecemos o valor do texto. O preço de venda é o valor que precisamos atualizar, e assim o preenchermos, mas isso não é tudo. O tipo de propriedade que precisamos preencher é um valor de string, e nosso preço de venda está no formato double. Assim, precisamos converter o valor do tipo double para o tipo string, caso contrário, receberemos um aviso durante a compilação - conversão implícita de 'número' para 'string'.

DESCRIÇÃO DO AVISO

Neste ponto, poderíamos fazer o tipo de conversão do valor double para uma string diretamente como mostrado abaixo, mas geralmente não é recomendado, pois deve ser usado com cuidado.

    // --- Update price quotes ---
    
    // Set the text of the "SELL PRICE" label to the current Bid price
    ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, (string)Bid());

Conversão de Tipo em MQL5, como converter um valor numérico para uma string, tem algumas nuances. Uma das mais comuns é a perda de precisão. Por exemplo, o valor do nosso preço de venda é um valor de ponto flutuante, e por exemplo, quando seu preço é 11.77900, os dois últimos zeros serão ignorados e o valor final será 11.779. Tecnicamente, não há diferença lógica entre os dois valores, mas visualmente há uma diferença matemática, pois um contém 5 dígitos e o outro tem 3 dígitos. Aqui está um exemplo do que queremos dizer.

NUÂNCIAS DE CONVERSÃO DE TIPO

Como vimos, a conversão de tipo elimina o aviso, mas não é a melhor abordagem quando a precisão é importante. Portanto, será necessária outra função. Usamos a função interna DoubleToString do MQL5 para fazer a conversão. Essa função é usada para converter um valor numérico com ponto flutuante em uma string de texto. Ela recebe dois parâmetros de entrada ou argumentos, o valor flutuante alvo e o formato de precisão. No nosso caso, usamos o preço de venda como valor alvo e o formato de precisão como _Digits, uma variável que armazena o número de dígitos após o ponto decimal, o que define a precisão de preço do símbolo do gráfico atual.. Você também pode usar a função Digits(). Isso seria qualquer número arbitrário dentro do intervalo de 0 a 8, e se não for especificado, assume-se o valor de 8 dígitos. Por exemplo, nosso símbolo é GOLD, (XAUUSD), com 3 dígitos. Então teríamos 3 como o valor do dígito, mas para automação e para tornar o código adaptável aos pares de moedas, usamos a função para obter automaticamente o número de dígitos do par de moedas específico. No entanto, se você quiser um intervalo fixo de casas decimais, use um valor estático. Aqui está o código final para essa definição do preço de venda. 

    // --- Update price quotes ---
    
    // Set the text of the "SELL PRICE" label to the current Bid price
    ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, DoubleToString(Bid(), _Digits));

Agora que temos a lógica de conversão correta, graças aos desenvolvedores do MQL5 pela bela função, teremos os resultados abaixo.

REPRESENTAÇÃO CORRETA DO PREÇO DE VENDA

Para definir o preço de venda do botão de compra e o spread, a mesma lógica prevalece. Aqui está o código para isso. 

    // --- Update price quotes ---
    
    // Set the text of the "SELL PRICE" label to the current Bid price
    ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, DoubleToString(Bid(), _Digits));
    
    // Set the text of the "BUY PRICE" label to the current Ask price
    ObjectSetString(0, LABEL_BUY_PRICE, OBJPROP_TEXT, DoubleToString(Ask(), _Digits));
    
    // Set the text of the "SPREAD" button to the current spread value
    ObjectSetString(0, BTN_SPREAD, OBJPROP_TEXT, (string)Spread());

Você deve ter notado que, para o spread, fazemos diretamente a conversão de tipo para o valor de string, embora tenhamos criticado essa abordagem anteriormente em relação à manutenção da precisão. Aqui, a função do spread é um tipo de dado inteiro, e, portanto, a precisão não é prioridade máxima. De qualquer forma, teremos o formato correto. No entanto, você também pode usar a função IntegerToString para fazer a conversão, o que resultará no mesmo valor.

    // Set the text of the "SPREAD" button to the current spread value
    ObjectSetString(0, BTN_SPREAD, OBJPROP_TEXT, IntegerToString(Spread()));

A função recebe três argumentos, mas apenas o valor alvo é suficiente, pois ela não especifica o formato de precisão. Agora você pode obter a diferença. Em um formato de Imagem Gráfica Intercambiável (GIF), veja o que conseguimos até agora.

GIF DE PREÇOS

Isso é tudo o que precisamos fazer no manipulador de eventos e o código fonte completo responsável por atualizar os preços está abaixo:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){
    // --- Update price quotes ---
    
    // Set the text of the "SELL PRICE" label to the current Bid price
    ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, DoubleToString(Bid(), _Digits));
    
    // Set the text of the "BUY PRICE" label to the current Ask price
    ObjectSetString(0, LABEL_BUY_PRICE, OBJPROP_TEXT, DoubleToString(Ask(), _Digits));
    
    // Set the text of the "SPREAD" button to the current spread value
    ObjectSetString(0, BTN_SPREAD, OBJPROP_TEXT, IntegerToString(Spread()));
}
//+------------------------------------------------------------------+

Agora, o primeiro componente de automação está concluído. Foi fácil, certo? Agora vamos prosseguir para os outros componentes do nosso painel GUI. A automação dos outros elementos será feita dentro do manipulador da função OnChartEvent, então vamos dar uma olhada mais profunda em seus parâmetros de entrada e suas funções.

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{

        ...

}

O objetivo da função é lidar com as mudanças de gráfico feitas por um usuário ou um programa MQL5. Assim, as interações que o usuário fará, como mover o mouse, editar os campos de botão e clicar em rótulos e botões, serão capturadas e tratadas por esse manipulador de eventos. Vamos quebrar seus argumentos para uma interpretação mais detalhada:

  • id: Este parâmetro representa o ID do evento e corresponde a um dos 11 tipos de eventos predefinidos. Eles incluem eventos como pressionamentos de tecla, movimentos de mouse, criação de objetos, mudanças de gráfico e eventos personalizados. Para eventos personalizados, você pode usar IDs de CHARTEVENT_CUSTOM a CHARTEVENT_CUSTOM_LAST. Os 11 tipos de eventos são mostrados abaixo:

TIPOS DE EVENTOS DE GRÁFICO

  • lparam: Um parâmetro de evento do tipo long. Seu valor depende do evento específico que está sendo tratado. Por exemplo, ele pode representar um código de tecla durante um evento de pressionamento de tecla.
  • dparam: Um parâmetro de evento do tipo double. Semelhante ao lparam, seu valor varia de acordo com o tipo de evento. Por exemplo, durante um evento de movimento de mouse, ele pode indicar a posição do cursor do mouse.
  • sparam: Um parâmetro de evento do tipo string. Novamente, seu significado depende do evento. Por exemplo, durante a criação de um objeto, ele pode conter o nome do objeto recém-criado.

Para exibir isso de forma mais compreensível, dentro da função, vamos ter uma impressão que contém todos os quatro argumentos no diário de eventos.

// Print the 4 function parameters    
Print("ID = ",id,", LPARAM = ",lparam,", DPARAM = ",dparam,", SPARAM = ",sparam);

Essa função imprimirá o ID do evento do gráfico, seu valor de evento do tipo long, valor de evento do tipo double e valor de evento do tipo string. Vamos dar uma olhada no seguinte GIF para facilitar a referência.

GIF DE EVENTOS DE GRÁFICO

A partir do GIF fornecido, tudo agora deve estar claro. Agora vamos capturar os eventos de clique no gráfico nos elementos do painel GUI. Assim, nosso ID será "CHARTEVENT_OBJECT_CLICK". 

   //Print("ID = ",id,", LPARAM = ",lparam,", DPARAM = ",dparam,", SPARAM = ",sparam);

   if (id==CHARTEVENT_OBJECT_CLICK){
        
        ...

   }

Primeiro, comentamos a linha de código anterior porque não queremos lotar nosso diário com informações irrelevantes. As duas barras (//) usadas são chamadas de comentários de linha única e comentam o código a partir de seu início, continuando até o final da linha, daí o nome 'comentário de linha única'. Os comentários são ignorados pelo computador durante a execução. Usamos a instrução if para verificar se houve um clique em um objeto. Isso é alcançado equiparando o ID do evento do gráfico às enumerações de clique no objeto. Se clicamos em um objeto, vamos imprimir os argumentos e ver o que obtemos. O seguinte código é utilizado. 

   if (id==CHARTEVENT_OBJECT_CLICK){
      Print("ID = ",id,", LM = ",lparam,", DM = ",dparam,", SPARAM = ",sparam);

      ...

   }

Na função de impressão, mudamos apenas "LPARAM" para "LP" e "DPARAM" para "DP", para que possamos nos concentrar apenas no ID do evento do gráfico e no nome do objeto clicado. A partir daí, obteremos o ID do objeto e tomaremos as medidas necessárias, se necessário. Abaixo está uma ilustração da lógica:

GIF DE CLIQUE EM OBJETO

A primeira funcionalidade de automação do componente será sobre a destruição do painel GUI quando o ícone da ambulância for clicado. A partir do GIF acima, você pode ver que, uma vez que um objeto é clicado, o nome do objeto é armazenado na variável de tipo string de evento. Assim, a partir dessa variável, podemos obter o nome do objeto clicado e verificar se é o nosso objeto desejado e, se for, podemos tomar uma ação, no nosso caso destruir o painel. 

      //--- if icon car is clicked, destroy the panel
      if (sparam==ICON_CAR){
         Print("BTN CAR CLICKED. DESTROY PANEL NOW");
         destroyPanel();
         ChartRedraw(0);
      }

Outra instrução if é usada para verificar o caso em que o ícone do carro é clicado e, se for esse o caso, informamos a instância de que ele foi clicado, e a destruição do painel pode ser realizada, pois é o ícone correto para a tarefa. Após isso, chamamos a função "destroyPanel", cujo objetivo é excluir todos os elementos do nosso painel. Essa função já deve ser familiar para você, pois a utilizamos no nosso artigo anterior, que é a parte 1. Por fim, chamamos a função ChartRedraw. A função é usada para forçar a atualização de um gráfico especificado. Quando você modifica propriedades de gráficos ou objetos (como indicadores, linhas ou formas) programaticamente, as mudanças podem não ser refletidas imediatamente no gráfico. Ao chamá-la, você garante que o gráfico seja atualizado e exiba as últimas alterações. Em uma representação visual, aqui estão os resultados que obtemos.

GIF DE DESTRUÇÃO DO PAINEL

Você pode ver que a lógica é bem simples. O mesmo método será empregado nos outros cliques em objetos. Agora, vamos proceder para o evento em que o botão de fechamento é clicado. Quando isso acontecer, precisamos fechar todas as posições abertas e excluir todas as ordens pendentes. Isso garantirá que não tenhamos ordens de mercado. Será necessário usar uma instrução else-if para verificar a condição de se o botão de fechamento foi clicado.

      else if (sparam == BTN_CLOSE) {
          // Button "Close" clicked. Close all orders and positions now.
          Print("BTN CLOSE CLICKED. CLOSE ALL ORDERS & POSITIONS NOW");
      
          // Store the original color of the button
          long originalColor = ObjectGetInteger(0, BTN_CLOSE, OBJPROP_COLOR);
      
          // Change the button color to red (for visual feedback)
          ObjectSetInteger(0, BTN_CLOSE, OBJPROP_COLOR, clrRed);

          ...

      }

Aqui, queremos adicionar um pequeno ajuste à instância do evento. Queremos que, quando o botão for clicado, a cor do botão seja alterada, indicando que o botão foi clicado, assim o processo de fechamento das ordens de mercado deve ser iniciado. Após o fechamento completo, precisaremos redefinir a cor do botão para sua cor padrão. Para obter a cor original do rótulo do botão, declaramos uma variável de tipo long chamada "originalColor" e nela armazenamos a cor padrão do botão. Para recuperar a cor do botão, usamos a função ObjectGetInteger, passamos o ID do gráfico, o nome do botão e a propriedade do botão, que é a cor no nosso caso. Após armazenar a cor original, podemos agora mexer na cor do rótulo do botão, já que temos uma reserva de seu valor original. Usamos a função ObjectSetInteger para definir a cor do objeto para vermelho. Enquanto estiver nesse estado, iniciamos o processo de fechamento da ordem.

          // Iterate through all open positions
          for (int i = 0; i <= PositionsTotal(); i++) {
              ulong ticket = PositionGetTicket(i);
              if (ticket > 0) {
                  if (PositionSelectByTicket(ticket)) {
                      // Check if the position symbol matches the current chart symbol
                      if (PositionGetString(POSITION_SYMBOL) == _Symbol) {
                          obj_Trade.PositionClose(ticket); // Close the position
                      }
                  }
              }
          }

Usamos um for loop para iterar sobre todas as posições abertas e fechá-las. Para obter todas as posições abertas, usamos a função interna do MQL5 PositionsTotal. Essa função retornará o número de posições abertas na conta de negociação específica. Em seguida, obtemos o ticket dessa posição fornecendo o índice da posição na função PositionGetTicket e o armazenamos na variável de tipo ulong chamada "ticket". A função retorna o ticket da posição especificada e, em caso de falha, retorna 0. Para podermos prosseguir, precisamos garantir que temos um ticket. Isso é alcançado utilizando a instrução if para garantir que o valor do ticket seja maior que 0. Se for esse o caso, significa que temos um ticket e continuamos a selecionar o ticket para que possamos trabalhar com ele. Se conseguirmos selecionar o ticket com sucesso, podemos recuperar as informações da posição. Como pode haver várias posições na conta de negociação, garantimos que fecharemos apenas as posições associadas ao par de moedas específico. Finalmente, fechamos essa posição pelo número do ticket e prosseguimos para fazer o mesmo com outras posições abertas, se houver.

No entanto, para fechar a posição usamos "obj_Trade" seguido de um operador ponto. Isso é chamado de objeto de classe. Para facilitar a operação de fechamento de posição, precisamos incluir uma instância de classe que auxilie no processo. Assim, incluímos uma instância de trade utilizando #include no início do código fonte. Isso nos dá acesso à classe CTrade, que usaremos para criar um objeto de negociação. Isso é crucial, pois precisamos dela para realizar as operações de negociação.

#include <Trade/Trade.mqh>
CTrade obj_Trade;

O pré-processador substituirá a linha #include <Trade/Trade.mqh> pelo conteúdo do arquivo Trade.mqh. Os colchetes angulares indicam que o arquivo Trade.mqh será retirado do diretório padrão (geralmente é terminal_installation_directory\MQL5\Include). O diretório atual não está incluído na busca. A linha pode ser colocada em qualquer lugar do programa, mas geralmente todas as inclusões são colocadas no início do código-fonte, para uma melhor estrutura do código e referência mais fácil. A declaração do objeto obj_Trade da classe CTrade nos dará acesso aos métodos contidos nessa classe com facilidade, graças aos desenvolvedores do MQL5.

CLASSE CTRADE

Para excluir as ordens pendentes, a mesma lógica de iteração é utilizada.

          // Iterate through all pending orders
          for (int i = 0; i <= OrdersTotal(); i++) {
              ulong ticket = OrderGetTicket(i);
              if (ticket > 0) {
                  if (OrderSelect(ticket)) {
                      // Check if the order symbol matches the current chart symbol
                      if (OrderGetString(ORDER_SYMBOL) == _Symbol) {
                          obj_Trade.OrderDelete(ticket); // Delete the order
                      }
                  }
              }
          }

A principal diferença na lógica de iteração é que usamos a função OrdersTotal para obter o total de ordens. Tudo o mais está vinculado às ordens. Após todas as posições serem fechadas e as ordens excluídas, precisaremos redefinir a cor do rótulo do botão para sua cor original. 

          // Reset the button color to its original value
          Print("Resetting button to original color");
          ObjectSetInteger(0, BTN_CLOSE, OBJPROP_COLOR, originalColor);
      
          // Force a redrawing of the chart to reflect the changes
          ChartRedraw(0);

A função "ObjectSetInteger" é utilizada passando o ID do gráfico, o nome do botão, a propriedade de cor e a cor original. É aqui que nossa variável anterior se torna muito útil. Não precisamos sempre gravar a cor original de um objeto, pois podemos armazená-la e recuperá-la automaticamente. O código completo responsável por fechar todas as posições abertas e excluir todas as ordens abertas está abaixo:

      else if (sparam == BTN_CLOSE) {
          // Button "Close" clicked. Close all orders and positions now.
          Print("BTN CLOSE CLICKED. CLOSE ALL ORDERS & POSITIONS NOW");
      
          // Store the original color of the button
          long originalColor = ObjectGetInteger(0, BTN_CLOSE, OBJPROP_COLOR);
      
          // Change the button color to red (for visual feedback)
          ObjectSetInteger(0, BTN_CLOSE, OBJPROP_COLOR, clrRed);
      
          // Iterate through all open positions
          for (int i = 0; i <= PositionsTotal(); i++) {
              ulong ticket = PositionGetTicket(i);
              if (ticket > 0) {
                  if (PositionSelectByTicket(ticket)) {
                      // Check if the position symbol matches the current chart symbol
                      if (PositionGetString(POSITION_SYMBOL) == _Symbol) {
                          obj_Trade.PositionClose(ticket); // Close the position
                      }
                  }
              }
          }
      
          // Iterate through all pending orders
          for (int i = 0; i <= OrdersTotal(); i++) {
              ulong ticket = OrderGetTicket(i);
              if (ticket > 0) {
                  if (OrderSelect(ticket)) {
                      // Check if the order symbol matches the current chart symbol
                      if (OrderGetString(ORDER_SYMBOL) == _Symbol) {
                          obj_Trade.OrderDelete(ticket); // Delete the order
                      }
                  }
              }
          }
      
          // Reset the button color to its original value
          Print("Resetting button to original color");
          ObjectSetInteger(0, BTN_CLOSE, OBJPROP_COLOR, originalColor);
      
          // Force a redrawing of the chart to reflect the changes
          ChartRedraw(0);
      }

É sempre recomendado que, após cada adição de lógica a um painel, você compile e execute o código para garantir que tudo está funcionando como esperado antes de passar para outra lógica de controle. Isso é o que conseguimos até agora.

GIF DE FECHAMENTO

Agora podemos fechar todas as posições e ordens com sucesso. Observe como, quando o botão de fechamento é clicado, enquanto as posições estão sendo fechadas, a cor do rótulo do botão permanece vermelha até que todas sejam fechadas e, finalmente, retorne à sua cor original. Novamente, você pode perceber que não fechamos a posição de compra do "AUDUSD" porque o Expert Advisor (EA) está atualmente anexado ao símbolo do Ouro. Agora, a mesma lógica pode ser usada para definir a responsividade dos rótulos dos outros botões. 

      else if (sparam == BTN_MARKET) {
          // Button "Market" clicked. Close all positions related to the current chart symbol.
          Print(sparam + " CLICKED. CLOSE ALL POSITIONS NOW");
      
          // Iterate through all open positions
          for (int i = 0; i <= PositionsTotal(); i++) {
              ulong ticket = PositionGetTicket(i);
              if (ticket > 0) {
                  if (PositionSelectByTicket(ticket)) {
                      // Check if the position symbol matches the current chart symbol
                      if (PositionGetString(POSITION_SYMBOL) == _Symbol) {
                          obj_Trade.PositionClose(ticket); // Close the position
                      }
                  }
              }
          }
      
          // Force a redrawing of the chart to reflect the changes
          ChartRedraw(0);
      }

A diferença neste código em relação ao código do botão de fechamento é que eliminamos a iteração de fechamento de ordens, já que queremos fechar apenas todas as posições abertas. Para fechar todas as posições que estão em lucro, o trecho de código abaixo é utilizado.

      else if (sparam == BTN_PROFIT) {
          // Button "Profit" clicked. Close all positions in profit now.
          Print(sparam + " CLICKED. CLOSE ALL POSITIONS IN PROFIT NOW");
      
          // Iterate through all open positions
          for (int i = 0; i <= PositionsTotal(); i++) {
              ulong ticket = PositionGetTicket(i);
              if (ticket > 0) {
                  if (PositionSelectByTicket(ticket)) {
                      // Check if the position symbol matches the current chart symbol
                      if (PositionGetString(POSITION_SYMBOL) == _Symbol) {
                          double profit_or_loss = PositionGetDouble(POSITION_PROFIT);
                          if (profit_or_loss > 0) {
                              obj_Trade.PositionClose(ticket); // Close the position
                          }
                      }
                  }
              }
          }
      
          // Force a redrawing of the chart to reflect the changes
          ChartRedraw(0);
      }

A principal diferença neste trecho de código em relação ao anterior, que supostamente fecha todas as posições abertas, é que adicionamos uma lógica extra para verificar se o lucro da posição está acima de zero, o que significa que fechamos apenas as posições que estão com lucro. Abaixo está a lógica específica:

                          double profit_or_loss = PositionGetDouble(POSITION_PROFIT);
                          if (profit_or_loss > 0) {
                              obj_Trade.PositionClose(ticket); // Close the position
                          }

Definimos uma variável do tipo double chamada "profit_or_loss" e nela armazenamos o lucro ou prejuízo flutuante atual da posição selecionada. Se o valor for maior que 0, fechamos a posição, pois ela já está em lucro. A mesma lógica é transferida para o botão de prejuízo como mostrado abaixo, onde fechamos uma posição apenas se ela estiver em prejuízo. 

      else if (sparam == BTN_LOSS) {
          // Button "Loss" clicked. Close all positions in loss now.
          Print(sparam + " CLICKED. CLOSE ALL POSITIONS IN LOSS NOW");
      
          // Iterate through all open positions
          for (int i = 0; i <= PositionsTotal(); i++) {
              ulong ticket = PositionGetTicket(i);
              if (ticket > 0) {
                  if (PositionSelectByTicket(ticket)) {
                      // Check if the position symbol matches the current chart symbol
                      if (PositionGetString(POSITION_SYMBOL) == _Symbol) {
                          double profit_or_loss = PositionGetDouble(POSITION_PROFIT);
                          if (profit_or_loss < 0) {
                              obj_Trade.PositionClose(ticket); // Close the position
                          }
                      }
                  }
              }
          }
      
          // Force a redrawing of the chart to reflect the changes
          ChartRedraw(0);
      }

Finalmente, para fechar as ordens pendentes quando o botão de ordens pendentes for clicado, a iteração das ordens é utilizada e seu código é como mostrado abaixo.

      else if (sparam == BTN_PENDING) {
          // Button "Pending" clicked. Delete all pending orders related to the current chart symbol.
          Print(sparam + " CLICKED. DELETE ALL PENDING ORDERS NOW");
      
          // Iterate through all pending orders
          for (int i = 0; i <= OrdersTotal(); i++) {
              ulong ticket = OrderGetTicket(i);
              if (ticket > 0) {
                  if (OrderSelect(ticket)) {
                      // Check if the order symbol matches the current chart symbol
                      if (OrderGetString(ORDER_SYMBOL) == _Symbol) {
                          obj_Trade.OrderDelete(ticket); // Delete the order
                      }
                  }
              }
          }
      
          // Force a redrawing of the chart to reflect the changes
          ChartRedraw(0);
      }

Abaixo está a visualização do marco.

GIF DE TODOS OS BOTÕES DE FECHAMENTO

Como ilustrado, é evidente que os botões do cabeçalho do painel agora são responsivos quando clicados. Agora, vamos adicionar vida ao botão de volume de negociação. Queremos que, quando o botão ou o próprio rótulo forem clicados ou quando o ícone de lista suspensa for clicado, criemos outro subpainel com a lista de várias opções das quais o usuário pode escolher. A lógica está abaixo.

      else if (sparam == BTN_LOTS || sparam == LABEL_LOTS || sparam == ICON_DROP_DN1) {
          // Button "Lots," label "Lots," or dropdown icon clicked. Create a dropdown list.
          Print(sparam + " CLICKED. CREATE A DROPDOWN LIST");
      
          // Enable the button for dropdown functionality
          ObjectSetInteger(0, BTN_LOTS, OBJPROP_STATE, true);
      
          // Create the dropdown list
          createDropDown();
      
          // Redraw the chart to reflect the changes
          ChartRedraw(0);
      }

Assim que o botão for clicado, informamos a instância e definimos o estado do botão como verdadeiro. Isso faz com que o botão fique mais escuro, indicando que ele foi clicado. Uma vez que isso aconteça, criamos a lista suspensa chamando a função personalizada "createDropDown", cujo trecho de código foi fornecido anteriormente no primeiro artigo. Após a criação, o usuário terá que escolher entre as opções. Assim, se uma opção for escolhida ao clicar nela, precisaremos capturar e definir o rótulo do botão para a escolha do usuário, além de destruir a lista suspensa do painel de opções. Conseguimos isso usando o trecho de código abaixo.

      else if (sparam == LABEL_OPT1) {
          // Label "Lots" clicked.
          Print("LABEL LOTS CLICKED");
      
          // Get the text from LABEL_OPT1
          string text = ObjectGetString(0, LABEL_OPT1, OBJPROP_TEXT);
      
          // Get the state of the button (enabled or disabled)
          bool btnState = ObjectGetInteger(0, BTN_LOTS, OBJPROP_STATE);
      
          // Set the text of LABEL_LOTS to match LABEL_OPT1
          ObjectSetString(0, LABEL_LOTS, OBJPROP_TEXT, text);
      
          // Destroy the dropdown list
          destroyDropDown();
      
          // If the button was previously enabled, disable it
          if (btnState == true) {
              ObjectSetInteger(0, BTN_LOTS, OBJPROP_STATE, false);
          }
      
          // Redraw the chart
          ChartRedraw(0);
      }

Primeiro, verificamos se a primeira opção foi clicada. Se sim, obtemos o valor de texto da opção selecionada e o definimos como o valor de texto do botão de volume de negociação. Usamos uma função personalizada "destroyDropDown" para nos livrar do subpainel criado após definir a escolha selecionada pelo usuário no estado do botão, cujo trecho de código é mostrado abaixo.

//+------------------------------------------------------------------+
//|    Function to destroy dropdown                                  |
//+------------------------------------------------------------------+

void destroyDropDown(){
   ObjectDelete(0,BTN_DROP_DN);
   ObjectDelete(0,LABEL_OPT1);
   ObjectDelete(0,LABEL_OPT2);
   ObjectDelete(0,LABEL_OPT3);
   ObjectDelete(0,ICON_DRAG);
   ChartRedraw(0);
}

Finalmente, verificamos se o estado do botão estava previamente ativado, ou seja, no modo clicado, e, se sim, desativamos definindo a propriedade de estado como falsa. A mesma lógica é usada nas opções também. O trecho de código deles está abaixo:

      else if (sparam==LABEL_OPT2){
         Print("LABEL RISK % CLICKED");
         string text = ObjectGetString(0,LABEL_OPT2,OBJPROP_TEXT);
         bool btnState = ObjectGetInteger(0,BTN_LOTS,OBJPROP_STATE);
         ObjectSetString(0,LABEL_LOTS,OBJPROP_TEXT,text);
         destroyDropDown();
         if (btnState==true){
            ObjectSetInteger(0,BTN_LOTS,OBJPROP_STATE,false);
         }
         ChartRedraw(0);
      }
      else if (sparam==LABEL_OPT3){
         Print("LABEL MONEY CLICKED");
         string text = ObjectGetString(0,LABEL_OPT3,OBJPROP_TEXT);
         bool btnState = ObjectGetInteger(0,BTN_LOTS,OBJPROP_STATE);
         ObjectSetString(0,LABEL_LOTS,OBJPROP_TEXT,text);
         destroyDropDown();
         if (btnState==true){
            ObjectSetInteger(0,BTN_LOTS,OBJPROP_STATE,false);
         }
         ChartRedraw(0);
      }

Quando os botões laterais, ou seja, os botões de aumento e diminuição, são clicados, precisamos torná-los responsivos aumentando ou diminuindo o valor do respectivo campo de edição. Para começar, vejamos o botão de incremento do volume de negociação.

      else if (sparam == BTN_P1) {
          // Button "P1" clicked. Increase trading volume.
          Print(sparam + " CLICKED. INCREASE TRADING VOLUME");
      
          // Get the current trading volume from EDIT_LOTS
          double trade_lots = (double)ObjectGetString(0, EDIT_LOTS, OBJPROP_TEXT);
      
          // Increment the trading volume by 0.01
          trade_lots += 0.01;
      
          // Update the value in EDIT_LOTS
          ObjectSetString(0, EDIT_LOTS, OBJPROP_TEXT, DoubleToString(trade_lots, 2));
      
          // Redraw the chart
          ChartRedraw(0);
      }

Se o botão de incremento do volume de negociação for clicado, informamos a instância e nos preparamos para aumentar o valor do campo de lotes, pegando seu valor atual. Ao volume de negociação recuperado, adicionamos 0,01 como valor de incremento. O operador "+=" é usado para facilitar o processo. O que ele faz normalmente é aumentar o valor do tamanho do lote em 0,01. Isso é o mesmo que dizer (trade_lots = trade_lots + 0,01). O resultado é então passado para o campo do lote. O valor de ponto flutuante é convertido para uma string e uma precisão de 2 casas decimais é aplicada. A mesma lógica se aplica ao botão de diminuição, apenas precisamos subtrair 0,01 do valor. 

      else if (sparam == BTN_M1) {
          // Button "M1" clicked. Decrease trading volume.
          Print(sparam + " CLICKED. DECREASE TRADING VOLUME");
      
          // Get the current trading volume from EDIT_LOTS
          double trade_lots = (double)ObjectGetString(0, EDIT_LOTS, OBJPROP_TEXT);
      
          // Decrease the trading volume by 0.01
          trade_lots -= 0.01;
      
          // Update the value in EDIT_LOTS
          ObjectSetString(0, EDIT_LOTS, OBJPROP_TEXT, DoubleToString(trade_lots, 2));
      
          // Redraw the chart
          ChartRedraw(0);
      }

A mesma lógica se aplica aos outros botões semelhantes.

      else if (sparam==BTN_P2){
         Print(sparam+" CLICKED. INCREASE STOP LOSS POINTS");
         double sl_points = (double)ObjectGetString(0,EDIT_SL,OBJPROP_TEXT);
         sl_points+=10.0;
         ObjectSetString(0,EDIT_SL,OBJPROP_TEXT,DoubleToString(sl_points,1));
         ChartRedraw(0);
      }
      else if (sparam==BTN_M2){
         Print(sparam+" CLICKED. DECREASE STOP LOSS POINTS");
         double sl_points = (double)ObjectGetString(0,EDIT_SL,OBJPROP_TEXT);
         sl_points-=10.0;
         ObjectSetString(0,EDIT_SL,OBJPROP_TEXT,DoubleToString(sl_points,1));
         ChartRedraw(0);
      }
      
      else if (sparam==BTN_P3){
         Print(sparam+" CLICKED. INCREASE STOP LOSS POINTS");
         double tp_points = (double)ObjectGetString(0,EDIT_TP,OBJPROP_TEXT);
         tp_points+=10.0;
         ObjectSetString(0,EDIT_TP,OBJPROP_TEXT,DoubleToString(tp_points,1));
         ChartRedraw(0);
      }
      else if (sparam==BTN_M3){
         Print(sparam+" CLICKED. DECREASE STOP LOSS POINTS");
         double tp_points = (double)ObjectGetString(0,EDIT_TP,OBJPROP_TEXT);
         tp_points-=10.0;
         ObjectSetString(0,EDIT_TP,OBJPROP_TEXT,DoubleToString(tp_points,1));
         ChartRedraw(0);
      }

Aqui, especificamos nosso passo para ser de 10 pontos para os valores de stop loss e take profit. Para garantir que estamos no caminho certo, compilamos e visualizamos os resultados abaixo.

BOTÕES DE INCREMENTO E DECRECIMENTO DROPDOWN

Até este ponto, o progresso está bom. Os outros botões restantes são os botões de venda e compra. A lógica deles também é bem simples e segue a lógica anterior. Para o botão de venda, temos a seguinte lógica.

      else if (sparam==BTN_SELL){
         Print("BTN SELL CLICKED");
         ObjectSetInteger(0,BTN_SELL,OBJPROP_STATE,false);
         double trade_lots = (double)ObjectGetString(0,EDIT_LOTS,OBJPROP_TEXT);
         double sell_sl = (double)ObjectGetString(0,EDIT_SL,OBJPROP_TEXT);
         sell_sl = Ask()+sell_sl*_Point;
         sell_sl = NormalizeDouble(sell_sl,_Digits);
         double sell_tp = (double)ObjectGetString(0,EDIT_TP,OBJPROP_TEXT);
         sell_tp = Ask()-sell_tp*_Point;
         sell_tp = NormalizeDouble(sell_tp,_Digits);

         Print("Lots = ",trade_lots,", SL = ",sell_sl,", TP = ",sell_tp);
         obj_Trade.Sell(trade_lots,_Symbol,Bid(),sell_sl,sell_tp);
         ChartRedraw();
      }

Se o evento de clique for no botão de venda, informamos a instância e definimos o estado do botão como falso, indicando que ativamos a opção de clique. Para abrir uma posição de venda, precisaremos do volume de negociação, dos pontos de stop loss e dos pontos de take profit. Pegamos esses valores e os armazenamos em variáveis designadas para facilitar a recuperação. Para calcular o stop loss, pegamos os pontos de stop loss e os convertemos para o formato de ponto compatível com o par de moedas, multiplicando-os por _Point, e adicionamos o valor resultante ao preço atual de venda. Mais tarde, normalizamos o valor de saída em ponto flutuante para os dígitos do símbolo, garantindo precisão e exatidão. O mesmo é feito para o nível de take profit, e finalmente, abrimos uma posição de venda, passando os lotes de negociação, a cotação de venda como preço de venda, o stop loss e o take profit. A mesma lógica se aplica a uma posição de compra, e sua lógica é a seguinte.

      else if (sparam==BTN_BUY){
         Print("BTN BUY CLICKED");
         ObjectSetInteger(0,BTN_BUY,OBJPROP_STATE,false);
         double trade_lots = (double)ObjectGetString(0,EDIT_LOTS,OBJPROP_TEXT);
         double buy_sl = (double)ObjectGetString(0,EDIT_SL,OBJPROP_TEXT);
         buy_sl = Bid()-buy_sl*_Point;
         buy_sl = NormalizeDouble(buy_sl,_Digits);
         double buy_tp = (double)ObjectGetString(0,EDIT_TP,OBJPROP_TEXT);
         buy_tp = Bid()+buy_tp*_Point;
         buy_tp = NormalizeDouble(buy_tp,_Digits);

         Print("Lots = ",trade_lots,", SL = ",buy_sl,", TP = ",buy_tp);
         obj_Trade.Buy(trade_lots,_Symbol,Ask(),buy_sl,buy_tp);
         ChartRedraw();
      }

Ao testar, aqui estão os resultados:

GIF DE COMPRA E VENDA

Até este ponto, tudo está funcionando como esperado. O usuário poderia escolher não usar os botões de aumento e diminuição, mas, em vez disso, usar diretamente as opções de edição nos campos de botão de edição. Durante esse processo, erros imprevistos podem acontecer ao editar, o que levaria a operações sendo ignoradas. Por exemplo, o usuário pode digitar um tamanho de lote como "0.Q7". Tecnicamente, esse valor não é completamente numérico, pois contém a letra "Q". Como resultado, nenhuma operação de negociação será realizada sob o tamanho de lote. Portanto, vamos garantir que o valor seja sempre válido e, se não for, solicitar uma correção do erro. Para isso, é usado outro ID de evento de gráfico "CHARTEVENT_OBJECT_ENDEDIT". 

   else if (id==CHARTEVENT_OBJECT_ENDEDIT){
      if (sparam==EDIT_LOTS){
         Print(sparam+" WAS JUST EDITED. CHECK FOR ANY UNFORESEEN ERRORS");
         string user_lots = ObjectGetString(0,EDIT_LOTS,OBJPROP_TEXT);

         ...   

      }
   }

Primeiro, verificamos se o ID do evento de gráfico é um término de edição de um campo de edição. Se for, verificamos se o campo de edição é o botão de volume de negociação e, em caso afirmativo, informamos a instância e recuperamos o valor de entrada do usuário para uma análise posterior de erros imprevistos. A entrada é armazenada em uma variável string chamada "user_lots". Para análise, precisaremos dividir o tamanho do lote em partes, onde nosso limite será definido pelo caractere de ponto (.) - muitas vezes chamado de ponto final, ponto ou ponto.

         string lots_Parts_Array[];
         int splitCounts = StringSplit(user_lots,'.',lots_Parts_Array);//rep '.' = 'a' 
      
         Print("User lots split counts = ",splitCounts);ArrayPrint(lots_Parts_Array,0,"<&> ");

Definimos um array de armazenamento dinâmico das partes divididas como uma variável do tipo de dados string chamada "lots_Parts_Array". Em seguida, dividimos a entrada do usuário com o auxílio da função StringSplit, que recebe 3 argumentos. Fornecemos o valor da string de destino que deve ser dividida, a entrada do tamanho de lote do usuário neste caso, depois fornecemos o ponto como separador e, finalmente, um array de armazenamento das substrings resultantes. A função retornará o número de substrings no array de armazenamento. Se o separador especificado não for encontrado na string passada, apenas uma string de origem será colocada no array. Esses contadores de divisão serão armazenados na variável de contagem de divisão. Por fim, imprimimos o resultado da contagem de divisões, bem como os valores do array, ou seja, as substrings resultantes. Se editarmos o tamanho do lote para 0,05, aqui está o que obtemos:

DIVISÃO DE LOTE DE EDIÇÃO

Para que o valor de entrada seja válido, deve haver um separador de ponto, que deve resultar em duas contagens de divisão. Se sim, significa que a entrada tem um único separador de ponto.

         if (splitCounts == 2){

            ...

         }

No caso em que as contagens de divisão forem iguais a 1, isso indica que a entrada não contém um ponto, e portanto, não pode ser aceita. Neste caso, informamos o erro e definimos uma variável boolean chamada "isInputValid" como falsa.

         else if (splitCounts == 1){
            Print("ERROR: YOUR INPUT MUST CONTAIN DECIMAL POINTS");
            isInputValid = false;
         }

Se nenhuma das condições até agora for atendida, isso significa que a entrada possui mais de 1 separador de ponto, o que é errado, e então prosseguimos para informar o erro e definir o indicador de validade da entrada como falso.

         else {
            Print("ERROR: YOU CAN NOT HAVE MORE THAN ONE DECIMAL POINT IN INPUT");
            isInputValid = false;
         }

Se inserirmos um valor não válido com 2 separadores de ponto, este é o resultado que obtemos no diário do especialista.

2 PONTOS NA ENTRADA

Para verificar caracteres não numéricos na entrada, precisaremos percorrer cada uma das duas divisões e avaliar cada caractere individualmente. Será necessário um loop for para realizar isso facilmente.

            if (StringLen(lots_Parts_Array[0]) > 0){
            
               //
... 

            }

Primeiro, garantimos que a primeira string, no índice 0 no array de armazenamento, não esteja vazia, ou seja, quando seu comprimento de string for maior que 0. Usamos a função StringLen para obter o número de símbolos na string. Se o número de símbolos na string for menor ou igual a 0, significa que essa substring está vazia e que o valor de entrada já é inválido.

            else {
               Print("ERROR: PART 1 (LEFT HAND SIDE) IS EMPTY");
               isInputValid = false;
            }

Para visualização do erro, abaixo está o que obtemos se deixarmos a parte esquerda do separador vazia.

PARTE ESQUERDA VAZIA

Para verificar caracteres não numéricos, utilizamos um loop for conforme abaixo.

               string split = lots_Parts_Array[0];
               for (int i=0; i<StringLen(split); i++){
                  ushort symbol_code = StringGetCharacter(split,i);
                  string character = StringSubstr(split,i,1);
                  if (!(symbol_code >= 48 && symbol_code <= 57)){
                     Print("ERROR: @ index ",i+1," (",character,") is NOT a numeral. Code = ",symbol_code);
                     isInputValid = false;
                     break;
                  }
               }

Definimos uma variável string chamada "split", que é onde armazenamos nossa primeira substring no array de armazenamento. Em seguida, iteramos por todos os caracteres na substring. Para o caractere selecionado, obtemos o código do caractere usando a função StringGetCharacter, que retorna o valor de um símbolo localizado na posição especificada de uma string, e armazena o código do símbolo em uma variável do tipo short sem sinal chamada "symbol_code". Para obter o caractere real, usamos a função substring da string. Finalmente, usamos uma instrução if para verificar se o código resultante está entre os códigos numéricos e, caso contrário, significa que temos um caractere não numérico. Então, informamos o erro, definimos o indicador de validade da entrada como falso e saímos do loop antecipadamente. Caso contrário, significa que os caracteres são todos valores numéricos e nossa validade de entrada continuará verdadeira, conforme foi inicializada.

         bool isInputValid = true;

Você pode ter notado que a faixa numérica entre 48 e 57 é considerada uma faixa de códigos de símbolos numéricos. Bem, vamos ver o porquê. De acordo com a Tabela ASCII, esses símbolos numéricos têm um sistema de numeração decimal que começa com 48 para o símbolo "0" e vai até 57 para o símbolo "9".

CÓDIGOS DE SÍMBOLOS 1

Uma continuação segue abaixo.

CÓDIGOS DE SÍMBOLOS 2

A mesma lógica se aplica à segunda parte da string dividida, ou seja, à substring à direita do separador. O código-fonte dela está abaixo.

            if (StringLen(lots_Parts_Array[1]) > 0){
               string split = lots_Parts_Array[1];
               for (int i=0; i<StringLen(split); i++){
                  ushort symbol_code = StringGetCharacter(split,i);
                  string character = StringSubstr(split,i,1);
                  if (!(symbol_code >= 48 && symbol_code <= 57)){
                     Print("ERROR: @ index ",i+1," (",character,") is NOT a numeral. Code = ",symbol_code);
                     isInputValid = false;
                     break;
                  }
               }

            }
            else {
               Print("ERROR: PART 2 (RIGHT HAND SIDE) IS EMPTY");
               isInputValid = false;
            }

Para garantir que podemos diferenciar entre um símbolo numérico e um caractere não numérico, vamos ilustrar. 

ENTRADA NÃO NUMÉRICA

Você pode ver que, quando adicionamos a letra maiúscula "A", cujo código é 65, retornamos um erro, indicando que a entrada é inválida. Usamos "A" neste exemplo, pois seu código de símbolo pode ser facilmente referenciado nas imagens fornecidas. Podia ser qualquer outra coisa. Agora, prosseguimos novamente a usar o indicador de validade da entrada para definir o valor do texto como a entrada original do usuário, já que ela não possui discrepâncias. 

         if (isInputValid == true){
            Print("SUCCESS: INPUT IS VALID.");
            ObjectSetString(0,EDIT_LOTS,OBJPROP_TEXT,user_lots);
            ObjectSetInteger(0,EDIT_LOTS,OBJPROP_COLOR,clrBlack);
            ObjectSetInteger(0,EDIT_LOTS,OBJPROP_BGCOLOR,clrWhite);
            ChartRedraw(0);
         }

Caso o indicador de validade da entrada seja verdadeiro, informamos o sucesso e definimos o valor do texto como a entrada original do usuário, pois ela não possui discrepâncias. Definimos novamente a cor do texto como preta e a cor de fundo do botão como branca. Essas são normalmente as propriedades originais do campo de edição. Se a saída for falsa, significa que o valor de entrada do usuário teve falhas, e não pode ser usado para operações de negociação.

         else if (isInputValid == false){
            Print("ERROR: INPUT IS INVALID. ENTER A VALID INPUT!");
            ObjectSetString(0,EDIT_LOTS,OBJPROP_TEXT,"Error");
            ObjectSetInteger(0,EDIT_LOTS,OBJPROP_COLOR,clrWhite);
            ObjectSetInteger(0,EDIT_LOTS,OBJPROP_BGCOLOR,clrRed);
            ChartRedraw(0);
         }

Portanto, informamos o erro e definimos o valor do texto como "Erro". Para atrair a atenção máxima do usuário, definimos a cor do texto como branca e a cor de fundo como vermelha, uma combinação de cores chamativa que facilita para o usuário reconhecer que há um erro. Após a compilação, os seguintes resultados são o que obtemos.

GIF DE ENTRADA DO USUÁRIO

Até este ponto, a automação da maioria dos componentes do painel está concluída. Os únicos que permanecem sem contabilizar são o movimento da lista suspensa e o efeito de hover do mouse sobre um botão. Tudo isso precisa ser considerado quando há movimento do mouse no gráfico, e, portanto, o ID de evento "CHARTEVENT_MOUSE_MOVE" será considerado. Para rastrear o movimento do mouse, precisaremos ativar a lógica de detecção do movimento do mouse no gráfico na instância de inicialização do especialista, e isso é alcançado pela lógica abaixo. 

   //--- enable CHART_EVENT_MOUSE_MOVE detection
   ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,true);

Vamos começar com o mais fácil, que é o efeito de hover. Recebemos o evento quando o mouse se move no gráfico após ativar sua detecção. 

   else if (id==CHARTEVENT_MOUSE_MOVE){

      ...

   }

Para detectar a localização do mouse dentro do gráfico, precisaremos obter suas ordenadas, ou seja, sua localização ao longo dos eixos x e y, bem como seu estado, ou seja, quando está se movendo e quando está estático.

      int mouse_X = (int)lparam;    // mouseX   >>> mouse coordinates
      int mouse_Y = (int)dparam;    // mouseY   >>> mouse coordinates
      int mouse_State = (int)sparam; // Get the mouse state (0 = mouse moving)

Aqui, declaramos uma variável do tipo inteiro "mouse_X" para armazenar a distância do mouse ao longo do eixo X, ou seja, ao longo da escala de data e hora. Novamente, obtemos o parâmetro double e armazenamos seu valor no parâmetro "mouse_Y" e, finalmente, o parâmetro string na variável "mouse_State". Nós os convertimos para inteiros no final. Precisaremos das coordenadas iniciais do elemento alvo, e assim, as definimos via o trecho de código abaixo.

      //GETTING THE INITIAL DISTANCES AND SIZES OF BUTTON
      
      int XDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XDISTANCE);
      int YDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YDISTANCE);
      int XSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XSIZE);
      int YSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YSIZE);
      

Obtemos as distâncias e tamanhos dos respectivos botões e os armazenamos nas variáveis inteiras respectivas. O formato de typecasting é usado para converter o valor para os formatos inteiros. Para acompanhar as coordenadas do mouse em relação ao botão em questão, precisaremos de algumas variáveis para armazenar a lógica.

      static bool prevMouseInside = false;
      bool isMouseInside = false;

A variável booleana estática "prevMouseInside" é declarada para manter o controle de se o mouse estava anteriormente dentro da área do botão. A variável booleana "isMouseInside" armazenará o estado atual do mouse em relação ao botão, e todas as variáveis são inicializadas com um valor falso. Para determinar se o mouse está dentro da área do botão, usamos uma instrução condicional.

      if (mouse_X >= XDistance_Hover_Btn && mouse_X <= XDistance_Hover_Btn + XSize_Hover_Btn &&
          mouse_Y >= YDistance_Hover_Btn && mouse_Y <= YDistance_Hover_Btn + YSize_Hover_Btn){
         isMouseInside = true;
      }

A verificação condicional determina se o cursor do mouse está atualmente dentro da área do botão. Se sim, "isMouseInside" é definido como verdadeiro, indicando que o mouse está dentro do botão; caso contrário, a variável booleana será falsa se as condições não forem atendidas. Tecnicamente, quatro condições devem ser atendidas para que o cursor do mouse seja considerado dentro da área do botão. Vamos desintegrar cada condição para um melhor entendimento.

  • mouse_X >= XDistance_Hover_Btn: Isso verifica se a coordenada X do mouse (mouse_X) é maior ou igual ao limite esquerdo do botão (XDistance_Hover_Btn).
  • mouse_X <= XDistance_Hover_Btn + XSize_Hover_Btn: Isso verifica se a coordenada X do mouse é menor ou igual ao limite direito do botão (soma de XDistance_Hover_Btn e a largura do botão XSize_Hover_Btn).
  • mouse_Y >= YDistance_Hover_Btn: Da mesma forma, isso verifica se a coordenada Y do mouse (mouse_Y) é maior ou igual ao limite superior do botão (YDistance_Hover_Btn).
  • mouse_Y <= YDistance_Hover_Btn + YSize_Hover_Btn: Isso verifica se a coordenada Y do mouse é menor ou igual ao limite inferior do botão (soma de YDistance_Hover_Btn e a altura do botão YSize_Hover_Btn).

Se todas as condições forem atendidas, definimos a variável "isMouseInside" como verdadeira. Com o valor resultante, podemos verificar se o mouse está dentro do botão. A lógica a seguir é implementada. 

      if (isMouseInside != prevMouseInside) {

Aqui, verificamos se o estado atual do mouse (dentro ou fora da área do botão) mudou desde a última verificação. Isso garante que as ações subsequentes sejam realizadas apenas quando houver uma mudança na posição do mouse em relação ao botão. Novamente, precisaremos verificar se as condições foram atendidas.

         // Mouse entered or left the button area
         if (isMouseInside) {
            Print("Mouse entered the Button area. Do your updates!");
            //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'050,050,255');

            ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'050,050,255');
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, clrLightBlue);
         }

Se a variável boolean for verdadeira, significa que o mouse entrou na área do botão. Informamos a instância por meio de uma declaração de impressão. Em seguida, alteramos a cor do rótulo do botão, bem como seu fundo. Caso contrário, se a variável for falsa, significa que o cursor do mouse estava anteriormente dentro da área do botão e acabou de sair. Portanto, resetamos as cores para os valores padrões. Abaixo está o trecho de código responsável por essa lógica.

         else if (!isMouseInside) {
            Print("Mouse left Btn proximities. Return default properties.");
            //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'100,100,100');
            // Reset button properties when mouse leaves the area
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'100,100,100');
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, C'220,220,220');
         }

Após qualquer alteração nas propriedades do botão, a função "ChartRedraw" é chamada para atualizar a exibição do gráfico e refletir a aparência atualizada do botão. Finalmente, a variável "prevMouseInside" é atualizada para corresponder ao estado atual do mouse ("isMouseInside"). Isso garante que, da próxima vez que o evento for disparado, o programa possa comparar o novo estado com o anterior.

         ChartRedraw(0);//// Redraw the chart to reflect the changes
         prevMouseInside = isMouseInside;

O código completo responsável por criar o efeito de hover no botão é o seguinte:

   else if (id==CHARTEVENT_MOUSE_MOVE){
      int mouse_X = (int)lparam;    // mouseX   >>> mouse coordinates
      int mouse_Y = (int)dparam;    // mouseY   >>> mouse coordinates
      int mouse_State = (int)sparam; // Get the mouse state (0 = mouse moving)
      
      //GETTING THE INITIAL DISTANCES AND SIZES OF BUTTON
      
      int XDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XDISTANCE);
      int YDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YDISTANCE);
      int XSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XSIZE);
      int YSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YSIZE);
      
      static bool prevMouseInside = false;
      bool isMouseInside = false;
      
      //Print("Mouse STATE = ",mouse_State); // 0 = mouse moving

      if (mouse_X >= XDistance_Hover_Btn && mouse_X <= XDistance_Hover_Btn + XSize_Hover_Btn &&
          mouse_Y >= YDistance_Hover_Btn && mouse_Y <= YDistance_Hover_Btn + YSize_Hover_Btn){
         isMouseInside = true;
      }
      
      if (isMouseInside != prevMouseInside) {
         // Mouse entered or left the button area
         if (isMouseInside) {
            Print("Mouse entered the Button area. Do your updates!");
            //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'050,050,255');

            ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'050,050,255');
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, clrLightBlue);
         }
         else if (!isMouseInside) {
            Print("Mouse left Btn proximities. Return default properties.");
            //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'100,100,100');
            // Reset button properties when mouse leaves the area
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'100,100,100');
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, C'220,220,220');
         }
         ChartRedraw(0);//// Redraw the chart to reflect the changes
         prevMouseInside = isMouseInside;
      }
   }

Após a compilação, aqui está o que obtemos:

GIF DO EFEITO DE HOVER

Isso está excelente. Agora, vamos para a parte final, que não envolve apenas o rastreamento do movimento do cursor do mouse, mas também o movimento de objetos ou componentes junto com ele. Novamente, declaramos uma variável inteira estática para detectar quando o mouse é clicado e uma variável booleana para armazenar o estado do movimento do cursor do mouse. Isso é alcançado através do trecho de código abaixo.

      // CREATE MOVEMENT
      static int prevMouseClickState = false; // false = 0, true = 1;
      static bool movingState = false;

Em seguida, precisaremos inicializar as variáveis que armazenarão os tamanhos e distâncias dos nossos objetos. 

      // INITIALIZE VARIBALES TO STORE INITIAL SIZES AND DISTANCES OF OBJECTS
      // MLB = MOUSE LEFT BUTTON
      static int mlbDownX = 0; // Stores the X-coordinate of the mouse left button press
      static int mlbDownY = 0; // Stores the Y-coordinate of the mouse left button press
      
      static int mlbDownX_Distance = 0; // Stores the X-distance of an object
      static int mlbDownY_Distance = 0; // Stores the Y-distance of an object
      
      static int mlbDownX_Distance_BTN_DROP_DN = 0; // Stores X-distance for a specific button (BTN_DROP_DN)
      static int mlbDownY_Distance_BTN_DROP_DN = 0; // Stores Y-distance for the same button
      
      static int mlbDownX_Distance_LABEL_OPT1 = 0; // Stores X-distance for a label (LABEL_OPT1)
      static int mlbDownY_Distance_LABEL_OPT1 = 0; // Stores Y-distance for the same label
      
      static int mlbDownX_Distance_LABEL_OPT2 = 0; // Stores X-distance for another label (LABEL_OPT2)
      static int mlbDownY_Distance_LABEL_OPT2 = 0; // Stores Y-distance for the same label
      
      static int mlbDownX_Distance_LABEL_OPT3 = 0; // Stores X-distance for yet another label (LABEL_OPT3)
      static int mlbDownY_Distance_LABEL_OPT3 = 0; // Stores Y-distance for the same label
      
      static int mlbDownX_Distance_ICON_DRAG = 0; // Stores X-distance for an icon (ICON_DRAG)
      static int mlbDownY_Distance_ICON_DRAG = 0; // Stores Y-distance for the same icon

Para inicializar as variáveis de armazenamento, declaramos variáveis do tipo de dados estático e as inicializamos com 0, como acima. Elas são declaradas como estáticas, pois precisamos armazenar os respectivos tamanhos e distâncias para referência quando os componentes do painel estiverem em movimento. Novamente, precisaremos das distâncias iniciais dos elementos, e isso é alcançado através do trecho de código abaixo. 

      //GET THE INITIAL DISTANCES AND SIZES OF BUTTON
      
      int XDistance_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_XDISTANCE);
      int YDistance_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_YDISTANCE);
      //int XSize_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_XSIZE);
      //int YSize_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_YSIZE);
      
      int XDistance_Opt1_Lbl = (int)ObjectGetInteger(0,LABEL_OPT1,OBJPROP_XDISTANCE);
      int YDistance_Opt1_Lbl = (int)ObjectGetInteger(0,LABEL_OPT1,OBJPROP_YDISTANCE);
      
      int XDistance_Opt2_Lbl = (int)ObjectGetInteger(0,LABEL_OPT2,OBJPROP_XDISTANCE);
      int YDistance_Opt2_Lbl = (int)ObjectGetInteger(0,LABEL_OPT2,OBJPROP_YDISTANCE);
      
      int XDistance_Opt3_Lbl = (int)ObjectGetInteger(0,LABEL_OPT3,OBJPROP_XDISTANCE);
      int YDistance_Opt3_Lbl = (int)ObjectGetInteger(0,LABEL_OPT3,OBJPROP_YDISTANCE);

      int XDistance_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_XDISTANCE);
      int YDistance_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_YDISTANCE);
      int XSize_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_XSIZE);
      int YSize_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_YSIZE);

Aqui, usamos a função ObjectGetInteger para recuperar as distâncias dos elementos a serem movidos junto com o cursor. No entanto, observe que também obtemos o tamanho do ícone que será utilizado no movimento do painel. A razão pela qual precisamos dos tamanhos, assim como fizemos na lógica de efeito de hover, é que podemos determinar quando o cursor do mouse é clicado dentro da área do ícone, para que possamos começar o movimento. Em seguida, precisamos capturar as informações do clique inicial do mouse e armazenar as distâncias dos objetos a serem movidos.

if (prevMouseClickState == false && mouse_State == 1) {
    // Check if the left mouse button was clicked and the mouse is in the pressed state

    // Initialize variables to store initial distances and sizes of objects
    mlbDownX = mouse_X; // Store the X-coordinate of the mouse click
    mlbDownY = mouse_Y; // Store the Y-coordinate of the mouse click

    // Store distances for specific objects
    mlbDownX_Distance = XDistance_Drag_Icon; // Distance of the drag icon (X-axis)
    mlbDownY_Distance = YDistance_Drag_Icon; // Distance of the drag icon (Y-axis)

    mlbDownX_Distance_BTN_DROP_DN = XDistance_DropDn_Btn; // Distance of a specific button (BTN_DROP_DN)
    mlbDownY_Distance_BTN_DROP_DN = YDistance_DropDn_Btn;

    mlbDownX_Distance_LABEL_OPT1 = XDistance_Opt1_Lbl; // Distance of a label (LABEL_OPT1)
    mlbDownY_Distance_LABEL_OPT1 = YDistance_Opt1_Lbl;

    mlbDownX_Distance_LABEL_OPT2 = XDistance_Opt2_Lbl; // Distance of another label (LABEL_OPT2)
    mlbDownY_Distance_LABEL_OPT2 = YDistance_Opt2_Lbl;

    mlbDownX_Distance_LABEL_OPT3 = XDistance_Opt3_Lbl; // Distance of yet another label (LABEL_OPT3)
    mlbDownY_Distance_LABEL_OPT3 = YDistance_Opt3_Lbl;

    // Check if the mouse is within the drag icon area
    if (mouse_X >= XDistance_Drag_Icon && mouse_X <= XDistance_Drag_Icon + XSize_Drag_Icon &&
        mouse_Y >= YDistance_Drag_Icon && mouse_Y <= YDistance_Drag_Icon + YSize_Drag_Icon) {
        movingState = true; // Set the moving state to true
    }
}

Usamos uma instrução condicional para verificar duas condições. Uma, "prevMouseClickState == false" para garantir que o botão esquerdo do mouse não foi clicado anteriormente, e a segunda, "mouse_State == 1" para verificar se o mouse está atualmente no estado pressionado (botão para baixo). Se as duas condições forem atendidas, armazenamos as coordenadas X e Y do mouse, bem como as distâncias dos objetos. Finalmente, verificamos se o mouse está dentro da área do ícone de arraste, e se sim, definimos o estado de movimento como verdadeiro, uma indicação de que podemos iniciar o movimento dos componentes do painel. Para entender facilmente isso, vamos desmembrar as quatro condições:

  • mouse_X >= XDistance_Drag_Icon: Isso verifica se a coordenada X do mouse (mouse_X) é maior ou igual ao limite esquerdo da área do ícone de arraste (XDistance_Drag_Icon).
  • mouse_X <= XDistance_Drag_Icon + XSize_Drag_Icon: Da mesma forma, garante que a coordenada X seja menor ou igual ao limite direito da área do ícone de arraste (soma de XDistance_Drag_Icon e a largura do ícone XSize_Drag_Icon).
  • mouse_Y >= YDistance_Drag_Icon: Isso verifica se a coordenada Y do mouse (mouse_Y) é maior ou igual ao limite superior da área do ícone de arraste (YDistance_Drag_Icon).
  • mouse_Y <= YDistance_Drag_Icon + YSize_Drag_Icon: Da mesma forma, garante que a coordenada Y seja menor ou igual ao limite inferior da área do ícone de arraste (soma de YDistance_Drag_Icon e a altura do ícone YSize_Drag_Icon).

Se todas as quatro condições forem atendidas (ou seja, o mouse está dentro da área definida do ícone de arraste), definimos a variável "movingState" como verdadeira. Até esse ponto, se o estado de movimento for verdadeiro, movemos os objetos designados.

      if (movingState){
         ChartSetInteger(0,CHART_MOUSE_SCROLL,false);
         
         ObjectSetInteger(0,ICON_DRAG,OBJPROP_XDISTANCE,mlbDownX_Distance + mouse_X - mlbDownX);
         ObjectSetInteger(0,ICON_DRAG,OBJPROP_YDISTANCE,mlbDownY_Distance + mouse_Y - mlbDownY);
         
         ...

         ChartRedraw(0);
      }

Aqui, usamos a função ChartSetInteger para desativar a flag de rolagem do gráfico. Isso garantirá que, quando o mouse estiver se movendo, o gráfico não será rolado horizontalmente. Assim, somente o cursor do mouse se moverá junto com os objetos designados. Finalmente, definimos as novas distâncias dos objetos, em relação às coordenadas atuais do mouse, e redesenhamos o gráfico para que as mudanças tenham efeito. Em resumo, isso é o que temos:

GIF DO ÍCONE DE ARRASTE

Agora você pode ver que podemos arrastar o ícone. No entanto, também precisamos arrastá-lo junto com os outros componentes do painel. Assim, a mesma lógica se aplica. 

         ObjectSetInteger(0,BTN_DROP_DN,OBJPROP_XDISTANCE,mlbDownX_Distance_BTN_DROP_DN + mouse_X - mlbDownX);
         ObjectSetInteger(0,BTN_DROP_DN,OBJPROP_YDISTANCE,mlbDownY_Distance_BTN_DROP_DN + mouse_Y - mlbDownY);
         
         ObjectSetInteger(0,LABEL_OPT1,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT1 + mouse_X - mlbDownX);
         ObjectSetInteger(0,LABEL_OPT1,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT1 + mouse_Y - mlbDownY);
         
         ObjectSetInteger(0,LABEL_OPT2,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT2 + mouse_X - mlbDownX);
         ObjectSetInteger(0,LABEL_OPT2,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT2 + mouse_Y - mlbDownY);

         ObjectSetInteger(0,LABEL_OPT3,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT3 + mouse_X - mlbDownX);
         ObjectSetInteger(0,LABEL_OPT3,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT3 + mouse_Y - mlbDownY);

A adição da lógica de arraste dos outros elementos garantirá que, enquanto o ícone de arraste se move, os outros componentes do painel também se movem. Após a compilação, aqui está o resultado:

GUI DO ÍCONE DE ARRASTE FIXO

Isso foi um sucesso. Você pode ver que todos os componentes do painel se movem junto com o cursor do mouse. No entanto, há um pequeno problema que precisamos resolver. Quando o mouse é liberado, ou seja, não está no modo pressionado, os componentes continuam a se mover conforme o cursor se move. Para liberar o painel do estado de movimento, precisamos definir o estado como falso caso o mouse não esteja pressionado. 

      if (mouse_State == 0){
         movingState = false;
         ChartSetInteger(0,CHART_MOUSE_SCROLL,true);
      }

Se o estado do mouse for igual a zero, significa que o botão esquerdo do mouse foi liberado e, assim, definimos o estado de movimento como falso, indicando que não precisamos mais mover os componentes do painel. Mais tarde, ativamos o evento de rolagem do gráfico, definindo a flag como verdadeira. Finalmente, definimos o estado do mouse anterior para o estado atual do mouse.

      prevMouseClickState = mouse_State;

O código final responsável pela automação do efeito de hover e movimento do painel é o seguinte:

   else if (id==CHARTEVENT_MOUSE_MOVE){
      int mouse_X = (int)lparam;    // mouseX   >>> mouse coordinates
      int mouse_Y = (int)dparam;    // mouseY   >>> mouse coordinates
      int mouse_State = (int)sparam; // Get the mouse state (0 = mouse moving)
      
      //GETTING THE INITIAL DISTANCES AND SIZES OF BUTTON
      
      int XDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XDISTANCE);
      int YDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YDISTANCE);
      int XSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XSIZE);
      int YSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YSIZE);
      
      static bool prevMouseInside = false;
      bool isMouseInside = false;
      
      //Print("Mouse STATE = ",mouse_State); // 0 = mouse moving

      if (mouse_X >= XDistance_Hover_Btn && mouse_X <= XDistance_Hover_Btn + XSize_Hover_Btn &&
          mouse_Y >= YDistance_Hover_Btn && mouse_Y <= YDistance_Hover_Btn + YSize_Hover_Btn){
         isMouseInside = true;
      }
      
      if (isMouseInside != prevMouseInside) {
         // Mouse entered or left the button area
         if (isMouseInside) {
            Print("Mouse entered the Button area. Do your updates!");
            //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'050,050,255');

            ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'050,050,255');
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, clrLightBlue);
         }
         else if (!isMouseInside) {
            Print("Mouse left Btn proximities. Return default properties.");
            //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'100,100,100');
            // Reset button properties when mouse leaves the area
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'100,100,100');
            ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, C'220,220,220');
         }
         ChartRedraw(0);//// Redraw the chart to reflect the changes
         prevMouseInside = isMouseInside;
      }
      
      // CREATE MOVEMENT
      static int prevMouseClickState = false; // false = 0, true = 1;
      static bool movingState = false;
      
      // INITIALIZE VARIBALES TO STORE INITIAL SIZES AND DISTANCES OF OBJECTS
      // MLB = MOUSE LEFT BUTTON
      static int mlbDownX = 0; // Stores the X-coordinate of the mouse left button press
      static int mlbDownY = 0; // Stores the Y-coordinate of the mouse left button press
      
      static int mlbDownX_Distance = 0; // Stores the X-distance of an object
      static int mlbDownY_Distance = 0; // Stores the Y-distance of an object
      
      static int mlbDownX_Distance_BTN_DROP_DN = 0; // Stores X-distance for a specific button (BTN_DROP_DN)
      static int mlbDownY_Distance_BTN_DROP_DN = 0; // Stores Y-distance for the same button
      
      static int mlbDownX_Distance_LABEL_OPT1 = 0; // Stores X-distance for a label (LABEL_OPT1)
      static int mlbDownY_Distance_LABEL_OPT1 = 0; // Stores Y-distance for the same label
      
      static int mlbDownX_Distance_LABEL_OPT2 = 0; // Stores X-distance for another label (LABEL_OPT2)
      static int mlbDownY_Distance_LABEL_OPT2 = 0; // Stores Y-distance for the same label
      
      static int mlbDownX_Distance_LABEL_OPT3 = 0; // Stores X-distance for yet another label (LABEL_OPT3)
      static int mlbDownY_Distance_LABEL_OPT3 = 0; // Stores Y-distance for the same label
      
      static int mlbDownX_Distance_ICON_DRAG = 0; // Stores X-distance for an icon (ICON_DRAG)
      static int mlbDownY_Distance_ICON_DRAG = 0; // Stores Y-distance for the same icon
            
            
      //GET THE INITIAL DISTANCES AND SIZES OF BUTTON
      
      int XDistance_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_XDISTANCE);
      int YDistance_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_YDISTANCE);
      //int XSize_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_XSIZE);
      //int YSize_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_YSIZE);
      
      int XDistance_Opt1_Lbl = (int)ObjectGetInteger(0,LABEL_OPT1,OBJPROP_XDISTANCE);
      int YDistance_Opt1_Lbl = (int)ObjectGetInteger(0,LABEL_OPT1,OBJPROP_YDISTANCE);
      
      int XDistance_Opt2_Lbl = (int)ObjectGetInteger(0,LABEL_OPT2,OBJPROP_XDISTANCE);
      int YDistance_Opt2_Lbl = (int)ObjectGetInteger(0,LABEL_OPT2,OBJPROP_YDISTANCE);
      
      int XDistance_Opt3_Lbl = (int)ObjectGetInteger(0,LABEL_OPT3,OBJPROP_XDISTANCE);
      int YDistance_Opt3_Lbl = (int)ObjectGetInteger(0,LABEL_OPT3,OBJPROP_YDISTANCE);

      int XDistance_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_XDISTANCE);
      int YDistance_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_YDISTANCE);
      int XSize_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_XSIZE);
      int YSize_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_YSIZE);
            
      if (prevMouseClickState == false && mouse_State == 1) {
          // Check if the left mouse button was clicked and the mouse is in the pressed state
      
          // Initialize variables to store initial distances and sizes of objects
          mlbDownX = mouse_X; // Store the X-coordinate of the mouse click
          mlbDownY = mouse_Y; // Store the Y-coordinate of the mouse click
      
          // Store distances for specific objects
          mlbDownX_Distance = XDistance_Drag_Icon; // Distance of the drag icon (X-axis)
          mlbDownY_Distance = YDistance_Drag_Icon; // Distance of the drag icon (Y-axis)
      
          mlbDownX_Distance_BTN_DROP_DN = XDistance_DropDn_Btn; // Distance of BTN_DROP_DN
          mlbDownY_Distance_BTN_DROP_DN = YDistance_DropDn_Btn;
      
          mlbDownX_Distance_LABEL_OPT1 = XDistance_Opt1_Lbl; // Distance of LABEL_OPT1
          mlbDownY_Distance_LABEL_OPT1 = YDistance_Opt1_Lbl;
      
          mlbDownX_Distance_LABEL_OPT2 = XDistance_Opt2_Lbl; // Distance of LABEL_OPT2
          mlbDownY_Distance_LABEL_OPT2 = YDistance_Opt2_Lbl;
      
          mlbDownX_Distance_LABEL_OPT3 = XDistance_Opt3_Lbl; // Distance of LABEL_OPT3
          mlbDownY_Distance_LABEL_OPT3 = YDistance_Opt3_Lbl;
      
          // Check if the mouse is within the drag icon area
          if (mouse_X >= XDistance_Drag_Icon && mouse_X <= XDistance_Drag_Icon + XSize_Drag_Icon &&
              mouse_Y >= YDistance_Drag_Icon && mouse_Y <= YDistance_Drag_Icon + YSize_Drag_Icon) {
              movingState = true; // Set the moving state to true
          }
      }
            
      if (movingState){
         ChartSetInteger(0,CHART_MOUSE_SCROLL,false);
         
         ObjectSetInteger(0,ICON_DRAG,OBJPROP_XDISTANCE,mlbDownX_Distance + mouse_X - mlbDownX);
         ObjectSetInteger(0,ICON_DRAG,OBJPROP_YDISTANCE,mlbDownY_Distance + mouse_Y - mlbDownY);
         
         ObjectSetInteger(0,BTN_DROP_DN,OBJPROP_XDISTANCE,mlbDownX_Distance_BTN_DROP_DN + mouse_X - mlbDownX);
         ObjectSetInteger(0,BTN_DROP_DN,OBJPROP_YDISTANCE,mlbDownY_Distance_BTN_DROP_DN + mouse_Y - mlbDownY);
         
         ObjectSetInteger(0,LABEL_OPT1,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT1 + mouse_X - mlbDownX);
         ObjectSetInteger(0,LABEL_OPT1,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT1 + mouse_Y - mlbDownY);
         
         ObjectSetInteger(0,LABEL_OPT2,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT2 + mouse_X - mlbDownX);
         ObjectSetInteger(0,LABEL_OPT2,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT2 + mouse_Y - mlbDownY);

         ObjectSetInteger(0,LABEL_OPT3,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT3 + mouse_X - mlbDownX);
         ObjectSetInteger(0,LABEL_OPT3,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT3 + mouse_Y - mlbDownY);

         ChartRedraw(0);
      }
      
      if (mouse_State == 0){
         movingState = false;
         ChartSetInteger(0,CHART_MOUSE_SCROLL,true);
      }
      prevMouseClickState = mouse_State;
   }

Em poucas palavras, foi isso que alcançamos.

GIF FINAL

Isso foi Ótimo! Agora adicionamos vida ao nosso painel GUI e ele está interativo e responsivo. Ele possui efeitos de hover, cliques em botões, atualizações de dados ao vivo e responde aos movimentos do mouse.


Conclusão

Em conclusão, pela implementação do artigo, podemos dizer que integrar recursos dinâmicos em um painel GUI do MetaQuotes Language 5 (MQL5) melhora significativamente a experiência do usuário, tornando-a mais interativa e funcional. Adicionar efeitos de hover nos botões cria uma interface visualmente atraente que responde de forma intuitiva às ações do usuário. As atualizações em tempo real dos preços de compra e venda garantem que os traders tenham as informações mais atuais do mercado, permitindo-lhes tomar decisões informadas rapidamente. Botões clicáveis para executar ordens de compra e venda, bem como recursos de fechamento de posições e ordens, agilizam as operações de negociação, permitindo que os usuários reajam rapidamente às mudanças do mercado.

Além disso, a implementação de subpainéis móveis e listas suspensas adiciona uma camada de personalização e flexibilidade à interface. Os traders podem organizar seu espaço de trabalho de acordo com suas preferências, melhorando sua eficiência geral. A funcionalidade de lista suspensa oferece uma maneira conveniente de acessar várias opções sem sobrecarregar a interface principal, contribuindo para um ambiente de negociação mais limpo e organizado. No geral, essas melhorias transformam o painel GUI do MQL5 em uma ferramenta robusta e amigável ao usuário, atendendo às necessidades dos traders modernos e, finalmente, aprimorando sua experiência e eficácia na negociação. Os traders podem usar o conhecimento ilustrado para criar painéis GUI mais complexos e atraentes que melhorem sua experiência de negociação. Esperamos que você tenha achado o artigo detalhado, objetivamente explicado e fácil de seguir e aprender. Saudações!


Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/15263

Arquivos anexados |
Últimos Comentários | Ir para discussão (5)
Clemence Benjamin
Clemence Benjamin | 14 jul. 2024 em 05:51
Excelente trabalho, obrigado!
Allan Munene Mutiiria
Allan Munene Mutiiria | 14 jul. 2024 em 11:22
Clemence Benjamin #:
Excelente trabalho, obrigado!

@Clemence Benjamin, obrigado pelo feedback e pelo reconhecimento. É muito gentil de sua parte. Seja bem-vindo.

Jasser90
Jasser90 | 11 ago. 2024 em 04:07
Trabalho incrível, obrigado por compartilhar.
Allan Munene Mutiiria
Allan Munene Mutiiria | 11 ago. 2024 em 13:14
Jasser90 #:
Trabalho incrível, obrigado por compartilhar.
@Jasser90, obrigado por seu feedback gentil e reconhecimento. Nós o apreciamos muito.
Sergei Poliukhov
Sergei Poliukhov | 4 mar. 2025 em 20:55

Obrigado, ótimo material, você pode fazer um fechamento parcial em uma conta de compensação?

Porque a ordem oposta está aberta....

Estratégia de Trading Cascade Order Baseada em Cruzamentos de EMA para MetaTrader 5 Estratégia de Trading Cascade Order Baseada em Cruzamentos de EMA para MetaTrader 5
Este artigo orienta sobre como demonstrar um algoritmo automatizado baseado em cruzamentos de EMA para MetaTrader 5. Informações detalhadas sobre todos os aspectos de demonstrar um Expert Advisor em MQL5 e testá-lo no MetaTrader 5 – desde a análise de comportamentos de faixa de preços até o gerenciamento de risco.
Como Integrar o Conceito de Smart Money (BOS) Junto com o Indicador RSI em um EA Como Integrar o Conceito de Smart Money (BOS) Junto com o Indicador RSI em um EA
Conceito de Smart Money (Break Of Structure) acoplado com o Indicador RSI para tomar decisões informadas de negociação automatizada com base na estrutura do mercado.
Otimização de Portfólio em Python e MQL5 Otimização de Portfólio em Python e MQL5
Este artigo explora técnicas avançadas de otimização de portfólio usando Python e MQL5 com o MetaTrader 5. Ele demonstra como desenvolver algoritmos para análise de dados, alocação de ativos e geração de sinais de negociação, enfatizando a importância da tomada de decisões orientada por dados na gestão financeira moderna e na mitigação de riscos.
Eigenvetores e autovalores: Análise exploratória de dados no MetaTrader 5 Eigenvetores e autovalores: Análise exploratória de dados no MetaTrader 5
Neste artigo, exploramos diferentes maneiras pelas quais os eigenvetores e os autovalores podem ser aplicados na análise exploratória de dados para revelar relacionamentos únicos nos dados.