English Русский 中文 Deutsch 日本語 Português
preview
Interfaz gráfica: consejos y recomendaciones para crear una biblioteca gráfica en MQL

Interfaz gráfica: consejos y recomendaciones para crear una biblioteca gráfica en MQL

MetaTrader 5Ejemplos | 7 marzo 2024, 15:02
205 0
Manuel Alejandro Cercos Perez
Manuel Alejandro Cercos Perez

Introducción

El desarrollo de la biblioteca GUI es uno de los mayores proyectos no específicos en el contexto de MetaTrader 5, junto con cosas tan avanzadas como la inteligencia artificial, las redes neuronales eficientes y... la capacidad de usar libremente la biblioteca GUI desarrollada.

Bueno, he bromeado un poco con el último punto. ¡Obviamente, resulta más fácil aprender a usar una biblioteca ya preparada (aunque hay bibliotecas GUI realmente grandes)! Pero, si podemos aprender a utilizar una biblioteca que ya sea mejor que una que podría crear yo mismo, ¿por qué crearla desde cero?

Existen varias buenas razones. La biblioteca puede resultar demasiado lenta para nuestro proyecto en particular, es posible que necesitemos ampliarla (algunas bibliotecas son más difíciles de ampliar que otras) o podría tener un error (aparte de los que resultan del mal uso de la biblioteca). O tal vez simplemente queramos aprender a escribir bibliotecas por nosotros mismos. La mayoría de estos problemas pueden ser resueltos por los autores de cualquier biblioteca concreta, pero tendrá que confiar en ellos.

El propósito de este artículo no consiste en mostrar cómo crear una interfaz ni mostrarle los pasos para desarrollar una biblioteca completamente funcional. En su lugar, mostraremos ejemplos sobre cómo construir algunas partes específicas de las bibliotecas GUI para que puedan servir como punto de partida para resolver un problema específico, así como para obtener una comprensión inicial de lo que sucede dentro del enorme código de una biblioteca GUI lista para usar.


Estructura del programa y jerarquía de los objetos.

¿Qué es una biblioteca GUI? En pocas palabras, es una jerarquía de objetos que rastrean otros objetos (gráficos) y cambian sus propiedades para crear diferentes efectos y generar eventos como el desplazamiento, el clic o el cambio de color. La organización de esta jerarquía puede variar, pero la más común (y la más aceptable para mí, personalmente) es una estructura de árbol de elementos donde un elemento puede tener subelementos.

Comenzaremos con la implementación básica del 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(){}
};

Actualmente, la clase básica de un elemento solo contiene información sobre la posición, el tamaño y las relaciones con otros elementos.

Las variables posicionalesm_x y m_y son posiciones locales en el contexto del objeto principal. Esto crea la necesidad de una función de posición global que permita determinar la posición real del objeto en la pantalla. A continuación se puede ver cómo podemos obtener recursivamente la posición global (en este caso para X):

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

   return m_x + m_parent.GetGlobalX();
}

En el constructor, necesitaremos definirun nombre único para cada objeto. Para esto podemos usar una variable estática. Por ciertas razones, prefiero mantener esta variable en la clase de programa mostrada a continuación, pero para simplificar, la pondremos dentro de la clase de elemento.

¡No se olvide de eliminar los elementos hijos del destructor para evitar fugas de memoria!

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, definiremos las funciones de relación AddChild y SetParent, ya que necesitaremos ambas referencias para la comunicación entre los elementos: por ejemplo, para obtener la posición global, el hijo deberá conocer la posición del padre, pero cuando la posición cambie, el padre deberá notificar sobre ello a los hijos (esta última parte la implementaremos más adelante). Para evitar duplicaciones, hemos marcado AddChild como privado.

En las funciones de creación crearemos los propios objetos del gráfico (y cambiaremos otras propiedades). Debemos asegurarnos de que los elementos secundarios se creen después del padre, por lo que utilizaremos una función aparte para este propósito (ya que la función de creación se puede redefinir y esto puede cambiar el orden de ejecución). En la clase del elemento básico, create estará vacío.

//+------------------------------------------------------------------+
//| 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();
   }
}

Crearemos la clase program. Por ahora supone solo un marcador de posición para interactuar indirectamente con un elemento vacío usado como soporte, pero en el futuro realizará otras operaciones centralizadas que afectarán a todos los elementos (y que no desearemos hacer varias veces). Usar un contenedor de elementos vacío para contener otros elementos nos permitirá evitar reelaborar cualquier función que requiera la iteración recursiva a través de los elementos secundarios. Por ahora, no necesitaremos un constructor/destructor para esta clase ya que no almacenaremos el placeholder como un puntero.

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 realizar nuestra primera prueba, todavía necesitaremos ampliar la clase Element, ya que ahora está vacía. Esto nos permitirá gestionar distintos tipos de objeto de forma diferente. Primero, crearemos un elemento de lienzo (canvas) usando CCanvas (que en realidad es una etiqueta gráfica). Los objetos de lienzo son los objetos más versátiles para crear GUI personalizadas, y podremos crear casi cualquier cosa completamente a partir de un lienzo:

#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);
}

Añadiremos estas dos líneas al final de Create para rellenar el lienzo con un color aleatorio. Una forma más correcta sería ampliar la clase canvas para crear este tipo específico de objeto, pero por ahora no será necesario complicar el código.

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

También crearemos una clase de objeto de edición (edit). ¿Pero por qué esta? ¿Por qué no hacemos nuestros propios cambios dibujando directamente en el lienzo y monitoreando los eventos del teclado? Hay una cosa que no se puede hacer con el lienzo. Hablamos de copiar y pegar textos (al menos fuera de la aplicación) sin una DLL. Si no necesitamos esta funcionalidad para nuestra biblioteca, podremos añadir el lienzo directamente a la clase Element y usarlo para cada tipo de objeto. Como habrá notado, algunas cosas se hacen de forma diferente en relación con el lienzo...

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

En este caso, necesitaremos establecer explícitamente las propiedades de posición y tamaño (en Canvas, esto se hace dentro de CreateBitmapLabel).

Con todos estos cambios por fin podremos ejecutar la primera prueba:

#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);
}

El programa creará varios cuadrados y los eliminará cuando se eliminen del gráfico. Tenga en cuenta que cada elemento edit se colocará en la misma ubicación en relación con su elemento principal.

De momento estos cuadrados no harán nada. En las siguientes secciones hablaremos sobre cómo podremos añadirles este o aquel comportamiento.


Movimiento del ratón

Si alguna vez ha trabajado con eventos de gráficos, sabrá que usar eventos de pulsación no será suficiente para crear un comportamiento complejo. El truco para crear mejores interfaces será utilizar eventos de desplazamiento del ratón. Dichos eventos deberán activarse al iniciarse el asesor, por lo que los activaremos antes de crear la GUI:

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

Estos definirán la posición del ratón (x,y) y el estado de los botones del ratón, Ctrl y Shift. Cada vez que al menos uno de los estados o posiciones cambie, se activará un evento.

Primero, definiremos las etapas por las que puede pasar un botón:inactivo cuando no está en uso y activo cuando se presiona. También añadiremos down y up, que representarán los primeros estados activo e inactivo respectivamente (modificando el estado de la pulsación). Como podemos detectar cada fase usando solo los eventos del ratón, ni siquiera necesitaremos usar eventos de pulsación.

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

Para facilitar las cosas, centralizaremos el procesamiento de eventos del ratón en lugar de implementarlo dentro de cada objeto. De esta manera podremos acceder más fácilmente a los datos del ratón y rastrear estos, logrando usarlos incluso para otro tipo de eventos y evitando cálculos repetitivos. Llamaremos a esta clase CInputs porque, además de los datos de entrada con el ratón, también incluye los botones Ctrl y Shift.

//+------------------------------------------------------------------+
//| 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;
}
//+------------------------------------------------------------------+

En UpdateState, verificaremos el estado actual (valor booleano) y el último estado para determinar si la entrada está activa/inactiva y si este es el primer evento después de un cambio de estado (up/down). Luego obtendremos información sobre Ctrl y Shift "gratis" en Sparam. También obtendremos información sobre los botones central y derecho del ratón, así como sobre dos botones adicionales del mismo. No los hemos añadido al código, pero podrá realizar los cambios correspondientes, si lo considera necesario.

Después añadiremos un ejemplar de los datos de entrada del ratón al programa y lo haremos disponible para cada objeto usando un puntero. Con cada evento, el ejemplar de los datos de entrada se actualizará. El tipo de evento se filtrará dentro de la clase de datos de entrada (solo se utilizarán los eventos de movimiento del ratón).

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

En la siguiente sección ofreceremos una explicación más detallada de cómo se utilizará OnChartEvent.

Al configurar los datos de entrada para un elemento global, estos se transmitirán recursivamente a sus hijos. Sin embargo, no hay que olvidar que deberán pasarse al añadir elementos secundarios más adelante (tras la primera llamada a 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);
}

En la siguiente sección, crearemos funciones para la gestión de eventos y añadiremos algunos eventos de ratón a cada objeto.


Gestión de eventos

Antes de gestionar los eventos en sí, deberemos darnos cuenta de una cosa que necesitaremos si queremos tener un movimiento fluido en la interfaz: el redibujo o redibujado del gráfico. Cada vez que cambia una propiedad de un objeto, como su posición, o si se vuelve a dibujar, deberemos redibujar el gráfico para que estos cambios se reflejen instantáneamente. Sin embargo, llamar a ChartRedraw con demasiada frecuencia puede hacer que la GUI parpadee, por lo que resulta preferible centralizar su ejecución:

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;
   }
}

Deberemos llamar a OnTimer en la función del manejador de eventos del mismo nombre, y cada elemento deberá tener una referencia al programa para poder llamar a RequestRedraw. Esta función establecerá una bandera que, al activarse, redibujará todos los elementos la próxima vez que se llame al temporizador. También necesitaremos configurar el temporizador primero:

#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 milisegundos será el intervalo aproximado dentro del cual los temporizadores podrán ejecutarse de manera fiable. Sin embargo, los programas pesados ​​podrían bloquear la ejecución del temporizador.

A continuación le mostramos cómo se implementan los eventos del gráfico en 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);
}

En esta biblioteca, hemos decido transmitir los eventos de forma recursiva de padres a hijos. Esta es una elección de diseño (se puede hacer de otras formas, como usando oyentes o llamando a todos los objetos secuencialmente), pero como veremos más adelante, posee algunas ventajas. OnEvent es una función protegida anulada. Esta se encuentra separada de OnChartEvent (que es pública), por lo que el usuario no podrá redefinir la transmisión de eventos a los objetos hijo y elegir cómo se ejecuta la clase principal OnEvent (antes, después o no ejecutarla).

Como ejemplo de manejo de eventos , implementaremos la función de arrastre de cuadrados. Los eventos de pulsación y desplazamiento también se podrán implementar de una forma más trivial, pero no los necesitaremos en este ejemplo. Por el momento, los eventos solo transmitirán a través de objetos, pero no harán nada. No obstante, existe un problema en el estado actual: si intentamos arrastrar un objeto, el gráfico detrás de él se moverá como si el arrastre nunca hubiera ocurrido. ¿Cómo haremos entonces que los gráficos permanezcan en su lugar?

Objetos que no reaccionan a ningún evento

Para evitar este problema, primero verificaremos si el ratón está sobre un objeto. En caso afirmativo, bloquearemos el desplazamiento del gráfico. Por razones de rendimiento, solo comprobaremos los objetos que sean hijos directos del placeholder global (el programa siempre se ejecutará si otros subobjetos permanecen dentro del objeto principal).

//+------------------------------------------------------------------+
//| 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);
}

Ahora estos evitan los desplazamientos... pero solo cuando el puntero del ratón está sobre el objeto. Si lo arrastramos afuera, los gráficos aún se moverán.

Tendremos que añadir dos comprobaciones más y una variable booleana privada a 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;
}

Ahora al arrastrar objetos todo funcionará bien, pero falta una pequeña corrección... al arrastrar los gráficos. Si el ratón pasa sobre un objeto, el arrastre se detendrá. Necesitaremos filtrar los arrastres que comienzan fuera del objeto.

Los objetos detienen el arrastre de los gráficos

Afortunadamente, solucionar este comportamiento es algo sencillo:

//+------------------------------------------------------------------+
//| 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;
}

Ahora, si el ratón está sobre un objeto y está activo, el arrastre del gráfico no terminará (al arrastrar un objeto retornará true). La verificación del movimiento del ratón (mouseover) seguirá siendo necesaria porque si no estuviera allí, los eventos se desactivarían un cuadro más tarde.

Filtrado correcto de arrastres

Ahora que todo está listo, podremos añadir la función de arrastre a la clase de rectángulo. Sin embargo, en lugar de añadirla a CCanvasElement, ampliaremos la clase usando la herencia. También extraeremos el dibujo lineal del último ejemplo para que esté vacío por defecto. Como ya hemos añadido la comprobación del arrastre, podremos usarla para gestionar los eventos y desplazamientos de los objetos. Para cambiar la posición de un objeto en el gráfico, necesitaremos cambiar sus variables, luego actualizar sus propiedades de posición, su posición secundaria y redibujar el 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);
}

Tenga en cuenta que necesitaremos establecer la posición global del objeto: los objetos gráficos no saben nada sobre la jerarquía dentro de ellos. Si intentamos mover el objeto ahora, se bloqueará y también actualizará la posición de sus hijos, aunque podremos mover los objetos detrás de él:

Varios objetos moviéndose al mismo tiempo

Como cada evento se envía de forma recursiva a todos los objetos, estos lo recibirán incluso si están ubicados detrás de otro objeto. Necesitamos encontrar una manera de filtrar eventos si un objeto los recibe primero; en otras palabras, necesitaremos superponerlos .

Así, crearemos una variable booleana para monitorear si el objeto está cubierto:

class CElement
{
private:

   //...
   bool              m_occluded;

   //...

public:

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

Luego podremos usar OnChartEvent como una forma de "comunicación" entre objetos. Para ello retornaremos un valor booleano. Si el objeto recibe el evento, será true. Si el objeto ha sido arrastrado (incluso si el objeto no ha reaccionado al arrastre) o si el objeto está cubierto (por ejemplo, por un objeto secundario), también se retornará true, ya que esto también bloqueará los eventos para los objetos debajo de él.

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 en un objeto se ejecutará después de los eventos de sus hijos para tener en cuenta las superposiciones. Finalmente, necesitaremos añadir el filtrado de eventos a nuestro objeto arrastrable 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;

   //...
}

Tenga en cuenta que no filtraremos eventos cuando estos se envían. El motivo es que algunos elementos pueden responder a eventos incluso si están cerrados (por ejemplo, algunos eventos que reaccionan al pasar el ratón o simplemente a la posición del ratón).

Con todos estos elementos en su lugar, hemos logrado el comportamiento deseado en el segundo ejemplo; cada objeto puede ser arrastrado mientras está bloqueado por los demás objetos de arriba:

Los objetos responden correctamente al arrastre

No obstante, cabe señalar que aunque esta estructura funciona para este caso de uso, otras interfaces más complejas pueden requerir algo de trabajo. Dejaremos esta tarea fuera del presente artículo, ya que aumentará la complejidad del código no estará justificado en este caso particular. Por ejemplo, podemos diferenciar las superposiciones de los objetos secundarios y los objetos hermanos (aquellos que están en el mismo nivel de la jerarquía pero que aparecen primero). O podría resultar útil monitorear qué objeto ha recibido el evento y comprobarlo primero en el siguiente fotograma (para evitar superposiciones inesperadas).


Mostrar y ocultar objetos

Otra característica importante necesaria en cualquier biblioteca de gráficos es la capacidad de ocultar y mostrar objetos: por ejemplo, al abrir y cerrar ventanas, cambiar su contenido usando las pestañas del navegador o eliminar botones que no están disponibles bajo ciertas condiciones.

Una forma ingenua de lograr esto sería eliminar y crear objetos cada vez que deseemos ocultarlos o mostrarlos, o evitar esta característica en general. Sin embargo, existe una forma de ocultar objetos usando sus propiedades nativas (aunque no resulte obvio por el nombre):

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

Esta propiedad determinará "en qué marcos temporales se muestra el objeto y en cuáles no". Solo podremos mostrar objetos en algunos marcos temporales, pero resulta poco probable que lo necesitemos aquí.

Considerando las llamadas a la función ObjectSetInteger que hemos descrito antes, podremos implementar una función para mostrar y ocultar objetos en nuestra jerarquía de objetos GUI:

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();
}

Debemos distinguir entre un objeto oculto y un objeto oculto por su objeto padre. Si no hiciéramos esto, los objetos ocultos de forma predeterminada se mostrarían cuando su padre esté oculto y luego se mostrarían (ya que estas funciones se aplican de forma recursiva), o podría mostrar objetos con su padre oculto (por ejemplo, botones sin ventana detrás).

En este caso Show (mostrar) y Hide (ocultar) son las únicas funciones visibles externamente que permiten cambiar la visibilidad de un objeto. Las funciones se utilizan principalmente para cambiar las banderas de visibilidad y llamar a ObjectSetProperty cuando sea necesario. Además, las banderas de los objetos secundarios se cambiarán de forma recursiva. Existen otras comprobaciones que evitan llamadas a funciones innecesarias (como ocultar un objeto secundario cuando ya está oculto). Finalmente, cabe señalar que deberemos redibujar el gráfico para reflejar los cambios de visibilidad, por lo que en ambos casos llamaremos a RequestRedraw.

También deberemos ocultar los objetos al crearlos, ya que, en teoría, podrían marcarse como ocultos antes de su creación:

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

   if (IsHidden()) HideObject();
}

Con todos estos componentes implementados, podremos crear una pequeña demostración para probar la funcionalidad que permite ocultar y mostrar. Aprovecharemos la clase personalizada de la última parte (objetos arrastrables) y crearemos una nueva clase a partir de ella. Nuestra nueva clase reaccionará a los eventos de arrastre anteriores, así como a los eventos de teclado para alternar su estado oculto:

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

Al crear objetos, les daremos una identificación única (de 0 a 9) para alternar su visibilidad cuando se presione esta tecla. Además, para facilitar las cosas, también mostraremos este ID en los propios objetos. Los eventos de arrastre también se activarán primero (si no, se redefinirán por completo).

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

Ahora bien, si ejecutamos el programa podremos comprobar que los objetos se ocultan y se muestran correctamente cuando pulsamos el número correspondiente en el teclado.

Cambiar la visibilidad del objeto


El orden Z (y su cambio)

El orden Z se refiere al orden en que se muestran los objetos. En pocas palabras, las coordenadas X e Y determinarán la posición de un objeto en la pantalla, mientras que la coordenada Z determinará su profundidad u orden. Los objetos con valores Z más bajos aparecerán encima de los objetos con valores Z más altos.

Quizás ya sepa que en MetaTrader 5 no es posible cambiar el orden Z a voluntad, ya que la propiedad con ese nombre se utiliza para determinar qué objeto recibe eventos de pulsación cuando estos se encuentran uno encima del otro (pero esto no tiene nada que ver con el orden Z visual) y tampoco necesitamos verificar los eventos de pulsación como hemos indicado antes (al menos en esta implementación). En MetaTrader 5, los objetos recién creados siempre se colocan en la parte superior (a menos que estén destinados al fondo).

No obstante, si jugamos con el último ejemplo, notaremos algo interesante...

Cambiar el orden Z al mostrar

Si ocultamos y mostramos un objeto, ¡volverá a aparecer sobre todos los demás! ¿Significa esto que podemos ocultar y mostrar instantáneamente un objeto y aparecerá por encima de todos los demás? ¡Sí!

Para probar esto, solo necesitaremos modificar ligeramente nuestra última clase derivada (CHideElement) para que, en lugar de alternar la visibilidad con cada entrada del teclado, aumente el orden Z de ese objeto en concreto. También cambiaremos el nombre de la clase...

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 siempre, no deberemos olvidar que después de cambiar el orden Z, deberemos redibujar. Si ejecutamos la prueba, veremos esto:

Cambiando el orden Z

Como podemos ver, resulta posible subir objetos fácilmente a nuestro antojo. Para mantener simple el código del artículo, no implementaremos esta función particular en la biblioteca (además, este efecto se puede lograr llamando a Hide y luego llamando a Show inmediatamente después). Además, podemos hacer otras cosas con los órdenes Z. ¿Qué pasa si queremos elevar un objeto solo un nivel? ¿O queremos reducir su orden Z al mínimo? En todos los casos, la solución será llamar el orden Z en la cantidad requerida de objetos en el orden correcto esperado (de abajo hacia arriba). En el primer caso, deberemos llamar a la función en el objeto que deseamos subir un nivel, y luego en todos los objetos por encima de él, en el orden en que están clasificados. En el segundo caso, haríamos esto, pero para todos los objetos (aunque podríamos ignorar el que desciende al orden Z inferior).

Sin embargo, existe un problema al implementar sistemas de orden Z: no es posible mostrar un objeto que está oculto y cambiar su orden Z en el mismo cuadro. Supongamos que estamos mostrando un botón dentro de una ventana, pero al mismo tiempo deseamos abrir esa ventana: si llamamos a Show para el botón (que establece OBJPROP_TIMEFRAMES en all periods para ese botón) y luego subimos Z en la ventana (que establece para OBJPROP_TIMEFRAMES el valor no periods, y luego all periods para la ventana, y después para todos los objetos en la ventana en el orden correcto), entonces el botón permanecerá detrás de la ventana. La razón de esto parece ser que solo los primeros cambios en la propiedad OBJPROP_TIMEFRAMES tienen efecto, por lo que el objeto de botón solo se generará al mostrarse este (y no la próxima vez que se suba Z).

Una solución a este problema sería mantener una cola de objetos y comprobar si hay cambios en la visibilidad o el orden Z. Luego, todos se ejecutarían solo una vez por cuadro. Por lo tanto, también tendríamos que cambiar la función Show para "etiquetar" este objeto en lugar de mostrarlo directamente. Si usted acaba de comenzar, le recomiendo que no se preocupe demasiado por esto, ya que este tipo de situaciones son raras y no tienen una importancia vital (aunque deberá evitar situaciones en las que este problema pueda surgir en esta etapa).


Conclusión

En este artículo, hemos abarcado varias características clave que necesitamos conocer para crear una biblioteca GUI de manera efectiva, y también hemos ofrecido pequeños ejemplos para respaldar cada punto. El artículo ofrece una comprensión fundamental de cómo funcionan las cosas dentro de la biblioteca GUI en su conjunto. La biblioteca obtenida no resulta funcional en absoluto, sino más bien el mínimo necesario para demostrar algunas de las funciones utilizadas en muchas otras bibliotecas similares.

Si bien el código resultante es relativamente simple, cabe señalar que las bibliotecas GUI pueden volverse mucho más complejas (y de hecho lo harán) una vez que comencemos a añadir más funciones o a crear tipos de objetos prediseñados (especialmente si tienen subobjetos con eventos relacionados). Además, la estructura de otras bibliotecas podría tener algunas diferencias respecto a la descrita aquí, dependiendo de las soluciones de diseño o rendimiento/funcionalidad.

Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/13169

Archivos adjuntos |
GUIArticle.zip (91.13 KB)
Creamos un asesor multidivisa sencillo utilizando MQL5 (Parte 2): Señales del indicador - Parabolic SAR de marco temporal múltiple Creamos un asesor multidivisa sencillo utilizando MQL5 (Parte 2): Señales del indicador - Parabolic SAR de marco temporal múltiple
En este artículo, entenderemos por asesor multidivisa un asesor o robot comercial que puede comerciar (abrir/cerrar órdenes, gestionar órdenes, por ejemplo, trailing-stop y trailing-profit, etc.) con más de un par de símbolos de un gráfico. Esta vez usaremos solo un indicador, a saber, Parabolic SAR o iSAR en varios marcos temporales, comenzando desde PERIOD_M15 y terminando con PERIOD_D1.
Desarrollo de un sistema de repetición (Parte 32): Sistema de órdenes (I) Desarrollo de un sistema de repetición (Parte 32): Sistema de órdenes (I)
De todas las cosas desarrolladas hasta ahora, esta, como seguramente también notarás y con el tiempo estarás de acuerdo, es la más desafiante de todas. Lo que tenemos que hacer es algo simple: hacer que nuestro sistema simule lo que hace un servidor comercial en la práctica. Esto de tener que implementar una forma de simular exactamente lo que haría el servidor comercial parece simple. Al menos en palabras. Pero necesitamos hacer esto de manera que, para el usuario del sistema de repetición/simulación, todo suceda de la manera más invisible o transparente posible.
Teoría de Categorías en MQL5 (Parte 23): Otra mirada a la media móvil exponencial doble Teoría de Categorías en MQL5 (Parte 23): Otra mirada a la media móvil exponencial doble
En este artículo, seguiremos analizando desde un nuevo ángulo los indicadores comerciales más populares. Vamos a procesar una composición horizontal de transformaciones naturales. El mejor indicador para ello será la media móvil exponencial doble (Double Exponential Moving Average, DEMA).
Desarrollo de un sistema de repetición (Parte 31): Proyecto Expert Advisor — Clase C_Mouse (V) Desarrollo de un sistema de repetición (Parte 31): Proyecto Expert Advisor — Clase C_Mouse (V)
Desarrollar una manera de poner un cronómetro, de modo que durante una repetición/simulación, éste pueda decirnos cuánto tiempo falta, puede parecer a primera vista una tarea simple y de rápida solución. Muchos simplemente intentarían adaptar y usar el mismo sistema que se utiliza cuando tenemos el servidor comercial a nuestro lado. Pero aquí reside un punto que muchos quizás no consideran al pensar en tal solución. Cuando estás haciendo una repetición, y esto para no hablar del hecho de la simulación, el reloj no funciona de la misma manera. Este tipo de cosa hace complejo construir tal sistema.