English Русский 中文 Español Deutsch 日本語
preview
Interface Gráfico: Dicas e recomendações para criar uma biblioteca gráfica no MQL

Interface Gráfico: Dicas e recomendações para criar uma biblioteca gráfica no MQL

MetaTrader 5Exemplos | 8 março 2024, 14:22
198 0
Manuel Alejandro Cercos Perez
Manuel Alejandro Cercos Perez

Introdução

O desenvolvimento de uma biblioteca de interface gráfica é um dos projetos mais significativos no contexto do MetaTrader 5, juntamente com avanços como inteligência artificial, redes neurais eficientes e... a habilidade de usar livremente a biblioteca de interface gráfica desenvolvida.

Com o último ponto, eu estava brincando um pouco. Claro, é mais fácil aprender a usar uma biblioteca pronta (embora existam bibliotecas de interface gráfica realmente grandes)! Mas se eu posso aprender a usar uma biblioteca que já é melhor do que aquela que eu poderia criar, por que criar uma do zero?

Existem várias razões válidas. A biblioteca pode ser muito lenta para o seu projeto específico, você pode precisar expandi-la (algumas bibliotecas são mais difíceis de expandir do que outras) ou ela pode conter erros (exceto aqueles causados por uso impróprio da biblioteca). Ou talvez você simplesmente queira aprender a escrever bibliotecas por conta própria. A maioria desses problemas pode ser resolvida pelos autores de qualquer biblioteca específica, mas você terá que depender deles.

O objetivo deste artigo não é ensinar você a criar uma interface ou mostrar os passos para desenvolver uma biblioteca totalmente funcional. Em vez disso, vou mostrar através de exemplos como criar partes específicas de bibliotecas de interface gráfica, que podem servir como ponto de partida para resolver um problema específico, bem como para obter uma compreensão inicial do que acontece dentro do vasto código de uma biblioteca de interface gráfica já pronta.


Estrutura do programa e hierarquia de objetos

O que é uma biblioteca de interface gráfica? Em resumo, é uma hierarquia de objetos que rastreiam outros objetos (gráficos) e alteram suas propriedades para criar diferentes efeitos e acionar eventos, como movimento, clique ou mudança de cor. A organização desta hierarquia pode variar, no entanto, a estrutura de árvore dos elementos, onde um elemento pode ter subelementos, é a mais comum (e a que pessoalmente prefiro).

Vamos começar com a implementação básica de um elemento:

class CElement
{
private:
   //Variable to generate names
   static int        m_element_count;

   void              AddChild(CElement* child);

protected:
   //Chart object name
   string            m_name;

   //Element relations
   CElement*         m_parent;
   CElement*         m_children[];
   int               m_child_count;

   //Position and size
   int               m_x;
   int               m_y;
   int               m_size_x;
   int               m_size_y;

public:
                     CElement();
                    ~CElement();

   void              SetPosition(int x, int y);
   void              SetSize(int x, int y);
   void              SetParent(CElement* parent);

   int               GetGlobalX();
   int               GetGlobalY();

   void              CreateChildren();
   virtual void      Create(){}
};

A classe base do elemento, por ora, contém apenas informações sobre a posição, tamanho e relações com outros elementos.

As variáveis posicionais m_x e m_y representam posições locais no contexto do objeto pai. Isso cria a necessidade de uma função de posição global para determinar a posição real do objeto na tela. Abaixo, você pode ver como podemos obter recursivamente a posição global (neste caso, para X):

int CElement::GetGlobalX(void)
{
   if (CheckPointer(m_parent)==POINTER_INVALID)
      return m_x;

   return m_x + m_parent.GetGlobalX();
}

No construtor, precisamos definir um nome único para cada objeto. Para isso, podemos usar uma variável estática. Por razões específicas, prefiro manter essa variável na classe do programa, mostrada abaixo, mas para simplificar, vamos colocá-la dentro da classe elemento.

Não se esqueça de deletar os elementos filhos no destrutor para evitar vazamentos de memória!

int CElement::m_element_count = 0;

//+------------------------------------------------------------------+
//| Base Element class constructor                                   |
//+------------------------------------------------------------------+
CElement::CElement(void) : m_child_count(0), m_x(0), m_y(0), m_size_x(100), m_size_y(100)
{
   m_name = "element_"+IntegerToString(m_element_count++);
}

//+------------------------------------------------------------------+
//| Base Element class destructor (delete child objects)             |
//+------------------------------------------------------------------+
CElement::~CElement(void)
{
   for (int i=0; i<m_child_count; i++)
      delete m_children[i];
}

Finalmente, definimos as funções de relação AddChild e SetParent, já que precisaremos de ambas as referências para a conexão entre elementos: por exemplo, para obter a posição global, o elemento filho precisa saber a posição do pai, mas ao mudar a posição, o pai deve notificar os elementos filhos (implementaremos esta última parte mais tarde). Para evitar duplicação, marcamos AddChild como privado.

Nas funções create, criaremos os próprios objetos gráficos (e modificaremos outras propriedades). É importante garantir que os elementos filhos sejam criados após o pai, por isso usamos uma função separada para esse propósito (já que a função create pode ser sobrescrita, e isso pode alterar a ordem de execução). Na classe do elemento base, create é vazio.

//+------------------------------------------------------------------+
//| Set parent object (in element hierarchy)                         |
//+------------------------------------------------------------------+
void CElement::SetParent(CElement *parent)
{
   m_parent = parent;
   parent.AddChild(GetPointer(this));
}

//+------------------------------------------------------------------+
//| Add child object reference (function not exposed)                |
//+------------------------------------------------------------------+
void CElement::AddChild(CElement *child)
{
   if (CheckPointer(child)==POINTER_INVALID)
      return;

   ArrayResize(m_children, m_child_count+1);
   m_children[m_child_count] = child;
   m_child_count++;
}
//+------------------------------------------------------------------+
//| First creation of elements (children after)                      |
//+------------------------------------------------------------------+
void CElement::CreateChildren(void)
{
   for (int i=0; i<m_child_count; i++)
   {
      m_children[i].Create();
      m_children[i].CreateChildren();
   }
}

Criaremos a classe program. Por agora, é apenas um placeholder para interação indireta com um elemento vazio, usado como suporte, mas no futuro, ele realizará outras operações centralizadas que afetam todos os elementos (e que você não quer executar várias vezes). Usar um suporte de elemento vazio para armazenar outros elementos nos permitirá não reescrever qualquer função que exija de nós a iteração recursiva através dos elementos filhos. Por enquanto, não precisaremos de um construtor/destrutor para esta classe, já que não estamos armazenando o suporte como um ponteiro.

class CProgram
{
protected:
   CElement          m_element_holder;

public:

   void              CreateGUI() { m_element_holder.CreateChildren(); }
   void              AddMainElement(CElement* element) { element.SetParent(GetPointer(m_element_holder)); }
};

Antes de realizarmos nosso primeiro teste, ainda precisamos expandir a classe Element, já que atualmente ela está vazia. Isso nos permitirá gerenciar diferentes tipos de objetos de maneiras variadas. Primeiro, vamos criar um elemento de tela (canvas), usando CCanvas (que, na verdade, é uma etiqueta de bitmap). Os objetos de canvas são os mais versáteis para criar interfaces gráficas customizadas, e podemos fazer praticamente qualquer coisa completamente a partir do canvas:

#include <Canvas/Canvas.mqh>
//+------------------------------------------------------------------+
//| Generic Bitmap label element (Canvas)                            |
//+------------------------------------------------------------------+
class CCanvasElement : public CElement
{
protected:
   CCanvas           m_canvas;

public:
                    ~CCanvasElement();

   virtual void      Create();
};

//+------------------------------------------------------------------+
//| Canvas Element destructor (destroy canvas)                       |
//+------------------------------------------------------------------+
CCanvasElement::~CCanvasElement(void)
{
   m_canvas.Destroy();
}

//+------------------------------------------------------------------+
//| Create bitmap label (override)                                   |
//+------------------------------------------------------------------+
void CCanvasElement::Create()
{
   CElement::Create();

   m_canvas.CreateBitmapLabel(0, 0, m_name, GetGlobalX(), GetGlobalY(), m_size_x, m_size_y, COLOR_FORMAT_ARGB_NORMALIZE);
}

Adicionaremos essas duas linhas no final do Create para preencher o canvas com uma cor aleatória. Uma maneira mais adequada seria expandir a classe canvas para criar esse tipo específico de objeto, mas por enquanto não há necessidade de complicar o código.

m_canvas.Erase(ARGB(255, MathRand()%256, MathRand()%256, MathRand()%256));
m_canvas.Update(false);

Também criaremos a classe do objeto de edição (edit). Mas por que especificamente isso? Por que não fazer nossas próprias modificações, desenhando diretamente no canvas e acompanhando eventos do teclado? Há uma coisa que não podemos fazer com o canvas. Isso é copiar e colar texto (pelo menos fora do próprio aplicativo) sem uma DLL. Se você não precisa dessa funcionalidade para sua biblioteca, pode adicionar o canvas diretamente na classe Element e usá-lo para cada tipo de objeto. Como você pode ter notado, algumas coisas em relação ao canvas são feitas de maneira diferente...

class CEditElement : public CElement
{
public:
                    ~CEditElement();

   virtual void      Create();

   string            GetEditText() { return ObjectGetString(0, m_name, OBJPROP_TEXT); }
   void              SetEditText(string text) { ObjectSetString(0, m_name, OBJPROP_TEXT, text); }

};

//+------------------------------------------------------------------+
//| Edit element destructor (remove object from chart)               |
//+------------------------------------------------------------------+
CEditElement::~CEditElement(void)
{
   ObjectDelete(0, m_name);
}

//+------------------------------------------------------------------+
//| Create edit element (override) and set size/position             |
//+------------------------------------------------------------------+
void CEditElement::Create()
{
   CElement::Create();

   ObjectCreate(0, m_name, OBJ_EDIT, 0, 0, 0);

   ObjectSetInteger(0, m_name, OBJPROP_XDISTANCE, GetGlobalX());
   ObjectSetInteger(0, m_name, OBJPROP_YDISTANCE, GetGlobalY());
   ObjectSetInteger(0, m_name, OBJPROP_XSIZE, m_size_x);
   ObjectSetInteger(0, m_name, OBJPROP_YSIZE, m_size_y);
}

Neste caso, precisamos explicitamente definir as propriedades de posição e tamanho (no Canvas, elas são feitas dentro do CreateBitmapLabel).

Com todas essas mudanças, finalmente podemos realizar o primeiro teste:

#include "Basis.mqh"
#include "CanvasElement.mqh"
#include "EditElement.mqh"

input int squares = 5; //Amount of squares
input bool add_edits = true; //Add edits to half of the squares

CProgram program;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   MathSrand((uint)TimeLocal());
   
   //100 is element size by default
   int max_x = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS) - 100; 
   int max_y = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS) - 100;
   
   for (int i=0; i<squares; i++)
   {
      CCanvasElement* drawing = new CCanvasElement();
      drawing.SetPosition(MathRand()%max_x, MathRand()%max_y);
      program.AddMainElement(drawing);
      
      if (add_edits && i<=squares/2)
      {
         CEditElement* edit = new CEditElement();
         edit.SetParent(drawing);
         edit.SetPosition(10, 10);
         edit.SetSize(80, 20);
      }
   }
   
   program.CreateGUI();
   ChartRedraw(0);
   
   return(INIT_SUCCEEDED);
}

O programa criará vários quadrados e os removerá quando forem deletados do gráfico. Note que cada elemento edit está posicionado em relação ao seu pai no mesmo local.

Neste momento, esses quadrados não fazem nada. Nas seções seguintes, discutiremos como adicionar diferentes comportamentos a eles.


Movimento do Mouse

Se você já trabalhou com eventos gráficos, sabe que o uso de eventos de clique não é suficiente para criar qualquer comportamento complexo. O truque para criar interfaces melhores está em usar eventos de movimento do mouse. Esses eventos precisam ser ativados no lançamento do EA, portanto, os ativaremos antes de criar a interface gráfica:

void CProgram::CreateGUI(void)
{
   ::ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
   m_element_holder.CreateChildren();
}

Eles definem a posição do mouse (x,y) e o estado dos botões do mouse, Ctrl e Shift. Sempre que pelo menos um desses estados ou a posição muda, um evento é disparado.

Primeiro, definiremos os estágios que um botão pode passar: inativo, quando não está sendo usado, e ativo, quando está pressionado. Também adicionaremos down e up, que representam os primeiros estados ativo e inativo, respectivamente (mudanças de estado de clique). Como podemos detectar cada fase apenas com eventos de mouse, nem precisamos usar eventos de clique.

enum EInputState
{
   INPUT_STATE_INACTIVE=0,
   INPUT_STATE_UP=1,
   INPUT_STATE_DOWN=2,
   INPUT_STATE_ACTIVE=3
};

Para simplificar a tarefa, centralizaremos o processamento dos eventos de mouse, em vez de realizar dentro de cada objeto. Isso nos permitirá acessar e rastrear os dados do mouse mais facilmente, o que permite seu uso até mesmo para outros tipos de eventos e evita cálculos repetitivos. Chamaremos essa classe de CInputs, já que ela inclui os botões Ctrl e Shift, além da entrada do mouse.

//+------------------------------------------------------------------+
//| Mouse input processing class                                     |
//+------------------------------------------------------------------+
class CInputs
{
private:
   EInputState m_left_mouse_state;
   EInputState m_ctrl_state;
   EInputState m_shift_state;

   int m_pos_x;
   int m_pos_y;

   void UpdateState(EInputState &state, bool current);

public:
   CInputs();

   void OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam);

   EInputState GetLeftMouseState() { return m_left_mouse_state; }
   EInputState GetCtrlState() { return m_ctrl_state; }
   EInputState GetShiftState() { return m_shift_state; }
   
   int X() { return m_pos_x; }
   int Y() { return m_pos_y; }
};

//+------------------------------------------------------------------+
//| Inputs constructor (initialize variables)                        |
//+------------------------------------------------------------------+
CInputs::CInputs(void) : m_left_mouse_state(INPUT_STATE_INACTIVE), m_ctrl_state(INPUT_STATE_INACTIVE),
   m_shift_state(INPUT_STATE_INACTIVE), m_pos_x(0), m_pos_y(0)
{
}

//+------------------------------------------------------------------+
//| Mouse input event processing                                     |
//+------------------------------------------------------------------+
void CInputs::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   if(id!=CHARTEVENT_MOUSE_MOVE)
      return;

   m_pos_x = (int)lparam;
   m_pos_y = (int)dparam;

   uint state = uint(sparam);

   UpdateState(m_left_mouse_state, ((state & 1) == 1));
   UpdateState(m_ctrl_state, ((state & 8) == 8));
   UpdateState(m_shift_state, ((state & 4) == 4));
}

//+------------------------------------------------------------------+
//| Update state of a button (up, down, active, inactive)            |
//+------------------------------------------------------------------+
void CInputs::UpdateState(EInputState &state, bool current)
{
   if (current)
      state = (state>=INPUT_STATE_DOWN) ? INPUT_STATE_ACTIVE : INPUT_STATE_DOWN;
   else
      state = (state>=INPUT_STATE_DOWN) ? INPUT_STATE_UP : INPUT_STATE_INACTIVE;
}
//+------------------------------------------------------------------+

Em UpdateState, verificamos o estado atual (valor booleano) e o último estado para determinar se a entrada está ativa/inativa e se este é o primeiro evento após uma mudança de estado (up/down). As informações sobre ctrl e shift nós obtemos "gratuitamente" no sparam. Também recebemos informações sobre o botão do meio, o direito e outros dois botões adicionais do mouse. Não os adicionamos ao código, mas você pode fazer as alterações necessárias, se necessário.

Adicionaremos uma instância de entrada do mouse ao programa e a tornaremos acessível a cada objeto através de um ponteiro. A instância de entrada é atualizada a cada evento. O tipo de evento é filtrado internamente na classe de entrada (apenas eventos de movimento do mouse são usados).

class CProgram
{
//...

protected:
   CInputs           m_inputs;

//...

public:
   void              OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam);

//...
};

//+------------------------------------------------------------------+
//| Program constructor (pass inputs reference to holder)            |
//+------------------------------------------------------------------+
CProgram::CProgram(void)
{
   m_element_holder.SetInputs(GetPointer(m_inputs));
}

//+------------------------------------------------------------------+
//| Process chart event                                              |
//+------------------------------------------------------------------+
void CProgram::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   m_inputs.OnEvent(id, lparam, dparam, sparam);

   m_element_holder.OnChartEvent(id, lparam, dparam, sparam);
}

Na próxima seção, forneceremos uma explicação mais detalhada de como o OnChartEvent será usado.

Ao configurar as entradas para o elemento global, elas são passadas recursivamente para seus elementos filhos. No entanto, não devemos esquecer de passá-las também ao adicionar novos elementos filhos (após a primeira chamada de SetInputs):

//+------------------------------------------------------------------+
//| Set mouse inputs reference                                       |
//+------------------------------------------------------------------+
void CElement::SetInputs(CInputs* inputs)
{
   m_inputs = inputs;

   for (int i = 0; i < m_child_count; i++)
      m_children[i].SetInputs(inputs);
}

//+------------------------------------------------------------------+
//| Add child object reference (function not exposed)                |
//+------------------------------------------------------------------+
void CElement::AddChild(CElement *child)
{
   if (CheckPointer(child) == POINTER_INVALID)
      return;

   ArrayResize(m_children, m_child_count + 1);
   m_children[m_child_count] = child;
   m_child_count++;

   child.SetInputs(m_inputs);
}

No próximo segmento, vamos criar funções de processamento de eventos e adicionar vários eventos de mouse a cada objeto.


Processamento de Eventos

Antes de começarmos a processar os eventos em si, precisamos reconhecer algo que será necessário se quisermos ter um movimento suave na interface - redesenho do grafo. Sempre que a propriedade de um objeto muda, como sua posição, ou se ele for redesenhado, precisamos redesenhar o grafo para que essas mudanças se reflitam imediatamente. No entanto, chamar ChartRedraw com muita frequência pode causar cintilação na interface gráfica, por isso prefiro centralizar sua execução:

class CProgram
{
private:
   bool              m_needs_redraw;

//...

public:
   CProgram();

   void              OnTimer();

//...

   void              RequestRedraw()
   {
      m_needs_redraw = true;
   }
};

void CProgram::OnTimer(void)
{
   if (m_needs_redraw)
   {
      ChartRedraw(0);
      m_needs_redraw = false;
   }
}

OnTimer deve ser chamado na função de processamento de eventos homônima, e cada elemento deve ter uma referência ao programa para ser capaz de chamar RequestRedraw. Essa função definirá uma flag que, quando ativada, redesenha todos os elementos na próxima chamada do timer. Também precisamos definir o timer primeiro:

#define TIMER_STEP_MSC (16)

void CProgram::CreateGUI(void)
{
   ::ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
   m_element_holder.CreateChildren();
   ::EventSetMillisecondTimer(TIMER_STEP_MSC);
}

16 milissegundos é o intervalo aproximado em que os timers podem ser executados de forma confiável. No entanto, programas pesados podem bloquear a execução do timer.

A seguir, é mostrado como os eventos do grafo são implementados em cada elemento:

//+------------------------------------------------------------------+
//| Send event recursively and respond to it (for this element)      |
//+------------------------------------------------------------------+
void CElement::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   for (int i = m_child_count - 1; i >= 0; i--)
      m_children[i].OnChartEvent(id, lparam, dparam, sparam);

   OnEvent(id, lparam, dparam, sparam);
}

Nesta biblioteca, decidimos passar os eventos recursivamente do elemento pai para o filho. Essa é uma escolha de design (pode ser feita de forma diferente, por exemplo, usando ouvintes ou chamando todos os objetos em sequência), mas, como veremos mais tarde, ela tem algumas vantagens. OnEvent é uma função protegida que pode ser sobrescrita. Ela é separada do OnChartEvent (que é público), então o usuário não pode sobrescrever a passagem de eventos para os objetos filhos e escolher como executar o OnEvent da classe pai (antes, depois ou não executá-lo).

Como um exemplo de processamento de eventos, vamos implementar a função de arrastar quadrados. Eventos de clique e hover também podem ser implementados de maneira mais trivial, mas não precisaremos deles neste exemplo. Por enquanto, os eventos são passados apenas através dos objetos, mas não fazem nada. No entanto, no estado atual, há um problema: se você tentar arrastar um objeto, o grafo atrás dele se moverá, como se o arrasto não tivesse ocorrido. Para fazer os gráficos permanecerem no lugar:

Objetos que não reagem a nenhum evento

Para evitar esse problema, primeiro verificamos se o mouse está sobre algum objeto. Se sim, bloqueamos a rolagem do gráfico. Por questões de desempenho, verificaremos apenas aqueles objetos que são elementos filhos diretos do detentor global (o programa funcionará sempre, se outros subobjetos permanecerem dentro do objeto pai).

//+------------------------------------------------------------------+
//| Check if mouse is hovering any child element of this object      |
//+------------------------------------------------------------------+
bool CElement::CheckHovers(void)
{
   for (int i = 0; i < m_child_count; i++)
   {
      if (m_children[i].IsMouseHovering())
         return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| Process chart event                                              |
//+------------------------------------------------------------------+
void CProgram::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   m_inputs.OnEvent(id, lparam, dparam, sparam);

   if (id == CHARTEVENT_MOUSE_MOVE)
      EnableControls(!m_element_holder.CheckHovers());

   m_element_holder.OnChartEvent(id, lparam, dparam, sparam);
}

//+------------------------------------------------------------------+
//| Enable/Disable chart scroll responses                            |
//+------------------------------------------------------------------+
void CProgram::EnableControls(bool enable)
{
   //Allow or disallow displacing chart
   ::ChartSetInteger(0, CHART_MOUSE_SCROLL, enable);
}

Agora eles evitam deslocamentos... mas apenas quando o cursor do mouse está sobre o objeto. Se o arrastarmos para fora, os gráficos ainda se moverão.

Precisaremos adicionar mais duas verificações e uma variável lógica privada em CElement (m_dragged):

//+------------------------------------------------------------------+
//| Send event recursively and respond to it (for this element)      |
//+------------------------------------------------------------------+
void CElement::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   for (int i = m_child_count - 1; i >= 0; i--)
      m_children[i].OnChartEvent(id, lparam, dparam, sparam);

   //Check dragging start
   if (id == CHARTEVENT_MOUSE_MOVE)
   {
      if (IsMouseHovering() && m_inputs.GetLeftMouseState() == INPUT_STATE_DOWN)
         m_dragging = true;

      else if (m_dragging && m_inputs.GetLeftMouseState() == INPUT_STATE_UP)
         m_dragging = false;
   }

   OnEvent(id, lparam, dparam, sparam);
}

//+------------------------------------------------------------------+
//| Check if mouse hovers/drags any child element of this object     |
//+------------------------------------------------------------------+
bool CElement::CheckHovers(void)
{
   for (int i = 0; i < m_child_count; i++)
   {
      if (m_children[i].IsMouseHovering() || m_children[i].IsMouseDragging())
         return true;
   }
   return false;
}

Agora, ao arrastar objetos, tudo funciona normalmente, mas falta uma pequena correção... ao arrastar gráficos. Se o mouse passar sobre o objeto, o arrasto é interrompido. Precisamos filtrar os arrastos que começam fora do objeto.

Objetos interrompem o arrasto dos gráficos

Felizmente, corrigir esse comportamento é fácil:

//+------------------------------------------------------------------+
//| Check if mouse hovers/drags any child element of this object     |
//+------------------------------------------------------------------+
bool CElement::CheckHovers(void)
{
   EInputState state = m_inputs.GetLeftMouseState();
   bool state_check = state != INPUT_STATE_ACTIVE; //Filter drags that start in chart

   for (int i = 0; i < m_child_count; i++)
   {
      if ((m_children[i].IsMouseHovering() && state_check)
            || m_children[i].IsMouseDragging())
         return true;
   }
   return false;
}

Agora, se o mouse estiver sobre o objeto e estiver ativo, o arrasto do gráfico não é interrompido (ao arrastar o objeto, ele retornará true). A verificação do mouse ainda é necessária, pois, sem ela, os eventos seriam desativados um quadro depois.

Filtragem correta dos arrastos

Agora que tudo está pronto, podemos adicionar a função de arrasto à classe retângulo. No entanto, em vez de adicioná-lo em CCanvasElement, nós expandiremos a classe por meio de herança. Também extrairemos a figura da linha do último exemplo, para que por padrão ela fique vazia. Como já adicionamos a verificação de arrasto, podemos usá-las para manipular o evento e mover os objetos. Para mudar a posição do objeto no gráfico, precisamos alterar suas variáveis, depois atualizar suas propriedades de posição, a posição filha e redesenhar o gráfico.

class CElement
{
//...

public:
   void              UpdatePosition();
};

//+------------------------------------------------------------------+
//| Update element (and children) position properties                |
//+------------------------------------------------------------------+
void CElement::UpdatePosition(void)
{
   ObjectSetInteger(0, m_name, OBJPROP_XDISTANCE, GetGlobalX());
   ObjectSetInteger(0, m_name, OBJPROP_YDISTANCE, GetGlobalY());

   for (int i = 0; i < m_child_count; i++)
      m_children[i].UpdatePosition();
}
class CCanvasElement : public CElement
{
protected:
   CCanvas           m_canvas;
   virtual void      DrawCanvas() {}

//...
};

//+------------------------------------------------------------------+
//| Create bitmap label (override)                                   |
//+------------------------------------------------------------------+
void CCanvasElement::Create()
{
//...

   DrawCanvas();
}
//+------------------------------------------------------------------+
//| Canvas class which responds to mouse drag events                 |
//+------------------------------------------------------------------+
class CDragElement : public CCanvasElement
{
private:
   int m_rel_mouse_x, m_rel_mouse_y;

protected:
   virtual void      DrawCanvas();

protected:
   virtual bool      OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam);
};

//+------------------------------------------------------------------+
//| Check mouse drag events                                          |
//+------------------------------------------------------------------+
bool CDragElement::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   if (id != CHARTEVENT_MOUSE_MOVE)
      return false;

   if (!IsMouseDragging())
      return false;

   if (m_inputs.GetLeftMouseState() == INPUT_STATE_DOWN) //First click
   {
      m_rel_mouse_x = m_inputs.X() - m_x;
      m_rel_mouse_y = m_inputs.Y() - m_y;
      return true;
   }

   //Move object
   m_x = m_inputs.X() - m_rel_mouse_x;
   m_y = m_inputs.Y() - m_rel_mouse_y;
   UpdatePosition();
   m_program.RequestRedraw();

   return true;
}

//+------------------------------------------------------------------+
//| Custom canvas draw function (fill with random color)             |
//+------------------------------------------------------------------+
void CDragElement::DrawCanvas(void)
{
   m_canvas.Erase(ARGB(255, MathRand() % 256, MathRand() % 256, MathRand() % 256));
   m_canvas.Update(false);
}

Observe que precisamos estabelecer a posição global do objeto: os objetos gráficos não sabem nada sobre a hierarquia dentro deles. Se tentarmos mover o objeto agora, ele funcionará e também atualizará a posição de seus elementos filhos, no entanto, podemos mover objetos que estão atrás dele:

Vários objetos se movem simultaneamente

Já que cada evento é recursivamente enviado a todos os objetos, eles o recebem, mesmo se estiverem localizados atrás de outro objeto. Precisamos encontrar uma maneira de filtrar eventos, se um objeto os recebe primeiro, em outras palavras, precisamos sobrepor eles.

Vamos criar uma variável lógica para acompanhar se o objeto está sobreposto:

class CElement
{
private:

   //...
   bool              m_occluded;

   //...

public:

   //...
   void              SetOccluded(bool occluded)
   {
      m_occluded = occluded;
   }
   bool              IsOccluded()
   {
      return m_occluded;
   }
   //...
};

Então podemos usar OnChartEvent como uma maneira de "comunicação" entre os objetos. Para isso, retornamos um valor lógico. Se o objeto recebeu o evento, seria true. Se o objeto foi arrastado (mesmo se o objeto não reagiu ao arrastar) ou se o objeto está sobreposto (por exemplo, por um objeto filho), ele também retornará true, pois isso também bloqueará eventos para os objetos abaixo dele.

bool CElement::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   for (int i = m_child_count - 1; i >= 0; i--)
   {
      m_children[i].SetOccluded(IsOccluded());
      if (m_children[i].OnChartEvent(id, lparam, dparam, sparam))
         SetOccluded(true);
   }
   //Check dragging start
   if (id == CHARTEVENT_MOUSE_MOVE && !IsOccluded())
   {
      if (IsMouseHovering() && m_inputs.GetLeftMouseState() == INPUT_STATE_DOWN)
         m_dragging = true;

      else if (m_dragging && m_inputs.GetLeftMouseState() == INPUT_STATE_UP)
         m_dragging = false;
   }

   return OnEvent(id, lparam, dparam, sparam) || IsMouseDragging() || IsOccluded();
}

OnEvent para um objeto é executado após os eventos de seus elementos filhos para levar em conta as sobreposições. Finalmente, precisamos adicionar filtragem de eventos ao nosso objeto arrastável personalizado:

bool CDragElement::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   if (IsOccluded()) return false;

   if (id != CHARTEVENT_MOUSE_MOVE)
      return false;

   //...
}

Observe que não filtramos eventos ao enviá-los. Isso é porque alguns elementos podem reagir aos eventos, mesmo se estiverem cobertos (por exemplo, alguns eventos que reagem ao passar do mouse ou apenas à posição do mouse).

Tendo todos esses elementos, alcançamos o comportamento desejado no segundo exemplo: cada objeto pode ser arrastado, simultaneamente bloqueando-o com outros objetos acima:

Objetos respondem corretamente ao arrastar

No entanto, deve-se notar que, embora esta estrutura funcione para este caso, outras interfaces mais complexas podem requerer algumas modificações. Deixaremos esta tarefa fora do artigo, pois o aumento da complexidade do código não seria justificado neste caso específico. Por exemplo, é possível diferenciar sobreposições de objetos filhos e objetos relacionados (que estão no mesmo nível de hierarquia, mas que vêm primeiro). Ou pode ser útil rastrear qual objeto recebeu o evento e primeiro verificar isso no próximo quadro (para evitar sobreposições inesperadas).


Mostrar e esconder objetos

Outra função importante necessária em qualquer biblioteca gráfica é a capacidade de esconder e mostrar objetos: por exemplo, ao abrir e fechar janelas, alterar seu conteúdo com abas de navegação ou ao remover botões que não estão disponíveis sob certas condições.

Métodos ingênuos para conseguir isso seriam deletar e criar objetos toda vez que você quiser escondê-los ou mostrá-los, ou mesmo evitar essa função. No entanto, existe uma maneira de esconder objetos, usando suas propriedades nativas (embora isso não seja óbvio pelo nome):

ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); //Show
ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); //Hide

Esta propriedade define, "em quais frames de tempo o objeto é exibido, e em quais não é". Você pode exibir objetos apenas em certos frames de tempo, mas é improvável que precisemos disso aqui.

Considerando as chamadas de função ObjectSetInteger, descritas acima, podemos implementar a função de mostrar e esconder objetos em nossa hierarquia de objetos da interface gráfica:

class CElement
{
private:
   //...
   bool              m_hidden;
   bool              m_hidden_parent;

   void              HideObject();
   void              HideByParent();
   void              HideChildren();

   void              ShowObject();
   void              ShowByParent();
   void              ShowChildren();
   //...

public:
   
   //...
   void              Hide();
   void              Show();
   bool              IsHidden()
   {
      return m_hidden || m_hidden_parent;
   }
};

//+------------------------------------------------------------------+
//| Display element (if parent is also visible)                      |
//+------------------------------------------------------------------+
void CElement::Show(void)
{

   if (!IsHidden())

      return;
        
   m_hidden = false;
   ShowObject();
        

   if (CheckPointer(m_program) != POINTER_INVALID)
      m_program.RequestRedraw();
}

//+------------------------------------------------------------------+
//| Hide element                                                     |
//+------------------------------------------------------------------+
void CElement::Hide(void)
{
   m_hidden = true;

   if (m_hidden_parent)
      return;

   HideObject();

   if (CheckPointer(m_program) != POINTER_INVALID)
      m_program.RequestRedraw();
}

//+------------------------------------------------------------------+
//| Change visibility property to show (not exposed)                 |
//+------------------------------------------------------------------+
void CElement::ShowObject(void)
{
   if (IsHidden()) //Parent or self
      return;
        
   ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS);
   ShowChildren();
}

//+------------------------------------------------------------------+
//| Show object when not hidden and parent is shown (not exposed)    |
//+------------------------------------------------------------------+
void CElement::ShowByParent(void)
{
   m_hidden_parent = false;
   ShowObject();
}

//+------------------------------------------------------------------+
//| Show child objects recursively (not exposed)                     |
//+------------------------------------------------------------------+
void CElement::ShowChildren(void)
{
   for (int i = 0; i < m_child_count; i++)
      m_children[i].ShowByParent();
}

//+------------------------------------------------------------------+
//| Change visibility property to hide (not exposed)                                                                  |
//+------------------------------------------------------------------+
void CElement::HideObject(void)
{
   ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   HideChildren();
}

//+------------------------------------------------------------------+
//| Hide element when parent is hidden (not exposed)                 |
//+------------------------------------------------------------------+
void CElement::HideByParent(void)
{
   m_hidden_parent = true;
   if (m_hidden)
      return;

   HideObject();
}

//+------------------------------------------------------------------+
//| Hide child objects recursively (not exposed)                     |
//+------------------------------------------------------------------+
void CElement::HideChildren(void)
{
   for (int i = 0; i < m_child_count; i++)
      m_children[i].HideByParent();
}

É importante fazer a distinção entre um objeto que está escondido e um objeto que é escondido por seu pai. Se não fizéssemos isso, objetos escondidos por padrão seriam exibidos quando seu elemento pai estivesse escondido, e então mostrado (já que essas funções são aplicadas recursivamente), ou você poderia mostrar objetos com um elemento pai escondido (por exemplo, botões sem uma janela atrás).

Nesse caso, Show (mostrar) e Hide (esconder) são as únicas funções visíveis externamente que permitem alterar a visibilidade de um objeto. Principalmente, as funções são usadas para alterar os flags de visibilidade e chamar ObjectSetProperty quando necessário. Além disso, os flags de objetos filhos são alterados recursivamente. Existem outras verificações que permitem evitar chamadas de função desnecessárias (por exemplo, esconder um objeto filho quando ele já está escondido). Finalmente, vale destacar que a redefinição do gráfico é necessária para exibir as alterações de visibilidade, portanto, em ambos os casos, chamamos RequestRedraw.

Também precisamos ocultar os objetos ao criá-los, já que teoricamente eles podem ser marcados como ocultos antes mesmo de serem criados:

void CElement::CreateChildren(void)
{
   for (int i = 0; i < m_child_count; i++)
   {
      m_children[i].Create();
      m_children[i].CreateChildren();
   }

   if (IsHidden()) HideObject();
}

Com todos esses componentes, podemos criar uma pequena demonstração para testar as funções de ocultar e exibir. Vamos aproveitar a classe personalizada da última parte (objetos arrastáveis) e criar uma nova classe a partir dela. Nossa nova classe responderá aos eventos de arrastar anteriores, bem como aos eventos de teclado, para alternar seu estado de visibilidade:

class CHideElement : public CDragElement
{
private:
   int               key_id;

protected:
   virtual void      DrawCanvas();

protected:
   virtual bool      OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam);

public:
   CHideElement(int id);
};

//+------------------------------------------------------------------+
//| Hide element constructor (set keyboard ID)                       |
//+------------------------------------------------------------------+
CHideElement::CHideElement(int id) : key_id(id)
{
}

//+------------------------------------------------------------------+
//| Hide element when its key is pressed (inherit drag events)       |
//+------------------------------------------------------------------+
bool CHideElement::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        
   bool drag = CDragElement::OnEvent(id, lparam, dparam, sparam);

   if (id != CHARTEVENT_KEYDOWN)
      return drag;

   if (lparam == '0' + key_id) //Toggle hide/show
   {
      if (IsHidden())
         Show();
      else
         Hide();
   }

   return true;
}

//+------------------------------------------------------------------+
//| Draw canvas function (fill with color and display number ID)     |
//+------------------------------------------------------------------+
void CHideElement::DrawCanvas(void)
{
   m_canvas.Erase(ARGB(255, MathRand() % 256, MathRand() % 256, MathRand() % 256));
   m_canvas.FontSet("Arial", 50);
   m_canvas.TextOut(25, 25, IntegerToString(key_id), ColorToARGB(clrWhite));
   m_canvas.Update(false);
}

Ao criar os objetos, lhes daremos um ID único (de 0 a 9), para alternar sua visibilidade ao pressionar essa tecla. Além disso, para simplificar a tarefa, também vamos exibir esse ID nos próprios objetos. Os eventos de arrastar também são chamados primeiro (se isso não for feito, eles serão completamente sobrescritos).

int OnInit()
{
   MathSrand((uint)TimeLocal());

   //100 is element size by default
   int max_x = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS) - 100;
   int max_y = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS) - 100;

   for (int i = 0; i < 10; i++)
   {
      CHideElement* drawing = new CHideElement(i);
      drawing.SetPosition(MathRand() % max_x, MathRand() % max_y);
      program.AddMainElement(drawing);
   }

   program.CreateGUI();
   ChartRedraw(0);

   return(INIT_SUCCEEDED);
}

Agora, se executarmos o programa, poderemos verificar que os objetos são corretamente ocultados e exibidos ao pressionar o número correspondente no teclado.

Alternância da visibilidade dos objetos


Ordem Z (e sua alteração)

Ordem Z (Z Order) refere-se à sequência na qual os objetos são exibidos. Simplificando, as coordenadas X e Y determinam a posição do objeto na tela, enquanto a coordenada Z determina sua profundidade ou ordem de colocação. Objetos com valores Z mais baixos são exibidos acima dos objetos com valores mais altos.

Você provavelmente já sabe que, no MetaTrader 5, não é possível alterar a ordem Z à vontade, já que uma propriedade com esse nome é usada para determinar qual objeto recebe eventos de clique quando estão sobrepostos (mas isso não tem nada a ver com a ordem Z visual), e também não precisamos verificar eventos de cliques, como mencionado anteriormente (pelo menos, nesta implementação). No MetaTrader 5, objetos recém-criados são sempre colocados no topo (a menos que sejam destinados ao fundo).

Porém, se brincarmos um pouco com o último exemplo, notaremos algo interessante...

Alteração da ordem Z na exibição

Se ocultarmos e mostrarmos um objeto, ele reaparecerá acima de todos os outros! Isso significa que podemos instantaneamente ocultar e mostrar um objeto, e ele será exibido acima de todos os outros? Sim!

Para verificar isso, precisaremos apenas modificar um pouco nossa última classe derivada (CHideElement), para que, em vez de alternar a visibilidade a cada entrada do teclado, ela aumente a ordem Z desse objeto específico. Também mudaremos o nome da classe...

bool CRaiseElement::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{

   bool drag = CDragElement::OnEvent(id, lparam, dparam, sparam);

   if (id != CHARTEVENT_KEYDOWN)
      return drag;

   if (lparam == '0' + key_id)
   {
      ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
      ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS);
      if (CheckPointer(m_program) != POINTER_INVALID)
         m_program.RequestRedraw();
   }

   return true;
}

Como sempre, não podemos esquecer que, após a alteração da ordem Z, é necessária uma redefinição. Se executarmos o teste, veremos o seguinte:

Alteração da ordem Z

Como podem ver, podemos facilmente elevar objetos conforme desejarmos. Não implementaremos essa função específica na biblioteca para não complicar os códigos no artigo (além disso, esse efeito pode ser obtido chamando Hide, e logo após, chamando Show). Ademais, com as ordens Z, podemos fazer outras coisas. E se quisermos elevar um objeto apenas por um nível? Ou queremos reduzir sua ordem Z ao mínimo? Em todos os casos, a solução envolve chamar o elevação da ordem Z no número necessário de objetos na ordem correta esperada (de baixo para cima). No primeiro caso, você chamaria a função para o objeto que deseja elevar por um nível e, em seguida, para todos os objetos acima dele, na ordem de classificação. No segundo caso, você faria isso, mas para todos os objetos (mesmo que pudesse ignorar aquele que atinge a menor ordem Z).

No entanto, há um truque ao implementar sistemas de ordem Z: você não pode mostrar um objeto que está oculto e alterar sua ordem Z no mesmo quadro. Suponha que você mostre um botão dentro de uma janela, mas ao mesmo tempo, você quer elevar essa janela: se você chamar Show para o botão (definindo OBJPROP_TIMEFRAMES para todos os períodos para esse botão) e depois disso eleva o Z para a janela (que define para OBJPROP_TIMEFRAMES o valor nenhum período, e depois todos os períodos para a janela, e então para todos os objetos na janela na ordem correta), então o botão permanecerá atrás da janela. Aparentemente, a razão disso é que apenas as primeiras alterações na propriedade OBJPROP_TIMEFRAMES são efetivas, então o objeto do botão é elevado apenas na sua exibição (e não na subsequente elevação de Z).

Uma das soluções para esse problema pode ser manter uma fila de objetos e verificar as alterações de visibilidade ou ordem Z. Então, todos eles seriam executados apenas uma vez por quadro. Assim, você também precisaria modificar a função Show para "marcar" esse objeto, ao invés de mostrá-lo diretamente. Se você está apenas começando, eu recomendaria não se preocupar demais com isso, já que tais situações são raras e não críticas (embora você deva evitar situações em que esse problema possa surgir no estágio atual).


Conclusão

Neste artigo, exploramos várias funções-chave necessárias para criar efetivamente uma biblioteca de interface gráfica e fornecemos exemplos pequenos para confirmar cada ponto. O artigo oferece uma visão fundamental de como tudo funciona dentro de uma biblioteca de interface gráfica como um todo. A biblioteca resultante de forma alguma é totalmente funcional, mas sim o mínimo necessário para demonstrar algumas funções usadas em muitas outras bibliotecas semelhantes.

Embora o código obtido seja relativamente simples, vale ressaltar que as bibliotecas de interface gráfica podem (e irão) se tornar muito mais complexas assim que você começar a adicionar mais funcionalidades ou criar tipos de objetos prontos (especialmente se eles tiverem subobjetos com eventos inter-relacionados). Além disso, a estrutura de outras bibliotecas pode variar da descrita aqui, dependendo das decisões de design ou performance/funcionalidade.

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

Arquivos anexados |
GUIArticle.zip (91.13 KB)
Criando um Expert Advisor simples multimoeda usando MQL5 (Parte 2): Sinais do indicador - Parabolic SAR multiframe Criando um Expert Advisor simples multimoeda usando MQL5 (Parte 2): Sinais do indicador - Parabolic SAR multiframe
Neste artigo, por EA multimoeda, entendemos um robô investidor ou um robô de negociação que pode negociar (abrir/fechar ordens, gerenciar ordens como trailing-stop-loss e trailing profit) mais de um par de moedas em um gráfico. Desta vez, usaremos apenas um indicador, o Parabolic SAR ou iSAR, em vários timeframes, começando com PERIOD_M15 e terminando com PERIOD_D1.
Teoria das Categorias em MQL5 (Parte 22): Outra Perspectiva sobre Médias Móveis Teoria das Categorias em MQL5 (Parte 22): Outra Perspectiva sobre Médias Móveis
Neste artigo, tentaremos simplificar a descrição dos conceitos discutidos nesta série, focando apenas em um indicador, o mais comum e, provavelmente, o mais fácil de entender. Estamos falando da média móvel. Também examinaremos o significado e as possíveis aplicações das transformações naturais verticais.
Redes neurais de maneira fácil (Parte 58): transformador de decisões (Decision Transformer — DT) Redes neurais de maneira fácil (Parte 58): transformador de decisões (Decision Transformer — DT)
Continuamos a explorar os métodos de aprendizado por reforço. Neste artigo, proponho apresentar um algoritmo ligeiramente diferente que considera a política do agente sob a perspectiva de construir uma sequência de ações.
Rede neural na prática: Reta Secante Rede neural na prática: Reta Secante
Como foi explicado na parte teórica. Precisamos usar regressões lineares e derivadas, quando o assunto é rede neural. Mas por que ?!?! O motivo disto, é que a regressão linear é uma das fórmulas mais simples que existe. Basicamente uma regressão linear, é apenas uma função afim. Porém quando falamos em rede neural, não estamos interessados na reta, que a regressão linear cria. Estamos interessados é na equação que gera tal reta. A reta gerada pouco importa. Mas você sabe qual é a equação principal a ser compreendida ?!?! Se não veja este artigo para começar a entender.