English Русский Português
preview
Tablas en el paradigma MVC en MQL5: integramos el componente Model en el componente View

Tablas en el paradigma MVC en MQL5: integramos el componente Model en el componente View

MetaTrader 5Ejemplos |
34 2
Artyom Trishkin
Artyom Trishkin

Índice



Introducción

En el artículo «Clases de tabla y encabezado basadas en el modelo de tabla de MQL5: Aplicación del concepto MVC», hemos completado la creación de la clase del modelo de la tabla (en el concepto MVC, el componente Modelo). A continuación, nos dedicamos a desarrollar una biblioteca de controles simples que permiten crear, a partir de ellos, controles completamente distintos en cuanto a finalidad y complejidad. En concreto, el componente Vista para crear el control TableView.

Este artículo estará dedicado a la implementación de la interacción entre el componente Modelo y el componente Vista. En otras palabras: hoy vamos a unir los datos tabulares con su representación gráfica en un único control.

El control se creará a partir de la clase del objeto Panel y estará compuesto por varios elementos:

  1. Panel: el panel base o soporte al que se adjuntan el encabezado de la tabla y el área de datos de la tabla;
  2. Panel: el encabezado de la tabla, compuesto por una serie de elementos —encabezados de columnas— creados a partir de la clase del objeto Button;
  3. Container: contenedor de datos tabulares con posibilidad de desplazamiento del contenido;
  4. Panel: panel para colocar en él las filas de la tabla (adjunto al contenedor (punto 3) y, al salir de los límites del contenedor, se desplaza mediante las barras de desplazamiento del contenedor);
  5. Panel: fila de la tabla; panel para dibujar en él las celdas de la tabla, adjunto al panel (punto 4); se crean tantos objetos de este tipo como filas tenga la tabla;
  6. La clase de celda de la tabla es una nueva clase que permite dibujar en las coordenadas especificadas sobre el lienzo indicado (CCanvas). Un objeto de este tipo se adjunta a un objeto de fila de la tabla (punto 5); los lienzos del objeto de fila de la tabla se especifican como elementos de dibujo, y la celda de la tabla se dibuja en este panel en las coordenadas indicadas. El área de cada celda de la tabla se define en el objeto de fila de la tabla (punto 5) mediante una instancia de la clase CBound, y a este objeto se le fija un objeto de la clase de celda de la tabla.

Este algoritmo de construcción de filas y celdas de la tabla, en el que cada fila se distribuye en áreas horizontales a las que se fijan objetos de la clase de celdas, permite ordenar y cambiar fácilmente la posición de las celdas con solo reasignarlas a las áreas necesarias de la fila.

A la vista de lo anterior, entendemos que necesitamos una nueva clase de objeto a la que se le pase un puntero al control y en cuyo lienzo se realice el dibujo. Actualmente, todos los objetos de la biblioteca, al crearse mediante el operador new, crean sus propios objetos de lienzo (de fondo y de primer plano). Nosotros necesitamos un objeto que permita establecer y obtener su tamaño, así como asignarle un puntero al control en el que se realizará el dibujo.

La clase CBound permite establecer y obtener las dimensiones del objeto creado, y forma parte del objeto base de todos los controles. En este mismo objeto se crean también los lienzos del fondo y del primer plano. Por lo tanto, tenemos que crear otro objeto intermedio que herede del objeto base y que incluya la clase CBound para establecer y obtener las dimensiones. La clase en la que se crean los objetos CCanvas heredará de este objeto. De este modo, a partir de esta clase intermedia podremos derivar la clase a la que pasaremos el puntero al control sobre el que vamos a dibujar.

Vamos a modificar los archivos de las clases ya creadas y, después, escribiremos las nuevas.


Perfeccionamos las clases de la biblioteca

Todos los archivos de la biblioteca que se está desarrollando se encuentran en la ruta \MQL5\Indicators\Tables\Controls\. Si aún no los tienes, puedes obtener la versión anterior de todos los archivos en el artículo anterior. Además, para el proyecto se necesitará el archivo de clases de tablas (componente Model), que se puede obtener en el artículo, donde finalizamos su desarrollo. El archivo Tables.mqh debe guardarse en la carpeta \MQL5\Indicators\Tables\. Se modificarán los archivos Base.mqh y Controls.mqh.

Al principio del archivo Tables.mqh, en la sección de macros, escribiremos el identificador del archivo:

//+------------------------------------------------------------------+
//| Macros                                                           |
//+------------------------------------------------------------------+
#define  __TABLES__                 // Identificador de este archivo
#define  MARKER_START_DATA    -1    // Marcador de inicio de datos en el archivo
#define  MAX_STRING_LENGTH    128   // Longitud máxima de la cadena en la celda
#define  CELL_WIDTH_IN_CHARS  19    // Anchura de la celda en caracteres

Esto es necesario para que la macro MARKER_START_DATA pueda declararse en dos archivos de inclusión diferentes, de modo que puedan compilarse por separado.

Abramos el archivo Base.mqh. Incluyamos en él el archivo de clases del modelo de tabla y añadamos la declaración anticipada de las nuevas clases:

//+------------------------------------------------------------------+
//|                                                         Base.mqh |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd." 
#property link      "https://www.mql5.com"

//+------------------------------------------------------------------+
//| Bibliotecas incluidas                                            |
//+------------------------------------------------------------------+
#include <Canvas\Canvas.mqh>              // Clase SB CCanvas
#include <Arrays\List.mqh>                // Clase SB CList
#include "..\Tables.mqh"

//--- Declaración anticipada de las clases de los controles
class    CBoundedObj;                     // Clase base que almacena las dimensiones del objeto
class    CCanvasBase;                     // Clase base del lienzo de elementos gráficos
class    CCounter;                        // Clase del contador de retardo
class    CAutoRepeat;                     // Clase de repetición automática de eventos
class    CImagePainter;                   // Clase de dibujo de imágenes
class    CVisualHint;                     // Clase de ayuda emergente
class    CLabel;                          // Clase de etiqueta de texto
class    CButton;                         // Clase de botón simple
class    CButtonTriggered;                // Clase de botón de dos posiciones
class    CButtonArrowUp;                  // Clase de botón con flecha hacia arriba
class    CButtonArrowDown;                // Clase de botón con flecha hacia abajo
class    CButtonArrowLeft;                // Clase de botón con flecha hacia la izquierda
class    CButtonArrowRight;               // Clase de botón con flecha hacia la derecha
class    CCheckBox;                       // Clase del control CheckBox
class    CRadioButton;                    // Clase del control RadioButton
class    CScrollBarThumbH;                // Clase del control deslizante de la barra de desplazamiento horizontal
class    CScrollBarThumbV;                // Clase del control deslizante de la barra de desplazamiento vertical
class    CScrollBarH;                     // Clase de barra de desplazamiento horizontal
class    CScrollBarV;                     // Clase de barra de desplazamiento vertical
class    CTableRowView;                   // Clase de representación visual de una fila de la tabla
class    CPanel;                          // Clase del control Panel
class    CGroupBox;                       // Clase del control GroupBox
class    CContainer;                      // Clase del control Container

//+------------------------------------------------------------------+
//| Definiciones de macros                                           |
//+------------------------------------------------------------------+

En la sección de definiciones de macros, la declaración de la macro MARKER_START_DATA la envolveremos con una comprobación de su existencia:

//+------------------------------------------------------------------+
//| Definiciones de macros                                           |
//+------------------------------------------------------------------+
#define  clrNULL              0x00FFFFFF  // Color transparente para CCanvas
#ifndef  __TABLES__
#define  MARKER_START_DATA    -1          // Marcador de inicio de datos en el archivo
#endif 
#define  DEF_FONTNAME         "Calibri"   // Fuente por defecto
#define  DEF_FONTSIZE         10          // Tamaño de la fuente por defecto
#define  DEF_EDGE_THICKNESS   3           // Grosor de la zona de captura de límite/ángulo

Ahora, declarar la misma sustitución de macro en dos archivos distintos no provocará errores de compilación, ni al compilar cada archivo por separado ni al compilar ambos juntos.

En la enumeración de tipos de elementos gráficos añadiremos nuevos tipos y estableceremos un nuevo tipo máximo para el elemento activo:

//+------------------------------------------------------------------+
//| Enumeraciones                                                    |
//+------------------------------------------------------------------+
enum ENUM_ELEMENT_TYPE                    // Enumeración de tipos de elementos gráficos
  {
   ELEMENT_TYPE_BASE = 0x10000,           // Objeto básico de los elementos gráficos
   ELEMENT_TYPE_COLOR,                    // Objeto de color
   ELEMENT_TYPE_COLORS_ELEMENT,           // Objeto de colores de un elemento de objeto gráfico
   ELEMENT_TYPE_RECTANGLE_AREA,           // Área rectangular del elemento
   ELEMENT_TYPE_IMAGE_PAINTER,            // Objeto para dibujar imágenes
   ELEMENT_TYPE_COUNTER,                  // Objeto contador
   ELEMENT_TYPE_AUTOREPEAT_CONTROL,       // Objeto de repetición automática de eventos
   ELEMENT_TYPE_BOUNDED_BASE,             // Objeto básico de las dimensiones de los elementos gráficos
   ELEMENT_TYPE_CANVAS_BASE,              // Objeto básico del lienzo de los elementos gráficos
   ELEMENT_TYPE_ELEMENT_BASE,             // Objeto básico de los elementos gráficos
   ELEMENT_TYPE_HINT,                     // Ayuda emergente
   ELEMENT_TYPE_LABEL,                    // Etiqueta de texto
   ELEMENT_TYPE_BUTTON,                   // Botón simple
   ELEMENT_TYPE_BUTTON_TRIGGERED,         // Botón de dos posiciones
   ELEMENT_TYPE_BUTTON_ARROW_UP,          // Botón con flecha hacia arriba
   ELEMENT_TYPE_BUTTON_ARROW_DOWN,        // Botón con flecha hacia abajo
   ELEMENT_TYPE_BUTTON_ARROW_LEFT,        // Botón con flecha hacia la izquierda
   ELEMENT_TYPE_BUTTON_ARROW_RIGHT,       // Botón con flecha hacia la derecha
   ELEMENT_TYPE_CHECKBOX,                 // Control CheckBox
   ELEMENT_TYPE_RADIOBUTTON,              // Control RadioButton
   ELEMENT_TYPE_SCROLLBAR_THUMB_H,        // Deslizador de la barra de desplazamiento horizontal
   ELEMENT_TYPE_SCROLLBAR_THUMB_V,        // Deslizador de la barra de desplazamiento vertical
   ELEMENT_TYPE_SCROLLBAR_H,              // Control ScrollBarHorisontal
   ELEMENT_TYPE_SCROLLBAR_V,              // Control ScrollBarVertical
   ELEMENT_TYPE_TABLE_CELL,               // Celda de la tabla (Vista)
   ELEMENT_TYPE_TABLE_ROW,                // Fila de la tabla (Vista)
   ELEMENT_TYPE_TABLE_COLUMN_CAPTION,     // Encabezado de columna de la tabla (Vista)
   ELEMENT_TYPE_TABLE_HEADER,             // Encabezado de la tabla (Vista)
   ELEMENT_TYPE_TABLE,                    // Tabla (Vista)
   ELEMENT_TYPE_PANEL,                    // Control Panel
   ELEMENT_TYPE_GROUPBOX,                 // Control GroupBox
   ELEMENT_TYPE_CONTAINER,                // Control Container
  };
#define  ACTIVE_ELEMENT_MIN   ELEMENT_TYPE_LABEL         // Valor mínimo de la lista de elementos activos
#define  ACTIVE_ELEMENT_MAX   ELEMENT_TYPE_TABLE_HEADER  // Valor máximo de la lista de elementos activos

Añadamos nuevos valores devueltos a la función que retorna el nombre abreviado del elemento según su tipo:

//+------------------------------------------------------------------+
//|  Devuelve el nombre abreviado del elemento según su tipo         |
//+------------------------------------------------------------------+
string ElementShortName(const ENUM_ELEMENT_TYPE type)
  {
   switch(type)
     {
      case ELEMENT_TYPE_ELEMENT_BASE         :  return "BASE";    // Objeto básico de los elementos gráficos
      case ELEMENT_TYPE_HINT                 :  return "HNT";     // Ayuda emergente
      case ELEMENT_TYPE_LABEL                :  return "LBL";     // Etiqueta de texto
      case ELEMENT_TYPE_BUTTON               :  return "SBTN";    // Botón simple
      case ELEMENT_TYPE_BUTTON_TRIGGERED     :  return "TBTN";    // Botón de dos posiciones
      case ELEMENT_TYPE_BUTTON_ARROW_UP      :  return "BTARU";   // Botón con flecha hacia arriba
      case ELEMENT_TYPE_BUTTON_ARROW_DOWN    :  return "BTARD";   // Botón con flecha hacia abajo
      case ELEMENT_TYPE_BUTTON_ARROW_LEFT    :  return "BTARL";   // Botón con flecha hacia la izquierda
      case ELEMENT_TYPE_BUTTON_ARROW_RIGHT   :  return "BTARR";   // Botón con flecha hacia la derecha
      case ELEMENT_TYPE_CHECKBOX             :  return "CHKB";    // Control CheckBox
      case ELEMENT_TYPE_RADIOBUTTON          :  return "RBTN";    // Control RadioButton
      case ELEMENT_TYPE_SCROLLBAR_THUMB_H    :  return "THMBH";   // Deslizador de la barra de desplazamiento horizontal
      case ELEMENT_TYPE_SCROLLBAR_THUMB_V    :  return "THMBV";   // Deslizador de la barra de desplazamiento vertical
      case ELEMENT_TYPE_SCROLLBAR_H          :  return "SCBH";    // Control ScrollBarHorisontal
      case ELEMENT_TYPE_SCROLLBAR_V          :  return "SCBV";    // Control ScrollBarVertical
      case ELEMENT_TYPE_TABLE_CELL           :  return "TCELL";   // Celda de la tabla (Vista)
      case ELEMENT_TYPE_TABLE_ROW            :  return "TROW";    // Fila de la tabla (Vista)
      case ELEMENT_TYPE_TABLE_COLUMN_CAPTION :  return "TCAPT";   // Encabezado de columna de la tabla (Vista)
      case ELEMENT_TYPE_TABLE_HEADER         :  return "THDR";    // Encabezado de la tabla (Vista)
      case ELEMENT_TYPE_TABLE                :  return "TABLE";   // Tabla (Vista)
      case ELEMENT_TYPE_PANEL                :  return "PNL";     // Control Panel
      case ELEMENT_TYPE_GROUPBOX             :  return "GRBX";    // Control GroupBox
      case ELEMENT_TYPE_CONTAINER            :  return "CNTR";    // Control Container
      default                                :  return "Unknown"; // Desconocido
     }
  }

En la clase base de los elementos gráficos, haremos virtual el método de establecimiento del identificador, ya que será necesario redefinirlo en las nuevas clases:

//+------------------------------------------------------------------+
//| Clase base de los elementos gráficos                             |
//+------------------------------------------------------------------+
class CBaseObj : public CObject
  {
protected:
   int               m_id;                                     // Identificador
   ushort            m_name[];                                 // Nombre
   
public:
//--- Establece (1) el nombre y (2) el identificador
   void              SetName(const string name)                { ::StringToShortArray(name,this.m_name);    }
   virtual void      SetID(const int id)                       { this.m_id=id;                              }
//--- Devuelve (1) el nombre y (2) el identificador
   string            Name(void)                          const { return ::ShortArrayToString(this.m_name);  }
   int               ID(void)                            const { return this.m_id;                          }

//--- Métodos virtuales: (1) comparación, (2) guardado en un archivo, (3) carga desde un archivo, (4) tipo de objeto
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_BASE); }
   
//--- (1) Devuelve, (2) muestra en el registro la descripción del objeto
   virtual string    Description(void);
   virtual void      Print(void);
   
//--- Constructor/destructor
                     CBaseObj (void) : m_id(-1) { this.SetName(""); }
                    ~CBaseObj (void) {}
  };

Cada objeto contiene un objeto de la clase de área rectangular CBound, que devuelve el tamaño del control. Además, cada control tiene una lista de objetos CBound. Esta lista permite almacenar múltiples objetos CBound, que a su vez permiten definir múltiples áreas diferentes en la superficie del elemento gráfico.

Cada una de estas áreas puede utilizarse para distintos escenarios. Por ejemplo, se puede definir una zona determinada dentro de un elemento gráfico, detectar si el cursor se encuentra dentro de esa área y reaccionar ante la interacción de dicha área con el cursor del ratón. Es posible colocar un control dentro de un área determinada para que se muestre dentro de dicha área. Para ello, es necesario permitir almacenar en el objeto CBound un puntero al control.

Vamos a implementar esta posibilidad en la clase CBound:

//+------------------------------------------------------------------+
//| Clase de área rectangular                                        |
//+------------------------------------------------------------------+
class CBound : public CBaseObj
  {
protected:
   CBaseObj         *m_assigned_obj;                           // Objeto asignado al área
   CRect             m_bound;                                  // Estructura del área rectangular

public:
//--- Modifica (1) la anchura, (2) la altura y (3) el tamaño del rectángulo delimitador
   void              ResizeW(const int size)                   { this.m_bound.Width(size);                                    }
   void              ResizeH(const int size)                   { this.m_bound.Height(size);                                   }
   void              Resize(const int w,const int h)           { this.m_bound.Width(w); this.m_bound.Height(h);               }
   
//--- Establece (1) la coordenada X, (2) la coordenada Y o (3) ambas coordenadas del rectángulo delimitador
   void              SetX(const int x)                         { this.m_bound.left=x;                                         }
   void              SetY(const int y)                         { this.m_bound.top=y;                                          }
   void              SetXY(const int x,const int y)            { this.m_bound.LeftTop(x,y);                                   }
   
//--- (1) Establece, (2) desplaza el rectángulo delimitador a las coordenadas indicadas / según el tamaño de desplazamiento especificado
   void              Move(const int x,const int y)             { this.m_bound.Move(x,y);                                      }
   void              Shift(const int dx,const int dy)          { this.m_bound.Shift(dx,dy);                                   }
   
//--- Devuelve las coordenadas, las dimensiones y los límites del objeto
   int               X(void)                             const { return this.m_bound.left;                                    }
   int               Y(void)                             const { return this.m_bound.top;                                     }
   int               Width(void)                         const { return this.m_bound.Width();                                 }
   int               Height(void)                        const { return this.m_bound.Height();                                }
   int               Right(void)                         const { return this.m_bound.right-(this.m_bound.Width()  >0 ? 1 : 0);}
   int               Bottom(void)                        const { return this.m_bound.bottom-(this.m_bound.Height()>0 ? 1 : 0);}

//--- Devuelve un indicador que señala si el cursor se encuentra dentro del área
   bool              Contains(const int x,const int y)   const { return this.m_bound.Contains(x,y);                           }
   
//--- (1) Asigna, (2) devuelve un puntero al elemento asignado
   void              AssignObject(CBaseObj *obj)               { this.m_assigned_obj=obj;                                     }
   CBaseObj         *GetAssignedObj(void)                      { return this.m_assigned_obj;                                  }
   
//--- Devuelve la descripción del objeto
   virtual string    Description(void);
   
//--- Métodos virtuales: (1) comparación, (2) guardado en un archivo, (3) carga desde un archivo, (4) tipo de objeto
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_RECTANGLE_AREA);                         }
   
//--- Constructores/destructor
                     CBound(void) { ::ZeroMemory(this.m_bound); }
                     CBound(const int x,const int y,const int w,const int h) { this.SetXY(x,y); this.Resize(w,h);             }
                    ~CBound(void) { ::ZeroMemory(this.m_bound); }
  };

Actualmente, el objeto CBound de área rectangular está declarado en la clase CCanvasBase, en la que se crean dos objetos CCanvas para dibujar el fondo y el primer plano del elemento gráfico. Es decir, todos los elementos gráficos constan de dos lienzos. Pero debemos crear una clase que permita indicar el control en cuyos lienzos se debe dibujar. Sin crear lienzos propios y, al mismo tiempo, sin apartarnos del concepto general de creación de elementos gráficos.

Por lo tanto, debemos trasladar el objeto de la clase CBound de la clase CCanvasBase a otra clase —la clase padre—, que no creará objetos de lienzo y de la que heredará la clase CCanvasBase. Y en la clase CCanvasBase no hay que declarar instancias de dos CCanvas, sino punteros a los lienzos que se van a crear, y crear un método que cree dos objetos de lienzo. También es necesario declarar un indicador que indique si el elemento gráfico gestiona o no los objetos de lienzo de fondo y de primer plano. Esto es necesario para gestionar la eliminación de los objetos de lienzo creados.

En el archivo Base.mqh, justo después de la clase CAutoRepeat y antes de la clase CCanvasBase, escribiremos una nueva clase CBoundedObj, que hereda de CBaseObj, a la que simplemente trasladaremos todos los métodos relacionados con el objeto CBound de la clase CCanvasBase:

//+------------------------------------------------------------------+
//| Clase base que almacena las dimensiones del objeto               |
//+------------------------------------------------------------------+
class CBoundedObj : public CBaseObj
  {
protected:
   CBound            m_bound;                                  // Límites del objeto
   bool              m_canvas_owner;                           // Bandera de propiedad de los lienzos
public:
//--- Devuelve las coordenadas, las dimensiones y los límites del objeto
   int               X(void)                             const { return this.m_bound.X();                                                          }
   int               Y(void)                             const { return this.m_bound.Y();                                                          }
   int               Width(void)                         const { return this.m_bound.Width();                                                      }
   int               Height(void)                        const { return this.m_bound.Height();                                                     }
   int               Right(void)                         const { return this.m_bound.Right();                                                      }
   int               Bottom(void)                        const { return this.m_bound.Bottom();                                                     }

//--- Modifica (1) la anchura, (2) la altura y (3) el tamaño del rectángulo delimitador
   void              BoundResizeW(const int size)              { this.m_bound.ResizeW(size);                                                       }
   void              BoundResizeH(const int size)              { this.m_bound.ResizeH(size);                                                       }
   void              BoundResize(const int w,const int h)      { this.m_bound.Resize(w,h);                                                         }
   
//--- Establece (1) la coordenada X, (2) la coordenada Y o (3) ambas coordenadas del rectángulo delimitador
   void              BoundSetX(const int x)                    { this.m_bound.SetX(x);                                                             }
   void              BoundSetY(const int y)                    { this.m_bound.SetY(y);                                                             }
   void              BoundSetXY(const int x,const int y)       { this.m_bound.SetXY(x,y);                                                          }
   
//--- (1) Establece, (2) desplaza el rectángulo delimitador a las coordenadas indicadas / según el tamaño de desplazamiento especificado
   void              BoundMove(const int x,const int y)        { this.m_bound.Move(x,y);                                                           }
   void              BoundShift(const int dx,const int dy)     { this.m_bound.Shift(dx,dy);                                                        }
   
//--- Métodos virtuales: (1) comparación, (2) guardado en un archivo, (3) carga desde un archivo, (4) tipo de objeto
   //virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_BOUNDED_BASE); }
                     
                     CBoundedObj (void) : m_canvas_owner(true) {}
                     CBoundedObj (const string user_name,const int id,const int x,const int y,const int w,const int h);
                    ~CBoundedObj (void){}
  };
//+------------------------------------------------------------------+
//| CBoundedObj::Constructor                                         |
//+------------------------------------------------------------------+
CBoundedObj::CBoundedObj(const string user_name,const int id,const int x,const int y,const int w,const int h) : m_canvas_owner(true)
  {
//--- Obtenemos el identificador corregido del gráfico y la distancia en píxeles a lo largo del eje vertical Y
   this.m_bound.SetName(user_name);
   this.m_bound.SetID(id);
   this.m_bound.SetXY(x,y);
   this.m_bound.Resize(w,h);
  }
//+------------------------------------------------------------------+
//| CBoundedObj::Guardado en un archivo                              |
//+------------------------------------------------------------------+
bool CBoundedObj::Save(const int file_handle)
  {
//--- Guardamos los datos del objeto padre
   if(!CBaseObj::Save(file_handle))
      return false;
 
//--- Guardamos el indicador de propiedad de los lienzos
   if(::FileWriteInteger(file_handle,this.m_canvas_owner,INT_VALUE)!=INT_VALUE)
      return false;
//--- Guardamos las dimensiones
   return this.m_bound.Save(file_handle);
  }
//+------------------------------------------------------------------+
//| CBoundedObj::Carga desde un archivo                              |
//+------------------------------------------------------------------+
bool CBoundedObj::Load(const int file_handle)
  {
//--- Cargamos los datos del objeto padre
   if(!CBaseObj::Load(file_handle))
      return false;
   
//--- Cargamos el indicador de propiedad de los lienzos
   this.m_canvas_owner=::FileReadInteger(file_handle,INT_VALUE);
//--- Cargamos las dimensiones
   return this.m_bound.Load(file_handle);
  }

La clase presentada simplemente hereda las propiedades de la clase base CBaseObj, permite especificar los límites del objeto (clase CBound) y tiene un indicador de propiedad de los lienzos.

La clase base del lienzo de los elementos gráficos ahora debe heredar de la nueva clase CBoundedObj, y no de la clase CBaseObj, como ocurría anteriormente. Todo lo relacionado con el manejo del objeto de la clase CBound para establecer y recuperar las dimensiones del elemento ya se ha trasladado a la nueva clase CBoundedObj y se ha eliminado de la clase CCanvasBase. Ahora, los objetos de los lienzos no se declaran como instancias de la clase, sino que se declaran como punteros a los objetos CCanvas que se van a crear:

//+------------------------------------------------------------------+
//| Clase base del lienzo de elementos gráficos                      |
//+------------------------------------------------------------------+
class CCanvasBase : public CBoundedObj
  {
private: 
   bool              m_chart_mouse_wheel_flag;                 // Indicador de envío de mensajes de desplazamiento de la rueda del ratón
   bool              m_chart_mouse_move_flag;                  // Indicador de envío de mensajes de movimiento del cursor del ratón
   bool              m_chart_object_create_flag;               // Indicador de envío de mensajes sobre el evento de creación de un objeto gráfico
   bool              m_chart_mouse_scroll_flag;                // Indicador de desplazamiento del gráfico con el botón izquierdo y la rueda del ratón
   bool              m_chart_context_menu_flag;                // Indicador de acceso al menú contextual al pulsar el botón derecho del ratón
   bool              m_chart_crosshair_tool_flag;              // Indicador de acceso a la herramienta «Cruz» al pulsar el botón central del ratón
   bool              m_flags_state;                            // Estado de los indicadores de desplazamiento del gráfico con la rueda del ratón, del menú contextual y de la Cruz
   
//--- Configuración de restricciones para el gráfico (desplazamiento con la rueda del ratón, menú contextual y Cruz)
   void              SetFlags(const bool flag);
   
protected:
   CCanvas          *m_background;                             // Lienzo para dibujar el fondo
   CCanvas          *m_foreground;                             // Lienzo para dibujar el primer plano

   CCanvasBase      *m_container;                              // Objeto contenedor padre
   CColorElement     m_color_background;                       // Objeto de control del color de fondo
   CColorElement     m_color_foreground;                       // Objeto de control del color de primer plano
   CColorElement     m_color_border;                           // Objeto de control del color del borde

Los métodos para obtener las coordenadas y las dimensiones de los objetos gráficos de los lienzos se han hecho públicos, y los métodos para cambiar las dimensiones de los lienzos se han hecho virtuales:

//--- Obtención de las coordenadas, los límites y las dimensiones de un objeto gráfico
public:
   int               ObjectX(void)                       const { return this.m_obj_x;                                                              }
   int               ObjectY(void)                       const { return this.m_obj_y;                                                              }
   int               ObjectWidth(void)                   const { return this.m_background.Width();                                                 }
   int               ObjectHeight(void)                  const { return this.m_background.Height();                                                }
   int               ObjectRight(void)                   const { return this.ObjectX()+this.ObjectWidth()-1;                                       }
   int               ObjectBottom(void)                  const { return this.ObjectY()+this.ObjectHeight()-1;                                      }
   
//--- Modifica (1) la anchura, (2) la altura y (3) el tamaño del objeto gráfico
protected:
   virtual bool      ObjectResizeW(const int size);
   virtual bool      ObjectResizeH(const int size);
   bool              ObjectResize(const int w,const int h);
   
//--- Establece la coordenada (1) X, (2) Y y (3) ambas coordenadas de un objeto gráfico
   virtual bool      ObjectSetX(const int x);
   virtual bool      ObjectSetY(const int y);
   bool              ObjectSetXY(const int x,const int y)      { return(this.ObjectSetX(x) && this.ObjectSetY(y));                                 }

En la sección protegida de la clase se ha declarado un método para crear lienzos:

//--- Establece un puntero al objeto contenedor padre
   void              SetContainerObj(CCanvasBase *obj);
   
protected:
//--- Crea lienzos de fondo y de primer plano
   bool              CreateCanvasObjects(void);
//--- Crea OBJ_BITMAP_LABEL
   bool              Create(const long chart_id,const int wnd,const string object_name,const int x,const int y,const int w,const int h); 
public:
//--- (1) Establece, (2) devuelve el estado
   void              SetState(ENUM_ELEMENT_STATE state)        { this.m_state=state; this.ColorsToDefault();                                       }
   ENUM_ELEMENT_STATE State(void)                        const { return this.m_state;                                                              }

El método que establece el indicador de permiso para recortar el elemento según los límites del contenedor se ha hecho virtual:

//--- Establece en el objeto el indicador de (1) movilidad, (2) objeto principal y (3) posibilidad de cambiar el tamaño
//--- recorte según los límites del contenedor
   void              SetMovable(const bool flag)               { this.m_movable=flag;                                                              }
   void              SetAsMain(void)                           { this.m_main=true;                                                                 }
   virtual void      SetResizable(const bool flag)             { this.m_resizable=flag;                                                            }
   void              SetAutorepeat(const bool flag)            { this.m_autorepeat_flag=flag;                                                      }
   void              SetScrollable(const bool flag)            { this.m_scroll_flag=flag;                                                          }
   virtual void      SetTrimmered(const bool flag)             { this.m_trim_flag=flag;                                                            }

En el constructor de la clase ahora se invoca el método para crear los objetos de los lienzos de fondo y de primer plano:

//+------------------------------------------------------------------+
//| CCanvasBase::Constructor                                         |
//+------------------------------------------------------------------+
CCanvasBase::CCanvasBase(const string object_name,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) :
   m_program_name(::MQLInfoString(MQL_PROGRAM_NAME)), m_wnd(wnd<0 ? 0 : wnd), m_alpha_bg(0), m_alpha_fg(255),
   m_hidden(false), m_blocked(false), m_focused(false), m_movable(false), m_resizable(false), m_main(false), m_autorepeat_flag(false), m_trim_flag(true), m_scroll_flag(false),
   m_border_width_lt(0), m_border_width_rt(0), m_border_width_up(0), m_border_width_dn(0), m_z_order(0),
   m_state(0), m_cursor_delta_x(0), m_cursor_delta_y(0)
  {
//--- Obtenemos el identificador corregido del gráfico y la distancia en píxeles a lo largo del eje vertical Y
//--- entre el borde superior de la subventana del indicador y el borde superior de la ventana principal del gráfico
   this.m_chart_id=this.CorrectChartID(chart_id);
   
//--- Si no hemos podido crear los lienzos, salimos
   if(!this.CreateCanvasObjects())
      return;
      
//--- Si se han creado el recurso gráfico y el objeto gráfico
   if(this.Create(this.m_chart_id,this.m_wnd,object_name,x,y,w,h))
     {
      //--- Limpiamos los lienzos de fondo y de primer plano y establecemos los valores iniciales de las coordenadas,
      //--- los nombres de los objetos gráficos y las propiedades del texto que se dibuja en primer plano
      this.Clear(false);
      this.m_obj_x=x;
      this.m_obj_y=y;
      this.m_color_background.SetName("Background");
      this.m_color_foreground.SetName("Foreground");
      this.m_color_border.SetName("Border");
      this.m_foreground.FontSet(DEF_FONTNAME,-DEF_FONTSIZE*10,FW_MEDIUM);
      this.m_bound.SetName("Perimeter");
      
      //--- Guardamos los permisos del ratón y de las herramientas del gráfico
      this.Init();
     }
  }

Método para crear lienzos de fondo y de primer plano:

//+------------------------------------------------------------------+
//| CCanvasBase::Crea lienzos de fondo y de primer plano             |
//+------------------------------------------------------------------+
bool CCanvasBase::CreateCanvasObjects(void)
  {
//--- Si ambos lienzos ya se han creado, o si la clase no gestiona los lienzos, devolvemos true
   if((this.m_background!=NULL && this.m_foreground!=NULL) || !this.m_canvas_owner)
      return true;
//--- Creamos el lienzo de fondo
   this.m_background=new CCanvas();
   if(this.m_background==NULL)
     {
      ::PrintFormat("%s: Error! Failed to create background canvas",__FUNCTION__);
      return false;
     }
//--- Creamos el lienzo de primer plano
   this.m_foreground=new CCanvas();
   if(this.m_foreground==NULL)
     {
      ::PrintFormat("%s: Error! Failed to create foreground canvas",__FUNCTION__);
      return false;
     }
//--- Todo correcto
   return true;
  }

En el método que destruye el objeto, ahora comprobamos el indicador de control de los lienzos:

//+------------------------------------------------------------------+
//| CCanvasBase::Destruye el objeto                                  |
//+------------------------------------------------------------------+
void CCanvasBase::Destroy(void)
  {
   if(this.m_canvas_owner)
     {
      this.m_background.Destroy();
      this.m_foreground.Destroy();
      delete this.m_background;
      delete this.m_foreground;
      this.m_background=NULL;
      this.m_foreground=NULL;
     }
  }

Los lienzos solo se eliminan si el objeto gestiona dichos lienzos. Sin embargo, si al objeto se le pasan los lienzos de otro control para dibujar, no los eliminará: solo debe eliminarlos el objeto al que pertenecen.

En el método de inicialización predeterminada de la clase, para el objeto se establece el indicador de control de los lienzos:

//+------------------------------------------------------------------+
//| CCanvasBase::Inicialización de la clase                          |
//+------------------------------------------------------------------+
void CCanvasBase::Init(void)
  {
//--- Guardamos los permisos del ratón y de las herramientas del gráfico
   this.m_chart_mouse_wheel_flag   = ::ChartGetInteger(this.m_chart_id, CHART_EVENT_MOUSE_WHEEL);
   this.m_chart_mouse_move_flag    = ::ChartGetInteger(this.m_chart_id, CHART_EVENT_MOUSE_MOVE);
   this.m_chart_object_create_flag = ::ChartGetInteger(this.m_chart_id, CHART_EVENT_OBJECT_CREATE);
   this.m_chart_mouse_scroll_flag  = ::ChartGetInteger(this.m_chart_id, CHART_MOUSE_SCROLL);
   this.m_chart_context_menu_flag  = ::ChartGetInteger(this.m_chart_id, CHART_CONTEXT_MENU);
   this.m_chart_crosshair_tool_flag= ::ChartGetInteger(this.m_chart_id, CHART_CROSSHAIR_TOOL);
//--- Configuramos los permisos para el ratón y el gráfico
   ::ChartSetInteger(this.m_chart_id, CHART_EVENT_MOUSE_WHEEL, true);
   ::ChartSetInteger(this.m_chart_id, CHART_EVENT_MOUSE_MOVE, true);
   ::ChartSetInteger(this.m_chart_id, CHART_EVENT_OBJECT_CREATE, true);

//--- Inicializamos los colores predeterminados del objeto
   this.InitColors();
//--- Inicializamos un temporizador en milisegundos
   ::EventSetMillisecondTimer(16);
   
//--- Indicador de propiedad de los lienzos
   this.m_canvas_owner=true;
  }

Solo para aquellos objetos que no tengan lienzos propios y que hereden de la clase CBoundedObj será necesario restablecer este indicador a false.

Ahora abriremos el archivo Controls.mqh y realizaremos en él las mejoras correspondientes.

En la sección de sustituciones de macro, añadiremos dos nuevas, que definen la altura predeterminada de la fila de la tabla y del encabezado:

//+------------------------------------------------------------------+
//| Definiciones de macros                                           |
//+------------------------------------------------------------------+
#define  DEF_LABEL_W                50          // Anchura de la etiqueta de texto por defecto
#define  DEF_LABEL_H                16          // Altura de la etiqueta de texto por defecto
#define  DEF_BUTTON_W               60          // Anchura del botón por defecto
#define  DEF_BUTTON_H               16          // Altura del botón por defecto
#define  DEF_TABLE_ROW_H            16          // Altura de la línea por defecto
#define  DEF_TABLE_HEADER_H         20          // Altura del encabezado de la tabla por defecto
#define  DEF_PANEL_W                80          // Anchura del panel por defecto
#define  DEF_PANEL_H                80          // Altura del panel por defecto
#define  DEF_PANEL_MIN_W            60          // Anchura mínima del panel
#define  DEF_PANEL_MIN_H            60          // Altura mínima del panel
#define  DEF_SCROLLBAR_TH           13          // Grosor de la barra de desplazamiento por defecto
#define  DEF_THUMB_MIN_SIZE         8           // Grosor mínimo del deslizador
#define  DEF_AUTOREPEAT_DELAY       500         // Retardo antes del inicio de la auto-repetición
#define  DEF_AUTOREPEAT_INTERVAL    100         // Frecuencia de las auto-repeticiones

El archivo contiene una clase de lista enlazada de objetos, CListObj, destinada a almacenar una lista de elementos gráficos. Lo hemos copiado a este archivo desde el archivo de clases del modelo de la tabla. Si se deja el mismo nombre de clase, se producirá un conflicto de nombres, mientras que los objetos almacenados en estas dos listas, en archivos diferentes, son completamente distintos. Así que aquí dejaremos la clase tal cual, pero le cambiaremos el nombre. Será una clase de lista enlazada de elementos gráficos:

//+------------------------------------------------------------------+
//| Clase de lista enlazada de elementos gráficos                    |
//+------------------------------------------------------------------+
class CListElm : public CList
  {
protected:
   ENUM_ELEMENT_TYPE m_element_type;   // Tipo del objeto que se crea en CreateElement()
public:
//--- Configuración del tipo de elemento
   void              SetElementType(const ENUM_ELEMENT_TYPE type) { this.m_element_type=type;   }
   
//--- Método virtual para (1) cargar una lista desde un archivo y (2) crear un elemento de la lista
   virtual bool      Load(const int file_handle);
   virtual CObject  *CreateElement(void);
  };

Vamos a sustituir la cadena «CListObj» por la cadena «CListElm» en todo el archivo.

En el método para crear un elemento de la lista, añadiremos nuevos tipos de elementos gráficos:

//+------------------------------------------------------------------+
//| Método para crear un elemento de la lista                        |
//+------------------------------------------------------------------+
CObject *CListElm::CreateElement(void)
  {
//--- En función del tipo de objeto en m_element_type, creamos un nuevo objeto
   switch(this.m_element_type)
     {
      case ELEMENT_TYPE_BASE                 :  return new CBaseObj();           // Objeto básico de los elementos gráficos
      case ELEMENT_TYPE_COLOR                :  return new CColor();             // Objeto de color
      case ELEMENT_TYPE_COLORS_ELEMENT       :  return new CColorElement();      // Objeto de colores de un elemento de objeto gráfico
      case ELEMENT_TYPE_RECTANGLE_AREA       :  return new CBound();             // Área rectangular del elemento
      case ELEMENT_TYPE_IMAGE_PAINTER        :  return new CImagePainter();      // Objeto para dibujar imágenes
      case ELEMENT_TYPE_CANVAS_BASE          :  return new CCanvasBase();        // Objeto básico del lienzo de los elementos gráficos
      case ELEMENT_TYPE_ELEMENT_BASE         :  return new CElementBase();       // Objeto básico de los elementos gráficos
      case ELEMENT_TYPE_HINT                 :  return new CVisualHint();        // Ayuda emergente
      case ELEMENT_TYPE_LABEL                :  return new CLabel();             // Etiqueta de texto
      case ELEMENT_TYPE_BUTTON               :  return new CButton();            // Botón simple
      case ELEMENT_TYPE_BUTTON_TRIGGERED     :  return new CButtonTriggered();   // Botón de dos posiciones
      case ELEMENT_TYPE_BUTTON_ARROW_UP      :  return new CButtonArrowUp();     // Botón con flecha hacia arriba
      case ELEMENT_TYPE_BUTTON_ARROW_DOWN    :  return new CButtonArrowDown();   // Botón con flecha hacia abajo
      case ELEMENT_TYPE_BUTTON_ARROW_LEFT    :  return new CButtonArrowLeft();   // Botón con flecha hacia la izquierda
      case ELEMENT_TYPE_BUTTON_ARROW_RIGHT   :  return new CButtonArrowRight();  // Botón con flecha hacia la derecha
      case ELEMENT_TYPE_CHECKBOX             :  return new CCheckBox();          // Control CheckBox
      case ELEMENT_TYPE_RADIOBUTTON          :  return new CRadioButton();       // Control RadioButton
      case ELEMENT_TYPE_TABLE_CELL           :  return new CTableCellView();     // Celda de la tabla (Vista)
      case ELEMENT_TYPE_TABLE_ROW            :  return new CTableRowView();      // Fila de la tabla (Vista)
      case ELEMENT_TYPE_TABLE_COLUMN_CAPTION :  return new CColumnCaptionView(); // Encabezado de columna de la tabla (Vista)
      case ELEMENT_TYPE_TABLE_HEADER         :  return new CTableHeaderView();   // Encabezado de la tabla (Vista)
      case ELEMENT_TYPE_TABLE                :  return new CTableView();         // Tabla (Vista)
      case ELEMENT_TYPE_PANEL                :  return new CPanel();             // Control Panel
      case ELEMENT_TYPE_GROUPBOX             :  return new CGroupBox();          // Control GroupBox
      case ELEMENT_TYPE_CONTAINER            :  return new CContainer();         // Control GroupBox
      default                                :  return NULL;
     }
  }

En la clase base del elemento gráfico añadiremos un método que devuelve un puntero al objeto:

public:
//--- Devuelve el propio objeto
   CElementBase     *GetObject(void)                           { return &this;                     }
//--- Devuelve un puntero a (1) una clase de dibujo y (2) una lista de ayudas emergentes
   CImagePainter    *Painter(void)                             { return &this.m_painter;           }
   CListElm         *GetListHints(void)                        { return &this.m_list_hints;        }

En la clase de la etiqueta de texto haremos virtual el método que muestra el texto en el lienzo:

//--- Establece la coordenada (1) X, (2) Y del texto
   void              SetTextShiftH(const int x)                { this.ClearText(); this.m_text_x=x;            }
   void              SetTextShiftV(const int y)                { this.ClearText(); this.m_text_y=y;            }
   
//--- Muestra el texto
   virtual void      DrawText(const int dx, const int dy, const string text, const bool chart_redraw);
   
//--- Dibuja el aspecto externo
   virtual void      Draw(const bool chart_redraw);

En la clase CPanel perfeccionaremos los métodos de cambio de tamaño:

//+------------------------------------------------------------------+
//| CPanel::Modifica el ancho del objeto                             |
//+------------------------------------------------------------------+
bool CPanel::ResizeW(const int w)
  {
   if(!this.ObjectResizeW(w))
      return false;
   this.BoundResizeW(w);
   this.SetImageSize(w,this.Height());
   if(!this.ObjectTrim())
     {
      this.Update(false);
      this.Draw(false);
     }
//--- Obtenemos un puntero al elemento base y, si existe y su tipo es contenedor,
//--- comprobamos la relación entre las dimensiones del elemento actual y las dimensiones del contenedor
//--- para mostrar las barras de desplazamiento en el contenedor cuando sea necesario
   CContainer *base=this.GetContainer();
   if(base!=NULL && base.Type()==ELEMENT_TYPE_CONTAINER)
      base.CheckElementSizes(&this);
      
//--- En el bucle que recorre los elementos adjuntos, recortamos cada elemento según los límites del contenedor
   int total=this.m_list_elm.Total();
   for(int i=0;i<total;i++)
     {
      CElementBase *elm=this.GetAttachedElementAt(i);
      if(elm!=NULL)
         elm.ObjectTrim();
     }
//--- Todo correcto
   return true;
  }
//+------------------------------------------------------------------+
//| CPanel::Modifica la altura del objeto                            |
//+------------------------------------------------------------------+
bool CPanel::ResizeH(const int h)
  {
   if(!this.ObjectResizeH(h))
      return false;
   this.BoundResizeH(h);
   this.SetImageSize(this.Width(),h);
   if(!this.ObjectTrim())
     {
      this.Update(false);
      this.Draw(false);
     }
//--- Obtenemos un puntero al elemento base y, si existe y su tipo es contenedor,
//--- comprobamos la relación entre las dimensiones del elemento actual y las dimensiones del contenedor
//--- para mostrar las barras de desplazamiento en el contenedor cuando sea necesario
   CContainer *base=this.GetContainer();
   if(base!=NULL && base.Type()==ELEMENT_TYPE_CONTAINER)
      base.CheckElementSizes(&this);
      
//--- En el bucle que recorre los elementos adjuntos, recortamos cada elemento según los límites del contenedor
   int total=this.m_list_elm.Total();
   for(int i=0;i<total;i++)
     {
      CElementBase *elm=this.GetAttachedElementAt(i);
      if(elm!=NULL)
         elm.ObjectTrim();
     }
//--- Todo correcto
   return true;
  }
//+------------------------------------------------------------------+
//| CPanel::Modifica las dimensiones del objeto                      |
//+------------------------------------------------------------------+
bool CPanel::Resize(const int w,const int h)
  {
   if(!this.ObjectResize(w,h))
      return false;
   this.BoundResize(w,h);
   this.SetImageSize(w,h);
   if(!this.ObjectTrim())
     {
      this.Update(false);
      this.Draw(false);
     }
//--- Obtenemos un puntero al elemento base y, si existe y su tipo es contenedor,
//--- comprobamos la relación entre las dimensiones del elemento actual y las dimensiones del contenedor
//--- para mostrar las barras de desplazamiento en el contenedor cuando sea necesario
   CContainer *base=this.GetContainer();
   if(base!=NULL && base.Type()==ELEMENT_TYPE_CONTAINER)
      base.CheckElementSizes(&this);
      
//--- En el bucle que recorre los elementos adjuntos, recortamos cada elemento según los límites del contenedor
   int total=this.m_list_elm.Total();
   for(int i=0;i<total;i++)
     {
      CElementBase *elm=this.GetAttachedElementAt(i);
      if(elm!=NULL)
         elm.ObjectTrim();
     }
//--- Todo correcto
   return true;
  }

La esencia de la mejora de los métodos radica en que, si un elemento forma parte de un contenedor y se han modificado sus dimensiones, es necesario comprobar las nuevas dimensiones del elemento con respecto al contenedor. Si el elemento supera las dimensiones del contenedor, se recorta al tamaño de este (esto ya se había implementado anteriormente), pero, además, deben aparecer barras de desplazamiento en el contenedor. Esta comprobación, mediante el método del objeto contenedor CheckElementSizes(), es la que hemos añadido a los métodos de cambio de tamaño del elemento gráfico.

En el método que dibuja la apariencia de un objeto de panel en la clase CPanel, dibujamos el borde solo si al menos uno de los cuatro lados tiene un valor distinto de cero para el parámetro BorderWidth:

//+------------------------------------------------------------------+
//| CPanel::Dibuja la apariencia                                     |
//+------------------------------------------------------------------+
void CPanel::Draw(const bool chart_redraw)
  {
//--- Rellenamos el objeto con el color de fondo
   this.Fill(this.BackColor(),false);
   
//--- Limpiamos el área de dibujo
   this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
//--- Establecemos el color de las líneas oscuras y claras y dibujamos el borde del panel
   color clr_dark =(this.BackColor()==clrNULL ? this.BackColor() : this.GetBackColorControl().NewColor(this.BackColor(),-20,-20,-20));
   color clr_light=(this.BackColor()==clrNULL ? this.BackColor() : this.GetBackColorControl().NewColor(this.BackColor(),  6,  6,  6));
   if(this.BorderWidthBottom()+this.BorderWidthLeft()+this.BorderWidthRight()+this.BorderWidthTop()!=0)
      this.m_painter.FrameGroupElements(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),
                                        this.m_painter.Width(),this.m_painter.Height(),this.Text(),
                                        this.ForeColor(),clr_dark,clr_light,this.AlphaFG(),true);
   
//--- Actualizamos el lienzo de fondo sin redibujar el gráfico
   this.m_background.Update(false);
   
//--- Dibujamos los elementos de la lista
   for(int i=0;i<this.m_list_elm.Total();i++)
     {
      CElementBase *elm=this.GetAttachedElementAt(i);
      if(elm!=NULL && elm.Type()!=ELEMENT_TYPE_SCROLLBAR_H && elm.Type()!=ELEMENT_TYPE_SCROLLBAR_V)
         elm.Draw(false);
     }
//--- Si se indica, actualizamos el gráfico
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

En el método para crear y añadir un nuevo elemento a la lista en la clase del objeto de panel, añadiremos la creación de nuevos controles:

//+------------------------------------------------------------------+
//| CPanel::Crea y añade un nuevo elemento a la lista                |
//+------------------------------------------------------------------+
CElementBase *CPanel::InsertNewElement(const ENUM_ELEMENT_TYPE type,const string text,const string user_name,const int dx,const int dy,const int w,const int h)
  {
//--- Creamos el nombre de un objeto gráfico
   int elm_total=this.m_list_elm.Total();
   string obj_name=this.NameFG()+"_"+ElementShortName(type)+(string)elm_total;
//--- Calculamos las coordenadas
   int x=this.X()+dx;
   int y=this.Y()+dy;
//--- Dependiendo del tipo de objeto, creamos un nuevo objeto
   CElementBase *element=NULL;
   switch(type)
     {
      case ELEMENT_TYPE_LABEL                :  element = new CLabel(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);             break;   // Etiqueta de texto
      case ELEMENT_TYPE_BUTTON               :  element = new CButton(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);            break;   // Botón simple
      case ELEMENT_TYPE_BUTTON_TRIGGERED     :  element = new CButtonTriggered(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Botón de dos posiciones
      case ELEMENT_TYPE_BUTTON_ARROW_UP      :  element = new CButtonArrowUp(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);     break;   // Botón con flecha hacia arriba
      case ELEMENT_TYPE_BUTTON_ARROW_DOWN    :  element = new CButtonArrowDown(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Botón con flecha hacia abajo
      case ELEMENT_TYPE_BUTTON_ARROW_LEFT    :  element = new CButtonArrowLeft(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Botón con flecha hacia la izquierda
      case ELEMENT_TYPE_BUTTON_ARROW_RIGHT   :  element = new CButtonArrowRight(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);  break;   // Botón con flecha hacia la derecha
      case ELEMENT_TYPE_CHECKBOX             :  element = new CCheckBox(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);          break;   // Control CheckBox
      case ELEMENT_TYPE_RADIOBUTTON          :  element = new CRadioButton(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);       break;   // Control RadioButton
      case ELEMENT_TYPE_SCROLLBAR_THUMB_H    :  element = new CScrollBarThumbH(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Barra de desplazamiento del ScrollBar horizontal
      case ELEMENT_TYPE_SCROLLBAR_THUMB_V    :  element = new CScrollBarThumbV(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Barra de desplazamiento del ScrollBar vertical
      case ELEMENT_TYPE_SCROLLBAR_H          :  element = new CScrollBarH(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);        break;   // Control ScrollBar horizontal
      case ELEMENT_TYPE_SCROLLBAR_V          :  element = new CScrollBarV(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);        break;   // Control ScrollBar vertical
      case ELEMENT_TYPE_TABLE_ROW            :  element = new CTableRowView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);      break;   // Objeto de representación visual de una fila de la tabla
      case ELEMENT_TYPE_TABLE_COLUMN_CAPTION :  element = new CColumnCaptionView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h); break;   // Objeto de representación visual del encabezado de una columna de la tabla
      case ELEMENT_TYPE_TABLE_HEADER         :  element = new CTableHeaderView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);   break;   // Objeto de representación visual del encabezado de la tabla
      case ELEMENT_TYPE_TABLE                :  element = new CTableView(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);         break;   // Objeto de representación visual de la tabla
      case ELEMENT_TYPE_PANEL                :  element = new CPanel(obj_name,"",this.m_chart_id,this.m_wnd,x,y,w,h);               break;   // Control Panel
      case ELEMENT_TYPE_GROUPBOX             :  element = new CGroupBox(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);          break;   // Control GroupBox
      case ELEMENT_TYPE_CONTAINER            :  element = new CContainer(obj_name,text,this.m_chart_id,this.m_wnd,x,y,w,h);         break;   // Control Container
      default                                :  element = NULL;
     }

//--- Si no se ha creado el nuevo elemento, lo indicamos y devolvemos NULL
   if(element==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create graphic element %s",__FUNCTION__,ElementDescription(type));
      return NULL;
     }
//--- Establecemos el identificador, el nombre, el contenedor y el orden de superposición (z-order) del elemento
   element.SetID(elm_total);
   element.SetName(user_name);
   element.SetContainerObj(&this);
   element.ObjectSetZOrder(this.ObjectZOrder()+1);
   
//--- Si el elemento creado no se ha añadido a la lista, lo notificamos, eliminamos el elemento creado y devolvemos NULL
   if(!this.AddNewElement(element))
     {
      ::PrintFormat("%s: Error. Failed to add %s element with ID %d to list",__FUNCTION__,ElementDescription(type),element.ID());
      delete element;
      return NULL;
     }
//--- Obtenemos el elemento padre al que están vinculados los elementos hijos
   CElementBase *elm=this.GetContainer();
//--- Si el elemento padre es de tipo «Contenedor», significa que tiene barras de desplazamiento
   if(elm!=NULL && elm.Type()==ELEMENT_TYPE_CONTAINER)
     {
      //--- Convertimos CElementBase a CContainer
      CContainer *container_obj=elm;
      //--- Si la barra de desplazamiento horizontal está visible,
      if(container_obj.ScrollBarHorzIsVisible())
        {
         //--- obtenemos un puntero a la barra de desplazamiento horizontal y la llevamos al primer plano
         CScrollBarH *sbh=container_obj.GetScrollBarH();
         if(sbh!=NULL)
            sbh.BringToTop(false);
        }
      //--- Si la barra de desplazamiento vertical está visible,
      if(container_obj.ScrollBarVertIsVisible())
        {
         //--- obtenemos un puntero a la barra de desplazamiento vertical y la llevamos al primer plano
         CScrollBarV *sbv=container_obj.GetScrollBarV();
         if(sbv!=NULL)
            sbv.BringToTop(false);
        }
     }
//--- Devolvemos un puntero al elemento creado y adjunto
   return element;
  }

En el método que coloca el objeto en primer plano, añadiremos el recorte de los objetos adjuntos a las dimensiones del contenedor:

//+------------------------------------------------------------------+
//| CPanel::Coloca el objeto en primer plano                         |
//+------------------------------------------------------------------+
void CPanel::BringToTop(const bool chart_redraw)
  {
//--- Colocamos el panel en primer plano
   CCanvasBase::BringToTop(false);
//--- Colocamos en primer plano los objetos adjuntos
   for(int i=0;i<this.m_list_elm.Total();i++)
     {
      CElementBase *elm=this.GetAttachedElementAt(i);
      if(elm!=NULL)
        {
         if(elm.Type()==ELEMENT_TYPE_SCROLLBAR_H || elm.Type()==ELEMENT_TYPE_SCROLLBAR_V)
            continue;
         elm.BringToTop(false);
         elm.ObjectTrim();
        }
     }
//--- Si así se indica, redibujamos el gráfico
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

En la clase de la barra de desplazamiento horizontal hay un indicador que determina el permiso para recortar el elemento según los límites del contenedor. El objeto de la barra de desplazamiento se compone de varios objetos, y hay que establecer el mismo valor del indicador para cada uno de ellos. Lo haremos todo en un único método. Redefiniremos en la clase el método que establece el indicador de recorte según los límites del contenedor:

//+------------------------------------------------------------------+
//| Clase de la barra de desplazamiento horizontal                   |
//+------------------------------------------------------------------+
class CScrollBarH : public CPanel
  {
protected:
   CButtonArrowLeft *m_butt_left;                              // Botón con flecha hacia la izquierda
   CButtonArrowRight*m_butt_right;                             // Botón con flecha hacia la derecha
   CScrollBarThumbH *m_thumb;                                  // Control deslizante de la barra de desplazamiento

public:
//--- Devuelve un puntero al (1) botón izquierdo, al (2) botón derecho y al (3) control deslizante
   CButtonArrowLeft *GetButtonLeft(void)                       { return this.m_butt_left;                                              }
   CButtonArrowRight*GetButtonRight(void)                      { return this.m_butt_right;                                             }
   CScrollBarThumbH *GetThumb(void)                            { return this.m_thumb;                                                  }

//--- (1) Establece, (2) devuelve el indicador de actualización del gráfico
   void              SetChartRedrawFlag(const bool flag)       { if(this.m_thumb!=NULL) this.m_thumb.SetChartRedrawFlag(flag);         }
   bool              ChartRedrawFlag(void)               const { return(this.m_thumb!=NULL ? this.m_thumb.ChartRedrawFlag() : false);  }

//--- Devuelve (1) la longitud, (2) el inicio de la pista y (3) la posición del control deslizante
   int               TrackLength(void)    const;
   int               TrackBegin(void)     const;
   int               ThumbPosition(void)  const;
   
//--- Establece la posición del control deslizante
   bool              SetThumbPosition(const int pos)     const { return(this.m_thumb!=NULL ? this.m_thumb.MoveX(pos) : false);         }
//--- Cambia el tamaño del control deslizante
   bool              SetThumbSize(const uint size)       const { return(this.m_thumb!=NULL ? this.m_thumb.ResizeW(size) : false);      }

//--- Modifica la anchura del objeto
   virtual bool      ResizeW(const int size);
   
//--- Establece el indicador de visibilidad en el contenedor
   virtual void      SetVisibleInContainer(const bool flag);
   
//--- Establece el indicador de recorte por los límites del contenedor
   virtual void      SetTrimmered(const bool flag);

//--- Dibuja el aspecto externo
   virtual void      Draw(const bool chart_redraw);
   
//--- Tipo de objeto
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_SCROLLBAR_H);                                     }
   
//--- Inicialización (1) del objeto de la clase y (2) de los colores predeterminados del objeto
   void              Init(void);
   virtual void      InitColors(void);
   
//--- Controlador de desplazamiento de la rueda (Wheel)
   virtual void      OnWheelEvent(const int id, const long lparam, const double dparam, const string sparam);

//--- Constructores/destructor
                     CScrollBarH(void);
                     CScrollBarH(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CScrollBarH(void) {}
  };

Implementación del método:

//+------------------------------------------------------------------+
//| CScrollBarH::Establece el indicador de recorte por los límites del contenedor   |
//+------------------------------------------------------------------+
void CScrollBarH::SetTrimmered(const bool flag)
  {
   this.m_trim_flag=flag;
   if(this.m_butt_left!=NULL)
      this.m_butt_left.SetTrimmered(flag);
   if(this.m_butt_right!=NULL)
      this.m_butt_right.SetTrimmered(flag);
   if(this.m_thumb!=NULL)
      this.m_thumb.SetTrimmered(flag);
  }

Para cada uno de los objetos que componen la barra de desplazamiento, se establece el indicador pasado al método.

En el método de inicialización del objeto, ya no es necesario establecer el indicador de recorte por los límites del contenedor para cada uno de los elementos, sino que basta con llamar al método SetTrimmered() al final del método:

//+------------------------------------------------------------------+
//| CScrollBarH::Inicialización                                      |
//+------------------------------------------------------------------+
void CScrollBarH::Init(void)
  {
//--- Inicialización de la clase padre
   CPanel::Init();
//--- Fondo: opaco
   this.SetAlphaBG(255);
//--- Ancho del borde y texto
   this.SetBorderWidth(0);
   this.SetText("");

//--- Creamos los botones de desplazamiento
   int w=this.Height();
   int h=this.Height();
   this.m_butt_left = this.InsertNewElement(ELEMENT_TYPE_BUTTON_ARROW_LEFT, "","ButtL",0,0,w,h);
   this.m_butt_right= this.InsertNewElement(ELEMENT_TYPE_BUTTON_ARROW_RIGHT,"","ButtR",this.Width()-w,0,w,h);
   if(this.m_butt_left==NULL || this.m_butt_right==NULL)
     {
      ::PrintFormat("%s: Init failed",__FUNCTION__);
      return;
     }
//--- Configuramos los colores y el aspecto del botón con la flecha hacia la izquierda
   this.m_butt_left.SetImageBound(1,1,w-2,h-4);
   this.m_butt_left.InitBackColors(this.m_butt_left.BackColorFocused());
   this.m_butt_left.ColorsToDefault();
   this.m_butt_left.InitBorderColors(this.BorderColor(),this.m_butt_left.BackColorFocused(),this.m_butt_left.BackColorPressed(),this.m_butt_left.BackColorBlocked());
   this.m_butt_left.ColorsToDefault();

//--- Configuramos los colores y el aspecto del botón con la flecha hacia la derecha
   this.m_butt_right.SetImageBound(1,1,w-2,h-4);
   this.m_butt_right.InitBackColors(this.m_butt_right.BackColorFocused());
   this.m_butt_right.ColorsToDefault();
   this.m_butt_right.InitBorderColors(this.BorderColor(),this.m_butt_right.BackColorFocused(),this.m_butt_right.BackColorPressed(),this.m_butt_right.BackColorBlocked());
   this.m_butt_right.ColorsToDefault();
 
//--- Creamos un control deslizante
   int tsz=this.Width()-w*2;
   this.m_thumb=this.InsertNewElement(ELEMENT_TYPE_SCROLLBAR_THUMB_H,"","ThumbH",w,1,tsz-w*4,h-2);
   if(this.m_thumb==NULL)
     {
      ::PrintFormat("%s: Init failed",__FUNCTION__);
      return;
     }
//--- Configuramos los colores del control deslizante y le establecemos el indicador de movilidad
   this.m_thumb.InitBackColors(this.m_thumb.BackColorFocused());
   this.m_thumb.ColorsToDefault();
   this.m_thumb.InitBorderColors(this.m_thumb.BackColor(),this.m_thumb.BackColorFocused(),this.m_thumb.BackColorPressed(),this.m_thumb.BackColorBlocked());
   this.m_thumb.ColorsToDefault();
   this.m_thumb.SetMovable(true);

//--- Prohibimos que el gráfico se redibuje de forma independiente
   this.m_thumb.SetChartRedrawFlag(false);
   
//--- Inicialmente, no se muestra en el contenedor ni se recorta por sus límites
   this.SetVisibleInContainer(false);
   this.SetTrimmered(false);
  }

Se han realizado exactamente las mismas modificaciones en la clase de la barra de desplazamiento vertical. Son totalmente idénticas a las que ya hemos analizado, así que no vamos a repetirlas.

En la clase del objeto contenedor, haremos que el método CheckElementSizes sea público:

//+------------------------------------------------------------------+
//| Clase Contenedor                                                 |
//+------------------------------------------------------------------+
class CContainer : public CPanel
  {
private:
   bool              m_visible_scrollbar_h;                    // Indicador de visibilidad de la barra de desplazamiento horizontal
   bool              m_visible_scrollbar_v;                    // Indicador de visibilidad de la barra de desplazamiento vertical
   int               m_init_border_size_top;                   // Tamaño inicial del borde superior
   int               m_init_border_size_bottom;                // Tamaño inicial del borde inferior
   int               m_init_border_size_left;                  // Tamaño inicial del borde izquierdo
   int               m_init_border_size_right;                 // Tamaño inicial del borde derecho
   
//--- Devuelve el tipo del elemento que ha enviado el evento
   ENUM_ELEMENT_TYPE GetEventElementType(const string name);
   
protected:
   CScrollBarH      *m_scrollbar_h;                            // Puntero a la barra de desplazamiento horizontal
   CScrollBarV      *m_scrollbar_v;                            // Puntero a la barra de desplazamiento vertical
 
//--- Procesador de arrastre de los bordes y las esquinas de un elemento
   virtual void      ResizeActionDragHandler(const int x, const int y);
   
public:
//--- Comprueba las dimensiones del elemento para mostrar las barras de desplazamiento
   void              CheckElementSizes(CElementBase *element);
protected:
//--- Calcula y devuelve el tamaño (1) del control deslizante, (2) el tamaño total y (3) el tamaño útil de la pista de la barra de desplazamiento horizontal
   int               ThumbSizeHorz(void);
   int               TrackLengthHorz(void)               const { return(this.m_scrollbar_h!=NULL ? this.m_scrollbar_h.TrackLength() : 0);       }
   int               TrackEffectiveLengthHorz(void)            { return(this.TrackLengthHorz()-this.ThumbSizeHorz());                           }

Este método se invoca desde los elementos adjuntos al contenedor y, para poder acceder a él, debe ser público.

Ya hemos analizado las principales modificaciones de las clases de la biblioteca. Quedaron fuera del alcance algunas modificaciones y correcciones menores. Los códigos completos se pueden consultar en los archivos adjuntos al artículo.

El modelo de tabla se compone de una celda de la tabla, una fila de la tabla y una lista de filas que, en conjunto, forman la tabla. Además de lo mencionado, la tabla cuenta también con un encabezado formado por los encabezados de las columnas de la tabla.

  • Una celda de la tabla es un elemento independiente que contiene el número de fila, el número de columna y el contenido;
  • Una fila de la tabla es una lista de celdas de la tabla y tiene su propio número;
  • Una tabla es una lista de filas que puede incluir un encabezado, que a su vez consiste en una lista de encabezados de columnas.

El componente Vista para mostrar los datos del modelo de tabla tendrá aproximadamente la misma estructura. El objeto de la tabla se construirá, por ejemplo, a partir de un array de datos y un array de encabezados de dichos datos. A continuación, al objeto creado de representación visual de la tabla (componente Vista) le pasaremos un puntero al modelo de la tabla (componente Modelo), y todos los datos se escribirán en las celdas de la tabla.

En esta fase se creará una tabla estática sencilla sin posibilidad de controlar su visualización con el cursor del ratón.


Clase de celda de la tabla (Vista)

La tabla está formada por una lista de filas. Las filas, a su vez, están formadas por objetos que cuentan con dos lienzos para dibujar el fondo y el primer plano. Las celdas de la tabla se disponen horizontalmente en sus respectivas filas. No tiene sentido dotar a las celdas de sus propios lienzos. Se puede simplemente «dividir» cada fila en áreas, de modo que cada área se corresponda con la ubicación de su celda. Y dibujar esas celdas en el lienzo del objeto de la fila de la tabla.

Precisamente con este fin, al principio creamos una clase intermedia que solo contiene el tamaño del objeto con sus coordenadas y un puntero al control en cuyo lienzo se realizará el dibujo. Y ese objeto será una celda de la tabla. A ella le pasaremos un puntero al objeto de la fila de la tabla para dibujar en sus lienzos.

En el archivo Controls.mqh, crearemos una nueva clase:

//+------------------------------------------------------------------+
//| Clase de representación visual de una celda de la tabla          |
//+------------------------------------------------------------------+
class CTableCellView : public CBoundedObj
  {
protected:
   CTableCell       *m_table_cell_model;                       // Puntero al modelo de celda
   CImagePainter    *m_painter;                                // Puntero al objeto de dibujo
   CTableRowView    *m_element_base;                           // Puntero al elemento base (fila de la tabla)
   CCanvas          *m_background;                             // Puntero al lienzo de fondo
   CCanvas          *m_foreground;                             // Puntero al lienzo de primer plano
   int               m_index;                                  // Índice en la lista de celdas
   ENUM_ANCHOR_POINT m_text_anchor;                            // Punto de anclaje del texto (alineación en la celda)
   int               m_text_x;                                 // Coordenada X del texto (desplazamiento respecto al borde izquierdo del área del objeto)
   int               m_text_y;                                 // Coordenada Y del texto (desplazamiento respecto al borde superior del área del objeto)
   ushort            m_text[];                                 // Texto
   
//--- Devuelve los desplazamientos de las coordenadas iniciales de dibujo en el lienzo con respecto al lienzo y a las coordenadas del elemento base
   int               CanvasOffsetX(void)     const { return(this.m_element_base.ObjectX()-this.m_element_base.X());  }
   int               CanvasOffsetY(void)     const { return(this.m_element_base.ObjectY()-this.m_element_base.Y());  }
   
//--- Devuelve la coordenada ajustada de un punto en el lienzo, teniendo en cuenta el desplazamiento del lienzo con respecto al elemento base
   int               AdjX(const int x)                            const { return(x-this.CanvasOffsetX());            }
   int               AdjY(const int y)                            const { return(y-this.CanvasOffsetY());            }

//--- Devuelve las coordenadas X e Y del texto en función del punto de anclaje
   bool              GetTextCoordsByAnchor(int &x, int&y);
   
public:
//--- (1) Establece, (2) devuelve el texto de la celda
   void              SetText(const string text)                         { ::StringToShortArray(text,this.m_text);    }
   string            Text(void)                                   const { return ::ShortArrayToString(this.m_text);  }
   
//--- Establece el identificador
   virtual void      SetID(const int id)                                { this.m_index=this.m_id=id;                 }
//--- (1) Establece, (2) devuelve el índice de la celda
   void              SetIndex(const int index)                          { this.SetID(index);                         }
   int               Index(void)                                  const { return this.m_index;                       }

//--- (1) Establece, (2) devuelve el desplazamiento del texto en el eje X
   void              SetTextShiftX(const int shift)                     { this.m_text_x=shift;                       }
   int               TextShiftX(void)                             const { return this.m_text_x;                      }
   
//--- (1) Establece, (2) devuelve el desplazamiento del texto en el eje Y
   void              SetTextShiftY(const int shift)                     { this.m_text_y=shift;                       }
   int               TextShiftY(void)                             const { return this.m_text_y;                      }
   
//--- (1) Establece, (2) devuelve el punto de anclaje del texto
   void              SetTextAnchor(const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw);
   int               TextAnchor(void)                             const { return this.m_text_anchor;                 }
   
//--- Establece el punto de anclaje y los desplazamientos del texto
   void              SetTextPosition(const ENUM_ANCHOR_POINT anchor,const int shift_x,const int shift_y,const bool cell_redraw,const bool chart_redraw);

//--- Asigna el elemento base (fila de la tabla)
   void              RowAssign(CTableRowView *base_element);
   
//--- (1) Asigna, (2) devuelve el modelo de celda
   bool              TableCellModelAssign(CTableCell *cell_model,int dx,int dy,int w,int h);
   CTableCell       *GetTableCellModel(void)                            { return this.m_table_cell_model;            }

//--- Imprime en el registro el modelo de celda asignado
   void              TableCellModelPrint(void);
   
//--- (1) Rellena el objeto con el color de fondo, (2) actualiza el objeto para mostrar los cambios, (3) dibuja la apariencia
   virtual void      Clear(const bool chart_redraw);
   virtual void      Update(const bool chart_redraw);
   virtual void      Draw(const bool chart_redraw);
   
//--- Muestra el texto
   virtual void      DrawText(const int dx, const int dy, const string text, const bool chart_redraw);
   
//--- Métodos virtuales: (1) comparación, (2) guardado en un archivo, (3) carga desde un archivo, (4) tipo de objeto
   virtual int       Compare(const CObject *node,const int mode=0)const { return CBaseObj::Compare(node,mode);       }
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                                   const { return(ELEMENT_TYPE_TABLE_CELL);           }
  
//--- Inicialización del objeto de la clase
   void              Init(const string text);
   
//--- Devuelve la descripción del objeto
   virtual string    Description(void);
   
//--- Constructores/destructor
                     CTableCellView(void);
                     CTableCellView(const int id, const string user_name, const string text, const int x, const int y, const int w, const int h);
                    ~CTableCellView (void){}
  };

Todas las variables y métodos están comentados en el código. El índice de la celda y su identificador son lo mismo en este caso. Por eso se ha redefinido el método virtual SetID(), en el que se asignan los mismos valores al índice y al identificador. Veamos la implementación de los métodos declarados.

En los constructores de la clase se invoca el método de inicialización del objeto y se establecen el identificador de la celda y su nombre:

//+------------------------------------------------------------------+
//| CTableCellView::Constructor por defecto. Construye un objeto en la ventana principal|
//| del gráfico actual, en las coordenadas 0,0 y con las dimensiones predeterminadas |
//+------------------------------------------------------------------+
CTableCellView::CTableCellView(void) : CBoundedObj("TableCell",-1,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(-1),m_text_anchor(ANCHOR_LEFT)
  {
//--- Inicialización
   this.Init("");
   this.SetID(-1);
   this.SetName("TableCell");
  }
//+------------------------------------------------------------------+
//| CTableCellView::Constructor paramétrico. Construye un objeto     |
//| en la ventana indicada del gráfico indicado, con el texto indicado,        |
//| las coordenadas y las dimensiones                                |
//+------------------------------------------------------------------+
CTableCellView::CTableCellView(const int id, const string user_name, const string text, const int x, const int y, const int w, const int h) :
   CBoundedObj(user_name,id,x,y,w,h), m_index(-1),m_text_anchor(ANCHOR_LEFT)
  {
//--- Inicialización
   this.Init(text);
   this.SetID(id);
   this.SetName(user_name);
  }

Método de inicialización del objeto de la clase:

//+------------------------------------------------------------------+
//| CTableCellView::Inicialización                                   |
//+------------------------------------------------------------------+
void CTableCellView::Init(const string text)
  {
//--- La clase no gestiona los lienzos
   this.m_canvas_owner=false;
//--- Texto de la celda
   this.SetText(text);
//--- Desplazamientos predeterminados del texto
   this.m_text_x=2;
   this.m_text_y=0;
  }

Es imprescindible establecer en el objeto el indicador de que no gestiona los lienzos para que, al destruirse el objeto, no elimine lienzos ajenos.

Método que devuelve la descripción de un objeto:

//+------------------------------------------------------------------+
//| CTableCellView::Devuelve la descripción del objeto               |
//+------------------------------------------------------------------+
string CTableCellView::Description(void)
  {
   string nm=this.Name();
   string name=(nm!="" ? ::StringFormat(" \"%s\"",nm) : nm);
   return ::StringFormat("%s%s ID %d, X %d, Y %d, W %d, H %d",ElementDescription((ENUM_ELEMENT_TYPE)this.Type()),name,this.ID(),this.X(),this.Y(),this.Width(),this.Height());
  }

El método muestra en el registro una línea con el nombre del tipo de objeto, el identificador, las coordenadas y las dimensiones de la celda.

Método que asigna a una celda una fila de la tabla y los lienzos de fondo y de primer plano:

//+------------------------------------------------------------------+
//| CTableCellView::Asigna fila y lienzos de fondo y de primer plano |
//+------------------------------------------------------------------+
void CTableCellView::RowAssign(CTableRowView *base_element)
  {
   if(base_element==NULL)
     {
      ::PrintFormat("%s: Error. Empty element passed",__FUNCTION__);
      return;
     }
   this.m_element_base=base_element;
   this.m_background=this.m_element_base.GetBackground();
   this.m_foreground=this.m_element_base.GetForeground();
   this.m_painter=this.m_element_base.Painter();
  }

Al método se le pasa un puntero a un objeto de fila de la tabla (la descripción de la clase aparecerá más adelante) y, si el puntero no es válido, se informa de ello y el método finaliza. A continuación, se guardan en las variables de la clase el puntero a la fila de la tabla y, desde el objeto de la fila de la tabla, los punteros a los lienzos de fondo y de primer plano, así como al objeto de dibujo.

El objeto del modelo de tabla también tiene sus propias filas y celdas. A esta clase se le asigna un modelo de celda de la tabla, y esta dibuja en los lienzos del objeto de la fila los datos del modelo de celda.

Método que asigna un modelo de celda:

//+------------------------------------------------------------------+
//| CTableCellView::Asigna el modelo de celda                        |
//+------------------------------------------------------------------+
bool CTableCellView::TableCellModelAssign(CTableCell *cell_model,int dx,int dy,int w,int h)
  {
//--- Si se pasa un objeto del modelo de celda no válido, se informa de ello y se devuelve false
   if(cell_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- Si el elemento base (fila de la tabla) no está asignado, se informa de ello y se devuelve false
   if(this.m_element_base==NULL)
     {
      ::PrintFormat("%s: Error. Base element not assigned. Please use RowAssign() method first",__FUNCTION__);
      return false;
     }
//--- Guardamos el modelo de celda
   this.m_table_cell_model=cell_model;
//--- Establecemos las coordenadas y las dimensiones de la representación visual de la celda
   this.BoundSetXY(dx,dy);
   this.BoundResize(w,h);
//--- Establecemos las dimensiones del área de dibujo de la representación visual de la celda
   this.m_painter.SetBound(dx,dy,w,h);
//--- Todo correcto
   return true;
  }

En primer lugar, se comprueba la validez del puntero pasado a un modelo de celda; a continuación, se comprueba que la fila de la tabla ya esté asignada a este objeto de celda. Si alguna de estas condiciones no se cumple, se informa de ello en el registro y se devuelve false. A continuación, el puntero al modelo de celda se guarda en una variable y se establecen las coordenadas y las dimensiones del área de la celda en la fila que le corresponde.

Al ordenar las filas y columnas de la tabla, simplemente se reasignará el puntero al objeto de la celda, y este objeto, que tiene sus coordenadas exactas en la tabla, dibujará los datos de la nueva celda que se le haya asignado.

Método que devuelve las coordenadas X e Y del texto en función del punto de anclaje:

//+------------------------------------------------------------------+
//| CTableCellView::Devuelve las coordenadas X e Y del texto         |
//| en función del punto de anclaje                                  |
//+------------------------------------------------------------------+
bool CTableCellView::GetTextCoordsByAnchor(int &x,int &y)
  {
//--- Obtenemos las dimensiones del texto en la celda
   int text_w=0, text_h=0;
   this.m_foreground.TextSize(this.Text(),text_w,text_h);
   if(text_w==0 || text_h==0)
      return false;
//--- Dependiendo del punto de anclaje del texto en la celda
//--- calculamos sus coordenadas iniciales (esquina superior izquierda)
   switch(this.m_text_anchor)
     {
      //--- Punto de anclaje a la izquierda, centrado
      case ANCHOR_LEFT :
        x=0;
        y=(this.Height()-text_h)/2;
        break;
      //--- Punto de anclaje en la esquina inferior izquierda
      case ANCHOR_LEFT_LOWER :
        x=0;
        y=this.Height()-text_h;
        break;
      //--- Punto de anclaje abajo, centrado
      case ANCHOR_LOWER :
        x=(this.Width()-text_w)/2;
        y=this.Height()-text_h;
        break;
      //--- Punto de anclaje en la esquina inferior derecha
      case ANCHOR_RIGHT_LOWER :
        x=this.Width()-text_w;
        y=this.Height()-text_h;
        break;
      //--- Punto de anclaje a la derecha, centrado
      case ANCHOR_RIGHT :
        x=this.Width()-text_w;
        y=(this.Height()-text_h)/2;
        break;
      //--- Punto de anclaje en la esquina superior derecha
      case ANCHOR_RIGHT_UPPER :
        x=this.Width()-text_w;
        y=0;
        break;
      //--- Punto de anclaje arriba, centrado
      case ANCHOR_UPPER :
        x=(this.Width()-text_w)/2;
        y=0;
        break;
      //--- Punto de anclaje estrictamente en el centro del objeto
      case ANCHOR_CENTER :
        x=(this.Width()-text_w)/2;
        y=(this.Height()-text_h)/2;
        break;
      //--- Punto de anclaje en la esquina superior izquierda
      //---ANCHOR_LEFT_UPPER
      default:
        x=0;
        y=0;
        break;
     }
   return true;
  }

Al método se le pasan por referencia las variables en las que se almacenarán las coordenadas calculadas de la esquina superior izquierda del texto que se va a mostrar. Si no se pueden obtener las dimensiones del texto de la celda, el método devuelve false.

Tras calcular las coordenadas de la esquina superior izquierda, las coordenadas calculadas del texto se escriben en las variables y se devuelve true.

Método que establece el punto de anclaje del texto:

//+------------------------------------------------------------------+
//| CTableCellView::Establece el punto de anclaje del texto          |
//+------------------------------------------------------------------+
void CTableCellView::SetTextAnchor(const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw)
  {
   if(this.m_text_anchor==anchor)
      return;
   this.m_text_anchor=anchor;
   if(cell_redraw)
      this.Draw(chart_redraw);
  }

En primer lugar, se asigna el nuevo punto de anclaje a la variable; a continuación, si está activado el indicador de redibujado de esta celda, se invoca su método de dibujo con el indicador especificado de redibujado del gráfico.

Método que establece el punto de anclaje y los desplazamientos del texto:

//+------------------------------------------------------------------+
//| CTableCellView::Establece el punto de anclaje y los desplazamientos textuales |
//+------------------------------------------------------------------+
void CTableCellView::SetTextPosition(const ENUM_ANCHOR_POINT anchor,const int shift_x,const int shift_y,const bool cell_redraw,const bool chart_redraw)
  {
   this.SetTextShiftX(shift_x);
   this.SetTextShiftY(shift_y);
   this.SetTextAnchor(anchor,cell_redraw,chart_redraw);
  }

En este método, además de establecer el punto de anclaje, se definen los desplazamientos iniciales del texto en los ejes X e Y.

Método que rellena un objeto con un color:

//+------------------------------------------------------------------+
//| CTableCellView::Rellena el objeto con un color                   |
//+------------------------------------------------------------------+
void CTableCellView::Clear(const bool chart_redraw)
  {
//--- Establecemos las coordenadas correctas de las esquinas de la celda
   int x1=this.AdjX(this.m_bound.X());
   int y1=this.AdjY(this.m_bound.Y());
   int x2=this.AdjX(this.m_bound.Right());
   int y2=this.AdjY(this.m_bound.Bottom());
//--- Borramos el fondo y el primer plano dentro del área rectangular que ocupa la celda
   if(this.m_background!=NULL)
      this.m_background.FillRectangle(x1,y1,x2,y2-1,::ColorToARGB(this.m_element_base.BackColor(),this.m_element_base.AlphaBG()));
   if(this.m_foreground!=NULL)
      this.m_foreground.FillRectangle(x1,y1,x2,y2-1,clrNULL);
  }

Obtenemos las coordenadas correctas de las cuatro esquinas del área rectangular y, a continuación, rellenamos el fondo con el color de fondo de la fila de la tabla, y el primer plano con un color transparente.

Método que actualiza el objeto para mostrar los cambios:

//+------------------------------------------------------------------+
//| CTableCellView::Actualiza el objeto para mostrar los cambios     |
//+------------------------------------------------------------------+
void CTableCellView::Update(const bool chart_redraw)
  {
   if(this.m_background!=NULL)
      this.m_background.Update(false);
   if(this.m_foreground!=NULL)
      this.m_foreground.Update(chart_redraw);
  }

Si los lienzos de fondo y de primer plano son válidos, se invocan sus métodos de actualización con el indicador especificado de redibujado del gráfico.

Método que dibuja el aspecto de la celda:

//+------------------------------------------------------------------+
//| CTableCellView::Dibuja el aspecto                                |
//+------------------------------------------------------------------+
void CTableCellView::Draw(const bool chart_redraw)
  {
//--- Obtenemos las coordenadas del texto en función del punto de anclaje
   int text_x=0, text_y=0;
   if(!this.GetTextCoordsByAnchor(text_x,text_y))
      return;
//--- Corregimos las coordenadas del texto
   int x=this.AdjX(this.X()+text_x);
   int y=this.AdjY(this.Y()+text_y);
   
//--- Establecemos las coordenadas de la línea divisoria
   int x1=this.AdjX(this.X());
   int x2=this.AdjX(this.X());
   int y1=this.AdjY(this.Y());
   int y2=this.AdjY(this.Bottom());
   
//--- Mostramos el texto en el lienzo de primer plano sin actualizar el gráfico
   this.DrawText(x+this.m_text_x,y+this.m_text_y,this.Text(),false);
   
//--- Si no se trata de la celda situada en el extremo derecho, dibujamos una línea divisoria vertical en el lado derecho de la celda.
   if(this.m_element_base!=NULL && this.Index()<this.m_element_base.CellsTotal()-1)
     {
      int line_x=this.AdjX(this.Right());
      this.m_background.Line(line_x,y1,line_x,y2,::ColorToARGB(this.m_element_base.BorderColor(),this.m_element_base.AlphaBG()));
     }
//--- Actualizamos el lienzo de fondo con el indicador especificado de redibujado del gráfico
   this.m_background.Update(chart_redraw);
  }

La lógica del método se explica en los comentarios. La corrección de las coordenadas es necesaria cuando el objeto queda recortado por los límites del contenedor; en este caso, las coordenadas iniciales no se calculan a partir de las coordenadas reales del objeto gráfico del lienzo, sino a partir de las coordenadas del elemento gráfico, que se sitúan virtualmente fuera de los límites del contenedor.

Las líneas divisorias se dibujan únicamente en el lado derecho de la celda y, cuando no se trata de la celda situada en el extremo derecho, esta queda delimitada por su contenedor, por lo que no conviene dibujar allí una línea adicional.

Método que muestra el texto de una celda:

//+------------------------------------------------------------------+
//| CTableCellView::Muestra el texto                                 |
//+------------------------------------------------------------------+
void CTableCellView::DrawText(const int dx,const int dy,const string text,const bool chart_redraw)
  {
//--- Comprobamos el elemento base
   if(this.m_element_base==NULL)
      return;
      
//--- Limpiamos la celda y establecemos el texto
   this.Clear(false);
   this.SetText(text);
   
//--- Mostramos el texto establecido en el lienzo de primer plano
   this.m_foreground.TextOut(dx,dy,this.Text(),::ColorToARGB(this.m_element_base.ForeColor(),this.m_element_base.AlphaFG()));
   
//--- Si el texto sobresale por el límite derecho del área de la celda
   if(this.Right()-dx<this.m_foreground.TextWidth(text))
     {
      //--- Obtenemos las dimensiones del texto «puntos suspensivos»
      int w=0,h=0;
      this.m_foreground.TextSize("... ",w,h);
      if(w>0 && h>0)
        {
         //--- Borramos el texto junto al límite derecho del objeto según el tamaño del texto «puntos suspensivos» y sustituimos por puntos suspensivos el final del texto de la etiqueta
         this.m_foreground.FillRectangle(this.AdjX(this.Right())-w,this.AdjY(this.Y()),this.AdjX(this.Right()),this.AdjY(this.Y())+h,clrNULL);
         this.m_foreground.TextOut(this.AdjX(this.Right())-w,this.AdjY(dy),"...",::ColorToARGB(this.m_element_base.ForeColor(),this.m_element_base.AlphaFG()));
        }
     }
//--- Actualizamos el lienzo de primer plano con el indicador especificado de redibujado del gráfico
   this.m_foreground.Update(chart_redraw);
  }

La lógica del método se explica detalladamente en los comentarios del código. En caso de que el texto mostrado sobrepase los límites del área de la celda, se recorta por los límites del área y su final se sustituye por puntos suspensivos («...»), lo que indica que no todo el texto cabe en el ancho de la celda.

Método que imprime en el registro el modelo de fila asignado:

//+------------------------------------------------------------------+
//| CTableCellView::Imprime en el registro el modelo de fila asignado|
//+------------------------------------------------------------------+
void CTableCellView::TableCellModelPrint(void)
  {
   if(this.m_table_cell_model!=NULL)
      this.m_table_cell_model.Print();
  }

Este método permite comprobar qué celda del componente Modelo está asignada a la celda del componente Vista.

Métodos para trabajar con archivos:

//+------------------------------------------------------------------+
//| CTableCellView::Guardado en un archivo                           |
//+------------------------------------------------------------------+
bool CTableCellView::Save(const int file_handle)
  {
//--- Guardamos los datos del objeto padre
   if(!CBaseObj::Save(file_handle))
      return false;
  
//--- Guardamos el número de celda
   if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE)
      return false;
//--- Guardamos el punto de anclaje del texto
   if(::FileWriteInteger(file_handle,this.m_text_anchor,INT_VALUE)!=INT_VALUE)
      return false;
//--- Guardamos la coordenada X del texto
   if(::FileWriteInteger(file_handle,this.m_text_x,INT_VALUE)!=INT_VALUE)
      return false;
//--- Guardamos la coordenada Y del texto
   if(::FileWriteInteger(file_handle,this.m_text_y,INT_VALUE)!=INT_VALUE)
      return false;
//--- Guardamos el texto
   if(::FileWriteArray(file_handle,this.m_text)!=sizeof(this.m_text))
      return false;
      
//--- Todo correcto
   return true;
  }
//+------------------------------------------------------------------+
//| CTableCellView::Carga desde un archivo                           |
//+------------------------------------------------------------------+
bool CTableCellView::Load(const int file_handle)
  {
//--- Cargamos los datos del objeto padre
   if(!CBaseObj::Load(file_handle))
      return false;
      
//--- Cargamos el número de celda
   this.m_id=this.m_index=::FileReadInteger(file_handle,INT_VALUE);
//--- Cargamos el punto de anclaje del texto
   this.m_text_anchor=(ENUM_ANCHOR_POINT)::FileReadInteger(file_handle,INT_VALUE);
//--- Cargamos la coordenada X del texto
   this.m_text_x=::FileReadInteger(file_handle,INT_VALUE);
//--- Cargamos la coordenada Y del texto
   this.m_text_y=::FileReadInteger(file_handle,INT_VALUE);
//--- Cargamos el texto
   if(::FileReadArray(file_handle,this.m_text)!=sizeof(this.m_text))
      return false;
   
//--- Todo correcto
   return true;
  }

Estos métodos permiten guardar en un archivo y cargar desde un archivo los datos de la celda de la tabla.

Ahora veamos la clase de representación visual de una fila de la tabla.


Clase de fila de la tabla (Vista)

Una fila de la tabla es un objeto heredado de la clase de panel. Contiene una lista de objetos de celda de la clase CTableCellView.

Sigamos escribiendo el código en el archivo Controls.mqh:

//+------------------------------------------------------------------+
//| Clase de representación visual de una fila de la tabla           |
//+------------------------------------------------------------------+
class CTableRowView : public CPanel
  {
protected:
   CTableCellView    m_temp_cell;                                    // Objeto temporal de celda para la búsqueda
   CTableRow        *m_table_row_model;                              // Puntero al modelo de fila
   CListElm          m_list_cells;                                   // Lista de celdas
   int               m_index;                                        // Índice en la lista de filas
//--- Crea un nuevo objeto de representación visual de celda y lo añade a la lista
   CTableCellView   *InsertNewCellView(const int index,const string text,const int dx,const int dy,const int w,const int h);
   
public:
//--- Devuelve (1) una lista y (2) el número de celdas
   CListElm         *GetListCells(void)                                 { return &this.m_list_cells;        }
   int               CellsTotal(void)                             const { return this.m_list_cells.Total(); }
   
//--- Establece el identificador
   virtual void      SetID(const int id)                                { this.m_index=this.m_id=id;        }
//--- (1) Establece, (2) devuelve el índice de la fila
   void              SetIndex(const int index)                          { this.SetID(index);                }
   int               Index(void)                                  const { return this.m_index;              }

//--- (1) Establece, (2) devuelve el modelo de fila
   bool              TableRowModelAssign(CTableRow *row_model);
   CTableRow        *GetTableRowModel(void)                             { return this.m_table_row_model;    }

//--- Imprime en el registro el modelo de fila asignado
   void              TableRowModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS);
   
//--- Dibuja el aspecto externo
   virtual void      Draw(const bool chart_redraw);
   
//--- Métodos virtuales: (1) comparación, (2) guardado en un archivo, (3) carga desde un archivo, (4) tipo de objeto
   virtual int       Compare(const CObject *node,const int mode=0)const { return CLabel::Compare(node,mode);}
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                                   const { return(ELEMENT_TYPE_TABLE_ROW);   }
  
//--- Inicialización (1) del objeto de la clase y (2) de los colores predeterminados del objeto
   void              Init(void);
   virtual void      InitColors(void);

//--- Constructores/destructor
                     CTableRowView(void);
                     CTableRowView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CTableRowView (void){}
  };

En una fila de la tabla, al igual que en una celda de la tabla, la propiedad «índice de fila» es igual a su identificador. Aquí también se ha redefinido el método virtual de establecimiento del identificador, que establece simultáneamente ambas propiedades: el índice y el identificador.

En los constructores de la clase se invoca el método de inicialización del objeto:

//+------------------------------------------------------------------+
//| CTableRowView::Constructor por defecto. Construye un objeto en la ventana principal |
//| del gráfico actual, en las coordenadas 0,0 y con las dimensiones predeterminadas |
//+------------------------------------------------------------------+
CTableRowView::CTableRowView(void) : CPanel("TableRow","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(-1)
  {
//--- Inicialización
   this.Init();
  }
//+------------------------------------------------------------------+
//| CTableRowView::Constructor con parámetros. Construye un objeto en   |
//| en la ventana indicada del gráfico indicado, con el texto indicado,          |
//| las coordenadas y las dimensiones                                |
//+------------------------------------------------------------------+
CTableRowView::CTableRowView(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) :
   CPanel(object_name,text,chart_id,wnd,x,y,w,h), m_index(-1)
  {
//--- Inicialización
   this.Init();
  }

Inicialización del objeto:

//+------------------------------------------------------------------+
//| CTableRowView::Inicialización                                    |
//+------------------------------------------------------------------+
void CTableRowView::Init(void)
  {
//--- Inicialización del objeto padre
   CPanel::Init();
//--- Fondo: opaco
   this.SetAlphaBG(255);
//--- Ancho del borde
   this.SetBorderWidth(1);
  }

Método para inicializar los colores predeterminados del objeto:

//+------------------------------------------------------------------+
//| CTableRowView::Inicialización de los colores predeterminados del objeto         |
//+------------------------------------------------------------------+
void CTableRowView::InitColors(void)
  {
//--- Inicializamos los colores de fondo para los estados normal y activado, y establecemos el color de fondo actual
   this.InitBackColors(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke);
   this.InitBackColorsAct(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke);
   this.BackColorToDefault();
   
//--- Inicializamos los colores de primer plano para los estados normal y activado, y establecemos el color de texto actual
   this.InitForeColors(clrBlack,clrBlack,clrBlack,clrSilver);
   this.InitForeColorsAct(clrBlack,clrBlack,clrBlack,clrSilver);
   this.ForeColorToDefault();
   
//--- Inicializamos los colores del borde para los estados normal y activado, y establecemos el color actual del borde
   this.InitBorderColors(C'200,200,200',C'200,200,200',C'200,200,200',clrSilver);
   this.InitBorderColorsAct(C'200,200,200',C'200,200,200',C'200,200,200',clrSilver);
   this.BorderColorToDefault();
   
//--- Inicializamos el color del borde y el color de primer plano para el elemento bloqueado
   this.InitBorderColorBlocked(clrSilver);
   this.InitForeColorBlocked(clrSilver);
  }

Método que crea y añade a la lista un nuevo objeto de representación visual de la celda:

//+------------------------------------------------------------------+
//| CTableRowView::Crea y añade a la lista                           |
//| nuevo objeto de representación visual de la celda                |
//+------------------------------------------------------------------+
CTableCellView *CTableRowView::InsertNewCellView(const int index,const string text,const int dx,const int dy,const int w,const int h)
  {
//--- Comprobamos si hay algún objeto en la lista con el identificador indicado y, si lo hay, lo indicamos y devolvemos NULL
   this.m_temp_cell.SetIndex(index);
//--- Establecemos en la lista el indicador de ordenación por identificador
   this.m_list_cells.Sort(ELEMENT_SORT_BY_ID);
   if(this.m_list_cells.Search(&this.m_temp_cell)!=NULL)
     {
      ::PrintFormat("%s: Error. The TableCellView object with index %d is already in the list",__FUNCTION__,index);
      return NULL;
     }
//--- Creamos el nombre y el identificador del objeto de celda
   string name="TableCellView"+(string)this.Index()+"x"+(string)index;
   int id=this.m_list_cells.Total();
//--- Creamos un nuevo objeto TableCellView; si falla, lo notificamos y devolvemos NULL
   CTableCellView *cell_view=new CTableCellView(id,name,""+text,dx,dy,w,h);
   if(cell_view==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create CTableCellView object",__FUNCTION__);
      return NULL;
     }
//--- Si no se ha podido añadir el nuevo objeto a la lista, lo notificamos, eliminamos el objeto y devolvemos NULL
   if(this.m_list_cells.Add(cell_view)==-1)
     {
      ::PrintFormat("%s: Error. Failed to add CTableCellView object to list",__FUNCTION__);
      delete cell_view;
      return NULL;
     }
//--- Asignamos el elemento base y devolvemos un puntero al objeto
   cell_view.RowAssign(this.GetObject());
   return cell_view;
  }

Aunque cada celda se asigna a su propia área del objeto de la fila de la tabla, de todos modos es necesario almacenar en una lista los objetos creados mediante el operador new, o bien realizar un seguimiento de ellos por cuenta propia y eliminarlos cuando sea necesario.

Es más fácil guardarlos en una lista; así, el subsistema del terminal se encargará de controlar cuándo hay que eliminarlos. El método crea un nuevo objeto de representación visual de la celda y lo añade a la lista de celdas de la fila. Una vez creado el objeto, se le asigna la fila en la que está ubicado y en cuyos lienzos el objeto creado dibujará los datos del modelo de celda de la tabla.

Método que establece el modelo de fila:

//+------------------------------------------------------------------+
//| CTableRowView::Establece el modelo de fila                       |
//+------------------------------------------------------------------+
bool CTableRowView::TableRowModelAssign(CTableRow *row_model)
  {
//--- Si se pasa un objeto vacío, lo indicamos y devolvemos false
   if(row_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- Si en el modelo de fila pasado no hay ninguna celda, lo indicamos y devolvemos false
   int total=(int)row_model.CellsTotal();
   if(total==0)
     {
      ::PrintFormat("%s: Error. Row model does not contain any cells",__FUNCTION__);
      return false;
     }
//--- Guardamos un puntero al modelo de fila pasado y calculamos la anchura de la celda
   this.m_table_row_model=row_model;
   int cell_w=(int)::round((double)this.Width()/(double)total);
   
//--- En un bucle que recorre el número de celdas del modelo de fila
   for(int i=0;i<total;i++)
     {
      //--- obtenemos el modelo de la celda siguiente,
      CTableCell *cell_model=this.m_table_row_model.GetCell(i);
      if(cell_model==NULL)
         return false;
      //--- Calculamos la coordenada y creamos un nombre para el área de la celda
      int x=cell_w*i;
      string name="CellBound"+(string)this.m_table_row_model.Index()+"x"+(string)i;
      //--- Creamos una nueva área de la celda
      CBound *cell_bound=this.InsertNewBound(name,x,0,cell_w,this.Height());
      if(cell_bound==NULL)
         return false;
      //--- Creamos un nuevo objeto de representación visual de la celda
      CTableCellView *cell_view=this.InsertNewCellView(i,cell_model.Value(),x,0,cell_w,this.Height());
      if(cell_view==NULL)
         return false;
      //--- Asignamos al área actual de la celda el objeto correspondiente de representación visual de la celda
      cell_bound.AssignObject(cell_view);
     }
//--- Todo correcto
   return true;
  }

Al método se le pasa un puntero al modelo de fila y, en un bucle que recorre el número de celdas del modelo, se crean nuevas áreas de ubicación para los objetos de representación visual de las celdas y las propias celdas (clase CTableCellView). Y cada nueva celda de este tipo se asigna a cada área nueva. Como resultado, obtenemos una lista de las áreas de las celdas a las que se han asignado punteros a las celdas correspondientes de la tabla.

Método que dibuja el aspecto de la fila:

//+------------------------------------------------------------------+
//| CTableRowView::Dibuja el aspecto                                 |
//+------------------------------------------------------------------+
void CTableRowView::Draw(const bool chart_redraw)
  {
//--- Rellenamos el objeto con el color de fondo, dibujamos la línea de la fila y actualizamos el lienzo de fondo
   this.Fill(this.BackColor(),false);
   this.m_background.Line(this.AdjX(0),this.AdjY(this.Height()-1),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG()));
  
//--- Dibujamos las celdas de la fila
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Obtenemos el área de la siguiente celda
      CBound *cell_bound=this.GetBoundAt(i);
      if(cell_bound==NULL)
         continue;
      
      //--- Desde el área de la celda obtenemos el objeto de celda adjunto
      CTableCellView *cell_view=cell_bound.GetAssignedObj();
      //--- Dibujamos la representación visual de la celda
      if(cell_view!=NULL)
         cell_view.Draw(false);
     }
//--- Actualizamos los lienzos de fondo y de primer plano con el indicador de redibujado del gráfico especificado
   this.Update(chart_redraw);
  }

Primero, la fila se rellena con el color de fondo y, a continuación, se dibuja una línea en la parte inferior. A continuación, en el bucle que recorre las áreas de las celdas de la fila, obtenemos el área de la siguiente celda; a partir de ella, obtenemos un puntero al objeto de la celda y llamamos a su método de dibujo.

Método que imprime en el registro el modelo de fila asignado:

//+------------------------------------------------------------------+
//| CTableRowView::Imprime en el registro el modelo de fila asignado |
//+------------------------------------------------------------------+
void CTableRowView::TableRowModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS)
  {
   if(this.m_table_row_model!=NULL)
      this.m_table_row_model.Print(detail,as_table,cell_width);
  }

Este método permite comprobar qué modelo de fila del modelo de tabla se ha asignado a este objeto de representación visual de la fila.

Métodos para trabajar con archivos:

//+------------------------------------------------------------------+
//| CTableRowView::Guardado en archivo                               |
//+------------------------------------------------------------------+
bool CTableRowView::Save(const int file_handle)
  {
//--- Guardamos los datos del objeto padre
   if(!CPanel::Save(file_handle))
      return false;

//--- Guardamos la lista de celdas
   if(!this.m_list_cells.Save(file_handle))
      return false;
//--- Guardamos el número de fila
   if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE)
      return false;
   
//--- Todo correcto
   return true;
  }
//+------------------------------------------------------------------+
//| CTableRowView::Carga desde archivo                               |
//+------------------------------------------------------------------+
bool CTableRowView::Load(const int file_handle)
  {
//--- Cargamos los datos del objeto padre
   if(!CPanel::Load(file_handle))
      return false;
     
//--- Cargamos la lista de celdas
   if(!this.m_list_cells.Load(file_handle))
      return false;
//--- Cargamos el número de fila
   this.m_id=this.m_index=(uchar)::FileReadInteger(file_handle,INT_VALUE);
   
//--- Todo correcto
   return true;
  }

Estos métodos permiten guardar en un archivo y cargar desde un archivo los datos de la fila de la tabla.

Cada tabla debe tener un encabezado. Permite comprender qué datos se muestran en las columnas de la tabla. El encabezado de la tabla es una lista de encabezados de columna. Cada columna de la tabla tiene su propio encabezado, que indica el tipo y el nombre de los datos ubicados en la columna.


Clase del encabezado de columna de la tabla (Vista)

El encabezado de una columna de la tabla es un objeto independiente, derivado del objeto «Botón» (CButton), que hereda el comportamiento de este objeto y lo complementa. En la implementación actual no se añadirán nuevas propiedades al botón; simplemente cambiaremos los colores de los distintos estados. En el siguiente artículo añadiremos al encabezado una indicación de la dirección de ordenación: flechas hacia arriba y hacia abajo en el lado derecho del área del botón, así como un modelo de eventos para iniciar la ordenación al pulsar el botón.

Sigamos escribiendo el código en el archivo Controls.mqh:

//+------------------------------------------------------------------+
//| Clase de representación visual del encabezado de una columna de la tabla        |
//+------------------------------------------------------------------+
class CColumnCaptionView : public CButton
  {
protected:
   CColumnCaption   *m_column_caption_model;                         // Puntero al modelo del encabezado de columna
   int               m_index;                                        // Índice en la lista de columnas
   
public:
//--- Establece el identificador
   virtual void      SetID(const int id)                                { this.m_index=this.m_id=id;                 }
//--- (1) Establece, (2) devuelve el índice de la celda
   void              SetIndex(const int index)                          { this.SetID(index);                         }
   int               Index(void)                                  const { return this.m_index;                       }
   
//--- (1) Asigna, (2) devuelve el modelo del encabezado de columna
   bool              ColumnCaptionModelAssign(CColumnCaption *caption_model);
   CColumnCaption   *ColumnCaptionModel(void)                           { return this.m_column_caption_model;        }

//--- Muestra en el registro el modelo de encabezado de columna asignado
   void              ColumnCaptionModelPrint(void);
 
//--- Dibuja el aspecto externo
   virtual void      Draw(const bool chart_redraw);
   
//--- Métodos virtuales: (1) comparación, (2) guardado en un archivo, (3) carga desde un archivo, (4) tipo de objeto
   virtual int       Compare(const CObject *node,const int mode=0)const { return CButton::Compare(node,mode);        }
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                                   const { return(ELEMENT_TYPE_TABLE_COLUMN_CAPTION); }
  
//--- Inicialización (1) del objeto de la clase y (2) de los colores predeterminados del objeto
   void              Init(const string text);
   virtual void      InitColors(void);
   
//--- Devuelve la descripción del objeto
   virtual string    Description(void);
   
//--- Constructores/destructor
                     CColumnCaptionView(void);
                     CColumnCaptionView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); 
                    ~CColumnCaptionView (void){}
  };

El método tiene un índice igual al identificador, al igual que en las dos clases anteriores que hemos visto. Y el método para establecer el identificador es similar al de las clases anteriores.

En los constructores de la clase se invoca el método de inicialización del objeto y se establece un identificador nulo (que cambia tras la creación del objeto):

//+------------------------------------------------------------------+
//| CColumnCaptionView::Constructor predeterminado. Construye un objeto      |
//| en la ventana principal del gráfico actual, en las coordenadas 0,0                |
//| con las dimensiones predeterminadas                              |
//+------------------------------------------------------------------+
CColumnCaptionView::CColumnCaptionView(void) : CButton("ColumnCaption","Caption",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(0)
  {
//--- Inicialización
   this.Init("Caption");
   this.SetID(0);
   this.SetName("ColumnCaption");
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Constructor parametrizado.                   |
//| Construye un objeto en la ventana indicada del gráfico especificado con              |
//| el texto, las coordenadas y las dimensiones indicados            |
//+------------------------------------------------------------------+
CColumnCaptionView::CColumnCaptionView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h) :
   CButton(object_name,text,chart_id,wnd,x,y,w,h), m_index(0)
  {
//--- Inicialización
   this.Init(text);
   this.SetID(0);
  }

Método de inicialización del objeto:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Inicialización                               |
//+------------------------------------------------------------------+
void CColumnCaptionView::Init(const string text)
  {
//--- Desplazamientos predeterminados del texto
   this.m_text_x=4;
   this.m_text_y=2;
//--- Establecemos los colores de los distintos estados
   this.InitColors();
  }

Método para inicializar los colores predeterminados del objeto:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Inicialización de los colores predeterminados del objeto    |
//+------------------------------------------------------------------+
void CColumnCaptionView::InitColors(void)
  {
//--- Inicializamos los colores de fondo para los estados normal y activado, y establecemos el color de fondo actual
   this.InitBackColors(clrWhiteSmoke,this.GetBackColorControl().NewColor(clrWhiteSmoke,-6,-6,-6),clrWhiteSmoke,clrWhiteSmoke);
   this.InitBackColorsAct(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke);
   this.BackColorToDefault();
   
//--- Inicializamos los colores de primer plano para los estados normal y activado, y establecemos el color de texto actual
   this.InitForeColors(clrBlack,clrBlack,clrBlack,clrSilver);
   this.InitForeColorsAct(clrBlack,clrBlack,clrBlack,clrSilver);
   this.ForeColorToDefault();
   
//--- Inicializamos los colores del borde para los estados normal y activado, y establecemos el color actual del borde
   this.InitBorderColors(clrLightGray,clrLightGray,clrLightGray,clrLightGray);
   this.InitBorderColorsAct(clrLightGray,clrLightGray,clrLightGray,clrLightGray);
   this.BorderColorToDefault();
   
//--- Inicializamos el color del borde y el color de primer plano para el elemento bloqueado
   this.InitBorderColorBlocked(clrNULL);
   this.InitForeColorBlocked(clrSilver);
  }

Método que dibuja la apariencia del encabezado de una columna:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Dibuja la apariencia                         |
//+------------------------------------------------------------------+
void CColumnCaptionView::Draw(const bool chart_redraw)
  {
//--- Rellenamos el botón con el color de fondo, dibujamos una línea vertical clara a la izquierda y otra oscura a la derecha
   this.Fill(this.BackColor(),false);
   color clr_dark =this.BorderColor();                                                       // "Color oscuro"
   color clr_light=this.GetBackColorControl().NewColor(this.BorderColor(), 100, 100, 100);   // "Color claro"
   this.m_background.Line(this.AdjX(0),this.AdjY(0),this.AdjX(0),this.AdjY(this.Height()-1),::ColorToARGB(clr_light,this.AlphaBG()));                          // Línea a la izquierda
   this.m_background.Line(this.AdjX(this.Width()-1),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(clr_dark,this.AlphaBG())); // Línea a la derecha
//--- Actualizamos el lienzo de fondo
   this.m_background.Update(false);
   
//--- Mostramos el texto del encabezado
   CLabel::Draw(false);
      
//--- Si se indica, actualizamos el gráfico
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Primero se rellena el fondo con el color establecido y, a continuación, se dibujan dos líneas verticales: una clara a la izquierda y otra oscura a la derecha. A continuación, se muestra el texto del encabezado en el lienzo de primer plano.

Método que devuelve la descripción de un objeto:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Devuelve la descripción del objeto           |
//+------------------------------------------------------------------+
string CColumnCaptionView::Description(void)
  {
   string nm=this.Name();
   string name=(nm!="" ? ::StringFormat(" \"%s\"",nm) : nm);
   return ::StringFormat("%s%s ID %d, X %d, Y %d, W %d, H %d",ElementDescription((ENUM_ELEMENT_TYPE)this.Type()),name,this.ID(),this.X(),this.Y(),this.Width(),this.Height());
  }

El método devuelve una descripción del tipo de elemento con el identificador, las coordenadas y las dimensiones del objeto.

Método que asigna el modelo del encabezado de la columna:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Establece el modelo del encabezado de la columna           |
//+------------------------------------------------------------------+
bool CColumnCaptionView::ColumnCaptionModelAssign(CColumnCaption *caption_model)
  {
//--- Si se ha pasado un objeto no válido del modelo del encabezado de la columna, informamos de ello y devolvemos false
   if(caption_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- Guardamos el modelo del encabezado de la columna
   this.m_column_caption_model=caption_model;
//--- Establecemos las dimensiones del área de dibujo de la representación visual del encabezado de la columna
   this.m_painter.SetBound(0,0,this.Width(),this.Height());
//--- Todo correcto
   return true;
  }

Al método se le pasa un puntero al modelo del encabezado de la columna, que se guarda en una variable. A continuación, se establecen las coordenadas y las dimensiones del área de dibujo del objeto de dibujo.

Método que imprime en el registro el modelo del encabezado de la columna asignado:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Imprime en el registro                       |
//el modelo del encabezado de la columna asignado
//+------------------------------------------------------------------+
void CColumnCaptionView::ColumnCaptionModelPrint(void)
  {
   if(this.m_column_caption_model!=NULL)
      this.m_column_caption_model.Print();
  }

Permite comprobar la representación visual del encabezado imprimiendo en el registro la descripción del modelo del encabezado de la columna asignado al objeto.

Métodos para trabajar con archivos:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Guardado en un archivo                       |
//+------------------------------------------------------------------+
bool CColumnCaptionView::Save(const int file_handle)
  {
//--- Guardamos los datos del objeto padre
   if(!CButton::Save(file_handle))
      return false;
  
//--- Guardamos el número del encabezado
   if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE)
      return false;
      
//--- Todo correcto
   return true;
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Carga desde un archivo                       |
//+------------------------------------------------------------------+
bool CColumnCaptionView::Load(const int file_handle)
  {
//--- Cargamos los datos del objeto padre
   if(!CButton::Load(file_handle))
      return false;
      
//--- Cargamos el número del encabezado
   this.m_id=this.m_index=::FileReadInteger(file_handle,INT_VALUE);
   
//--- Todo correcto
   return true;
  }

Permiten guardar los parámetros del encabezado en un archivo y cargarlos desde él.


Clase del encabezado de la tabla (Vista)

El encabezado de la tabla es una lista convencional de objetos de encabezado de columna, basada en el control Panel (CPanel). Este objeto ofrece, en definitiva, herramientas para gestionar las columnas de la tabla.

En esta implementación, se tratará de un objeto estático normal. Todas las funciones de interacción con el usuario se añadirán más adelante.

Seguiremos escribiendo el código en el mismo archivo:

//+------------------------------------------------------------------+
//| Clase de la representación visual del encabezado de la tabla     |
//+------------------------------------------------------------------+
class CTableHeaderView : public CPanel
  {
protected:
   CColumnCaptionView m_temp_caption;                                // Objeto temporal del encabezado de la columna para la búsqueda
   CTableHeader     *m_table_header_model;                           // Puntero al modelo del encabezado de la tabla

//--- Crea y añade a la lista un nuevo objeto de representación visual del encabezado de la columna
   CColumnCaptionView *InsertNewColumnCaptionView(const string text, const int x, const int y, const int w, const int h);
   
public:
//--- (1) Establece, (2) devuelve el modelo del encabezado de la tabla
   bool              TableHeaderModelAssign(CTableHeader *header_model);
   CTableHeader     *GetTableHeaderModel(void)                          { return this.m_table_header_model;    }

//--- Imprime en el registro el modelo del encabezado de la tabla asignado
   void              TableHeaderModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS);
   
//--- Dibuja el aspecto externo
   virtual void      Draw(const bool chart_redraw);
   
//--- Métodos virtuales: (1) comparación, (2) guardado en un archivo, (3) carga desde un archivo, (4) tipo de objeto
   virtual int       Compare(const CObject *node,const int mode=0)const { return CPanel::Compare(node,mode);   }
   virtual bool      Save(const int file_handle)                        { return CPanel::Save(file_handle);    }
   virtual bool      Load(const int file_handle)                        { return CPanel::Load(file_handle);    }
   virtual int       Type(void)                                   const { return(ELEMENT_TYPE_TABLE_HEADER);   }
  
//--- Inicialización (1) del objeto de la clase y (2) de los colores predeterminados del objeto
   void              Init(void);
   virtual void      InitColors(void);

//--- Constructores/destructor
                     CTableHeaderView(void);
                     CTableHeaderView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CTableHeaderView (void){}
  };

Al objeto se le asigna el modelo del encabezado de la tabla y, a partir de su contenido, se crean y se añaden a la lista de elementos adjuntos los objetos de encabezado de columna.

En los constructores de la clase se invoca el método de inicialización del objeto:

//+------------------------------------------------------------------+
//| CTableHeaderView::Constructor por defecto. Construye un objeto en   |
//| en la ventana principal del gráfico actual, en las coordenadas 0,0                 |
//| con las dimensiones predeterminadas                              |
//+------------------------------------------------------------------+
CTableHeaderView::CTableHeaderView(void) : CPanel("TableHeader","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H)
  {
//--- Inicialización
   this.Init();
  }
//+------------------------------------------------------------------+
//| CTableHeaderView::Constructor con parámetros. Construye un objeto en   |
//| en la ventana indicada del gráfico indicado, con el texto indicado,          |
//| las coordenadas y las dimensiones                                |
//+------------------------------------------------------------------+
CTableHeaderView::CTableHeaderView(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) :
   CPanel(object_name,text,chart_id,wnd,x,y,w,h)
  {
//--- Inicialización
   this.Init();
  }

Método de inicialización del objeto:

//+------------------------------------------------------------------+
//| CTableHeaderView::Inicialización                                 |
//+------------------------------------------------------------------+
void CTableHeaderView::Init(void)
  {
//--- Inicialización del objeto padre
   CPanel::Init();
//--- Color de fondo: opaco
   this.SetAlphaBG(255);
//--- Ancho del borde
   this.SetBorderWidth(1);
  }

Método para inicializar los colores predeterminados del objeto:

//+------------------------------------------------------------------+
//| CTableHeaderView::Inicialización de los colores predeterminados del objeto      |
//+------------------------------------------------------------------+
void CTableHeaderView::InitColors(void)
  {
//--- Inicializamos los colores de fondo para los estados normal y activado, y establecemos el color de fondo actual
   this.InitBackColors(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke);
   this.InitBackColorsAct(clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke,clrWhiteSmoke);
   this.BackColorToDefault();
   
//--- Inicializamos los colores de primer plano para los estados normal y activado, y establecemos el color de texto actual
   this.InitForeColors(clrBlack,clrBlack,clrBlack,clrSilver);
   this.InitForeColorsAct(clrBlack,clrBlack,clrBlack,clrSilver);
   this.ForeColorToDefault();
   
//--- Inicializamos los colores del borde para los estados normal y activado, y establecemos el color actual del borde
   this.InitBorderColors(C'200,200,200',C'200,200,200',C'200,200,200',clrSilver);
   this.InitBorderColorsAct(C'200,200,200',C'200,200,200',C'200,200,200',clrSilver);
   this.BorderColorToDefault();
   
//--- Inicializamos el color del borde y el color de primer plano para el elemento bloqueado
   this.InitBorderColorBlocked(clrSilver);
   this.InitForeColorBlocked(clrSilver);
  }

Método que crea y añade a la lista un nuevo objeto de representación visual del encabezado de la columna:

//+------------------------------------------------------------------+
//| CTableHeaderView::Crea y añade a la lista                        |
//| nuevo objeto de representación visual del encabezado de la columna                     |
//+------------------------------------------------------------------+
CColumnCaptionView *CTableHeaderView::InsertNewColumnCaptionView(const string text,const int x,const int y,const int w,const int h)
  {
//--- Creamos el nombre del objeto y devolvemos el resultado de la creación de un nuevo encabezado de la columna
   string user_name="ColumnCaptionView"+(string)this.m_list_elm.Total();
   CColumnCaptionView *caption_view=this.InsertNewElement(ELEMENT_TYPE_TABLE_COLUMN_CAPTION,text,user_name,x,y,w,h);
   return(caption_view!=NULL ? caption_view : NULL);
  }

El objeto del encabezado de la columna se crea mediante el método InsertNewElement(), estándar para los objetos de la biblioteca, que coloca los objetos creados en la lista de elementos gráficos del objeto.

Método que establece el modelo del encabezado:

//+------------------------------------------------------------------+
//| CTableHeaderView::Establece el modelo del encabezado             |
//+------------------------------------------------------------------+
bool CTableHeaderView::TableHeaderModelAssign(CTableHeader *header_model)
  {
//--- Si se pasa un objeto vacío, lo indicamos y devolvemos false
   if(header_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- Si en el modelo de encabezado recibido no hay ningún encabezado de columna, lo indicamos y devolvemos false
   int total=(int)header_model.ColumnsTotal();
   if(total==0)
     {
      ::PrintFormat("%s: Error. Header model does not contain any columns",__FUNCTION__);
      return false;
     }
//--- Guardamos un puntero al modelo del encabezado de la tabla que se nos ha pasado y calculamos la anchura de cada encabezado de columna
   this.m_table_header_model=header_model;
   int caption_w=(int)::round((double)this.Width()/(double)total);
   
//--- En un bucle según el número de encabezados de columna en el modelo del encabezado de la tabla
   for(int i=0;i<total;i++)
     {
      //--- obtenemos el modelo del encabezado de la columna siguiente,
      CColumnCaption *caption_model=this.m_table_header_model.GetColumnCaption(i);
      if(caption_model==NULL)
         return false;
      //--- calculamos la coordenada y creamos un nombre para el área del encabezado de la columna
      int x=caption_w*i;
      string name="CaptionBound"+(string)i;
      //--- Creamos una nueva área del encabezado de la columna
      CBound *caption_bound=this.InsertNewBound(name,x,0,caption_w,this.Height());
      if(caption_bound==NULL)
         return false;
      //--- Creamos un nuevo objeto de representación visual del encabezado de la columna
      CColumnCaptionView *caption_view=this.InsertNewColumnCaptionView(caption_model.Value(),x,0,caption_w,this.Height());
      if(caption_view==NULL)
         return false;
      //--- Al área actual del encabezado de la columna le asignamos el objeto correspondiente de representación visual del encabezado de la columna
      caption_bound.AssignObject(caption_view);
     }
//--- Todo correcto
   return true;
  }

Al método se le pasa el modelo del encabezado de la tabla. En un bucle según el número de encabezados de columna del modelo, creamos una nueva área para colocar el encabezado de la columna. Creamos un objeto del encabezado de la columna y lo asignamos al área actual. Al finalizar el bucle, tendremos una lista de áreas con los encabezados de columna asignados a cada una de ellas.

Método que dibuja el aspecto del encabezado de la tabla:

//+------------------------------------------------------------------+
//| CTableHeaderView::Dibuja el aspecto                              |
//+------------------------------------------------------------------+
void CTableHeaderView::Draw(const bool chart_redraw)
  {
//--- Rellenamos el objeto con el color de fondo, dibujamos la línea de la fila y actualizamos el lienzo de fondo
   this.Fill(this.BackColor(),false);
   this.m_background.Line(this.AdjX(0),this.AdjY(this.Height()-1),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG()));
   this.m_background.Update(false);
   
//--- Dibujamos los encabezados de las columnas
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Obtenemos el área del encabezado de la columna siguiente
      CBound *cell_bound=this.GetBoundAt(i);
      if(cell_bound==NULL)
         continue;
      
      //--- Del área del encabezado de la columna obtenemos el objeto adjunto del encabezado de la columna
      CColumnCaptionView *caption_view=cell_bound.GetAssignedObj();
      //--- Dibujamos la representación visual del encabezado de la columna
      if(caption_view!=NULL)
         caption_view.Draw(false);
     }
//--- Si se indica, actualizamos el gráfico
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

En primer lugar, todo el área del encabezado de la tabla se rellena con el color de fondo y, a continuación, se dibuja una línea divisoria en la parte inferior. A continuación, en el bucle que recorre la lista de áreas de encabezados, obtenemos la siguiente área y, a partir de ella, el objeto del encabezado de la columna asignado a dicha área, cuyo método de dibujo invocamos.

Método que imprime en el registro el modelo del encabezado de la tabla asignado:

//+------------------------------------------------------------------+
//| CTableHeaderView::Imprime en el registro                         |
//| el modelo del encabezado de la tabla asignado                    |
//+------------------------------------------------------------------+
void CTableHeaderView::TableHeaderModelPrint(const bool detail,const bool as_table=false,const int cell_width=CELL_WIDTH_IN_CHARS)
  {
   if(this.m_table_header_model!=NULL)
      this.m_table_header_model.Print(detail,as_table,cell_width);
  }

Permite imprimir en el registro el modelo del encabezado de la tabla asignado a este objeto.

Una tabla, es decir, su representación visual, es un objeto compuesto que obtiene de su modelo los datos de la tabla y del encabezado, y dibuja esos datos en distintos controles. Se trata de un panel en el que están ubicados el encabezado y un contenedor con datos tabulares (filas de la tabla).



Clase de tabla (Vista)

Sigamos escribiendo el código en el archivo Controls.mqh:

//+------------------------------------------------------------------+
//| Clase de la representación visual de la tabla                    |
//+------------------------------------------------------------------+
class CTableView : public CPanel
  {
protected:
//--- Datos obtenidos de la tabla
   CTable           *m_table_obj;                  // Puntero a un objeto de tabla (incluye los modelos de la tabla y del encabezado de la tabla)
   CTableModel      *m_table_model;                // Puntero al modelo de la tabla (obtenido de CTable)
   CTableHeader     *m_header_model;               // Puntero al modelo del encabezado de la tabla (obtenido de CTable)
   
//--- Datos del componente Vista
   CTableHeaderView *m_header_view;                // Puntero al encabezado de la tabla (Vista)
   CPanel           *m_table_area;                 // Panel para colocar las filas de la tabla
   CContainer       *m_table_area_container;       // Contenedor para alojar el panel con las filas de la tabla
   
//--- (1) Establece, (2) devuelve el modelo de la tabla
   bool              TableModelAssign(CTableModel *table_model);
   CTableModel      *GetTableModel(void)                                { return this.m_table_model;           }
   
//--- (1) Establece, (2) devuelve el modelo del encabezado de la tabla
   bool              HeaderModelAssign(CTableHeader *header_model);
   CTableHeader     *GetHeaderModel(void)                               { return this.m_header_model;          }

//--- Crea, a partir del modelo, un objeto (1) de encabezado y (2) de tabla
   bool              CreateHeader(void);
   bool              CreateTable(void);
   
public:
//--- (1) Establece, (2) devuelve un objeto de tabla
   bool              TableObjectAssign(CTable *table_obj);
   CTable           *GetTableObj(void)                                  { return this.m_table_obj;             }

//--- Muestra en el registro el modelo asignado (1) de la tabla, (2) del encabezado y (3) del objeto de tabla
   void              TableModelPrint(const bool detail);
   void              HeaderModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS);
   void              TablePrint(const int column_width=CELL_WIDTH_IN_CHARS);

//--- Dibuja el aspecto externo
   virtual void      Draw(const bool chart_redraw);
   
//--- Métodos virtuales: (1) comparación, (2) guardado en un archivo, (3) carga desde un archivo, (4) tipo de objeto
   virtual int       Compare(const CObject *node,const int mode=0)const { return CPanel::Compare(node,mode);   }
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                                   const { return(ELEMENT_TYPE_TABLE);          }
  
//--- Inicialización (1) del objeto de la clase y (2) de los colores predeterminados del objeto
   void              Init(void);

//--- Constructores/destructor
                     CTableView(void);
                     CTableView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CTableView (void){}
  };

Esta clase hereda de la clase del objeto de panel. Se ven tres componentes «Modelo» y tres componentes «Vista». En la clase hay punteros al modelo de la tabla, al modelo del encabezado y a la tabla. De la tabla obtenemos punteros a los datos de la tabla y al encabezado. Y hay tres punteros al encabezado, al panel de filas de la tabla y al contenedor para alojar este panel.

La idea es que las filas de la tabla se adjuntan a un objeto de panel (se pueden adjuntar varios elementos), y este panel se adjunta a un contenedor (se puede adjuntar un único elemento), donde hay barras de desplazamiento que permiten mover el panel con las filas de la tabla. De este modo, visualmente, el control tiene el aspecto de un encabezado de la tabla, y debajo aparece una tabla desplazable (filas y columnas formadas por celdas).

En los constructores de la clase, llamamos al método de inicialización del objeto:

//+------------------------------------------------------------------+
//| CTableView::Constructor por defecto.                             |
//| Construye un elemento en la ventana principal del gráfico actual |
//| en las coordenadas 0,0 con las dimensiones predeterminadas       |
//+------------------------------------------------------------------+
CTableView::CTableView(void) : CPanel("TableView","",::ChartID(),0,0,0,DEF_PANEL_W,DEF_PANEL_H),
   m_table_model(NULL),m_header_model(NULL),m_table_obj(NULL),m_header_view(NULL),m_table_area(NULL),m_table_area_container(NULL)
  {
//--- Inicialización
   this.Init();
  }
//+------------------------------------------------------------------+
//| CTableView::Constructor paramétrico.                             |
//| Construye un elemento en la ventana indicada del gráfico especificado               |
//| con el texto, las coordenadas y las dimensiones indicadas        |
//+------------------------------------------------------------------+
CTableView::CTableView(const string object_name,const string text,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) :
   CPanel(object_name,text,chart_id,wnd,x,y,w,h),m_table_model(NULL),m_header_model(NULL),m_table_obj(NULL),m_header_view(NULL),m_table_area(NULL),m_table_area_container(NULL)
  {
//--- Inicialización
   this.Init();
  }

En el método de inicialización creamos en el panel del objeto todos los controles necesarios:

//+------------------------------------------------------------------+
//| CTableView::Inicialización                                       |
//+------------------------------------------------------------------+
void CTableView::Init(void)
  {
//--- Inicialización del objeto padre
   CPanel::Init();
//--- Ancho del borde
   this.SetBorderWidth(1);
//--- Creamos el encabezado de la tabla
   this.m_header_view=this.InsertNewElement(ELEMENT_TYPE_TABLE_HEADER,"","TableHeader",0,0,this.Width(),DEF_TABLE_HEADER_H);
   if(this.m_header_view==NULL)
      return;
   this.m_header_view.SetBorderWidth(1);
   
//--- Creamos un contenedor en el que se ubicará el panel de filas de la tabla
   this.m_table_area_container=this.InsertNewElement(ELEMENT_TYPE_CONTAINER,"","TableAreaContainer",0,DEF_TABLE_HEADER_H,this.Width(),this.Height()-DEF_TABLE_HEADER_H);
   if(this.m_table_area_container==NULL)
      return;
   this.m_table_area_container.SetBorderWidth(0);
   this.m_table_area_container.SetScrollable(true);
   
//--- Adjuntamos al contenedor el panel para almacenar las filas de la tabla
   int shift_y=0;
   this.m_table_area=this.m_table_area_container.InsertNewElement(ELEMENT_TYPE_PANEL,"","TableAreaPanel",0,shift_y,this.m_table_area_container.Width(),this.m_table_area_container.Height()-shift_y);
   if(m_table_area==NULL)
      return;
   this.m_table_area.SetBorderWidth(0);
  }

Método que establece el modelo de la tabla:

//+------------------------------------------------------------------+
//| CTableView::Establece el modelo de la tabla                      |
//+------------------------------------------------------------------+
bool CTableView::TableModelAssign(CTableModel *table_model)
  {
   if(table_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
   this.m_table_model=table_model;
   return true;
  }

Simplemente pasamos al método un puntero al objeto del modelo de la tabla y lo almacenamos en una variable, desde la que luego accederemos a él.

Método que establece el modelo del encabezado de la tabla:

//+------------------------------------------------------------------+
//| CTableView::Establece el modelo del encabezado de la tabla       |
//+------------------------------------------------------------------+
bool CTableView::HeaderModelAssign(CTableHeader *header_model)
  {
   if(header_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
   this.m_header_model=header_model;
   return true;
  }

Pasamos al método un puntero al objeto del modelo del encabezado de la tabla y lo almacenamos en una variable, desde la que accederemos a él.

Método que establece el objeto de la tabla:

//+------------------------------------------------------------------+
//| CTableView::Establece el objeto de la tabla                      |
//+------------------------------------------------------------------+
bool CTableView::TableObjectAssign(CTable *table_obj)
  {
//--- Si se ha pasado un objeto de la tabla vacío, lo notificamos y devolvemos false
   if(table_obj==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- Guardamos el puntero en una variable
   this.m_table_obj=table_obj;
//--- Registramos el resultado de la asignación del modelo de la tabla y del modelo del encabezado
   bool res=this.TableModelAssign(this.m_table_obj.GetTableModel());
   res &=this.HeaderModelAssign(this.m_table_obj.GetTableHeader());
   
//--- Si no se ha podido asignar alguno de los modelos, devolvemos false
   if(!res)
      return false;
   
//--- Registramos el resultado de crear el encabezado de la tabla y la tabla a partir de sus modelos
   res=this.CreateHeader();
   res&=this.CreateTable();
   
//--- Devolvemos el resultado
   return res;
  }

Se pasa al método un puntero al objeto de la tabla. A partir de este objeto, obtenemos y asignamos el modelo del encabezado y el modelo de la tabla. A continuación, a partir de los datos de los modelos del encabezado y de la tabla, creamos los objetos de representación visual del encabezado y de la tabla.

Método que crea un objeto de tabla a partir de un modelo:

//+------------------------------------------------------------------+
//| CTableView::Crea un objeto de tabla a partir del modelo          |
//+------------------------------------------------------------------+
bool CTableView::CreateTable(void)
  {
   if(this.m_table_area==NULL)
      return false;
   
//--- En un bucle, creamos RowsTotal filas a partir de elementos TableRowView y las adjuntamos al elemento Panel (m_table_area)
   int total=(int)this.m_table_model.RowsTotal();
   int y=1;                   // Desplazamiento vertical
   int table_height=0;        // Altura calculada del panel
   CTableRowView *row=NULL;
   for(int i=0;i<total;i++)
     {
      //--- Creamos y adjuntamos al panel un objeto de fila de la tabla
      row=this.m_table_area.InsertNewElement(ELEMENT_TYPE_TABLE_ROW,"","TableRow"+(string)i,0,y+(row!=NULL ? row.Height()*i : 0),this.m_table_area.Width()-1,DEF_TABLE_ROW_H);
      if(row==NULL)
         return false;
      
      //--- Establecemos el identificador de la fila
      row.SetID(i);
      //--- En función del número de fila (par/impar), establecemos el color de su fondo
      if(row.ID()%2==0)
         row.InitBackColorDefault(clrWhite);
      else
         row.InitBackColorDefault(clrWhiteSmoke);
      row.BackColorToDefault();
      row.InitBackColorFocused(row.GetBackColorControl().NewColor(row.BackColor(),-4,-4,-4));
      
      //--- Obtenemos el modelo de fila a partir del objeto de la tabla
      CTableRow *row_model=this.m_table_model.GetRow(i);
      if(row_model==NULL)
         return false;
      //--- Asignamos el modelo de fila obtenido al objeto de fila de la tabla creado
      row.TableRowModelAssign(row_model);
      //--- Calculamos el nuevo valor de la altura del panel
      table_height+=row.Height();
     }
//--- Devolvemos el resultado de cambiar el tamaño del panel al valor calculado en el bucle
   return this.m_table_area.ResizeH(table_height+y);
  }

La lógica del método se explica en los comentarios del código. En un bucle según el número de filas del modelo de la tabla, creamos un nuevo objeto de fila de la tabla y lo adjuntamos al panel, calculando al mismo tiempo la altura futura del panel. Una vez finalizado el bucle y creadas todas las filas necesarias, la altura del panel se modifica de acuerdo con el tamaño calculado en el bucle.

Método que dibuja el aspecto de la tabla:

//+------------------------------------------------------------------+
//| CTableView::Dibuja el aspecto                                    |
//+------------------------------------------------------------------+
void CTableView::Draw(const bool chart_redraw)
  {
//--- Dibujamos el encabezado y las filas de la tabla
   this.m_header_view.Draw(false);
   this.m_table_area_container.Draw(false);
//--- Si se indica, actualizamos el gráfico
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Primero dibujamos el encabezado de la tabla y, debajo, las filas de la tabla dentro del contenedor.

Método que imprime en el registro el modelo de la tabla asignado:

//+------------------------------------------------------------------+
//| CTableView::Imprime en el registro el modelo de la tabla asignado|
//+------------------------------------------------------------------+
void CTableView::TableModelPrint(const bool detail)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.Print(detail);
  }

Permite imprimir en el registro el modelo de la tabla asignado.

Método que imprime en el registro el modelo del encabezado de la tabla asignado:

//+------------------------------------------------------------------+
//| CTableView::Imprime en el registro el modelo del encabezado de la tabla asignado |
//+------------------------------------------------------------------+
void CTableView::HeaderModelPrint(const bool detail,const bool as_table=false,const int column_width=CELL_WIDTH_IN_CHARS)
  {
   if(this.m_header_model!=NULL)
      this.m_header_model.Print(detail,as_table,column_width);
  }

Permite imprimir en el registro el modelo del encabezado de la tabla asignado.

Método que imprime en el registro el objeto de tabla asignado:

//+------------------------------------------------------------------+
//| CTableView::Imprime en el registro el objeto de tabla asignado   |
//+------------------------------------------------------------------+
void CTableView::TablePrint(const int column_width=CELL_WIDTH_IN_CHARS)
  {
   if(this.m_table_obj!=NULL)
      this.m_table_obj.Print(column_width);
  }

Imprime en el registro toda la tabla asignada al objeto: el encabezado y los datos.

Ya está, hemos creado todas las clases necesarias para generar la representación visual del modelo de la tabla. Veamos cómo se puede crear ahora una tabla sencilla a partir de un array de datos.


Probamos el resultado

En el directorio del terminal \MQL5\Indicators\Tables\ crearemos un nuevo indicador, sin búferes y que se dibuje en una subventana del gráfico:

//+------------------------------------------------------------------+
//|                                                   iTestTable.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 0
#property indicator_plots   0

Incluiremos la biblioteca en el archivo del indicador y declararemos globalmente los punteros a los objetos:

//+------------------------------------------------------------------+
//|                                                   iTestTable.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 0
#property indicator_plots   0

//+------------------------------------------------------------------+
//| Bibliotecas incluidas                                            |
//+------------------------------------------------------------------+
#include "Controls\Controls.mqh"    // Библиотека элементов управления

CPanel     *panel=NULL; // Puntero al elemento gráfico Panel
CTable     *table;      // Puntero al objeto de la tabla (Modelo)

A continuación, escribiremos el siguiente código en el controlador OnInit() del indicador:

//+------------------------------------------------------------------+
//| Función de inicialización del indicador personalizado            |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Buscamos la subventana del gráfico
   int wnd=ChartWindowFind();

//--- Creamos el elemento gráfico «Panel»
   panel=new CPanel("Panel","",0,wnd,100,40,400,192);
   if(panel==NULL)
      return INIT_FAILED;
//--- Configuramos los parámetros del panel
   panel.SetID(1);                     // Identificador 
   panel.SetAsMain();                  // En el gráfico debe haber necesariamente un elemento principal
   panel.SetBorderWidth(1);            // Ancho del borde (sangría del área visible de un píxel en cada lado del contenedor)
   panel.SetResizable(false);          // Se ha desactivado la opción de cambiar el tamaño arrastrando los bordes y las esquinas
   panel.SetName("Main container");    // Nombre
   
//--- Creamos los datos para la tabla
//--- Declaramos y rellenamos un array de encabezados de columnas con dimensión 4
   string captions[4]={"Column 0","Column 1","Column 2","Column 3"};
  
//--- Declaramos y rellenamos un array de datos con dimensión 10x4
//--- El tipo de array puede ser double, long, datetime, color o string
   long array[10][4]={{ 1,  2,  3,  4},
                      { 5,  6,  7,  8},
                      { 9, 10, 11, 12},
                      {13, 14, 15, 16},
                      {17, 18, 19, 20},
                      {21, 22, 23, 24},
                      {25, 26, 27, 28},
                      {29, 30, 31, 32},
                      {33, 34, 35, 36},
                      {37, 38, 39, 40}};
//--- Creamos un objeto de tabla a partir del array de tipo long de 10x4 creado anteriormente y del array de tipo string con los encabezados de columnas (componente Modelo)
   table=new CTable(array,captions);
   if(table==NULL)
      return INIT_FAILED;
   PrintFormat("The [%s] has been successfully created:",table.Description());
   
//--- En el panel, creamos un nuevo elemento: una tabla (componente Vista)
   CTableView *table_view=panel.InsertNewElement(ELEMENT_TYPE_TABLE,"","TableView",4,4,panel.Width()-8,panel.Height()-8);
//--- Asignamos un objeto de tabla (Modelo) al elemento gráfico «Tabla» (Vista)
   table_view.TableObjectAssign(table);
//--- Imprimiremos en el registro el modelo de la tabla
   table_view.TablePrint();
   
//--- Dibujemos la tabla junto con el panel
   panel.Draw(true);

//--- Correcto
   return(INIT_SUCCEEDED);
  }

Aquí hay tres bloques de código:

  1. Creación del panel sobre el que se construirá la tabla;
  2. Creación de datos tabulares en dos arrays: la tabla y el encabezado (componentes del Modelo);
  3. Creación de una tabla en el panel (componente Vista).

En esencia, para crear una tabla se necesitan dos pasos: construir los datos (punto 2) y construir la tabla (punto 3). El panel solo es necesario para la presentación y como base para el elemento gráfico.

Vamos a completar el resto del código del indicador:

//+------------------------------------------------------------------+
//| Función de inicialización del indicador personalizado            |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Eliminamos el elemento «Panel» y destruimos la tabla y el gestor de recursos comunes de la biblioteca
   delete panel;
   delete table;
   CCommonManager::DestroyInstance();
  }
//+------------------------------------------------------------------+
//| Función de iteración del indicador personalizado                 |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   
//--- valor de retorno de «prev_calculated» para la siguiente llamada
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| Función ChartEvent                                               |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Llamamos al controlador OnChartEvent del elemento «Panel»
   panel.OnChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+
//| Temporizador                                                     |
//+------------------------------------------------------------------+
void OnTimer(void)
  {
//--- Llamamos al controlador OnTimer del elemento «Panel»
   panel.OnTimer();
  }

Compilemos el indicador y ejecutémoslo en el gráfico:

En el registro del terminal aparecerá un mensaje indicando que la tabla se ha creado correctamente, se mostrará la descripción de la tabla y se verá la propia tabla con el encabezado:

The [Table: Rows total: 10, Columns total: 4] has been successfully created:
Table: Rows total: 10, Columns total: 4:
|                n/n |           Column 0 |           Column 1 |           Column 2 |           Column 3 |
| 0                  |                  1 |                  2 |                  3 |                  4 |
| 1                  |                  5 |                  6 |                  7 |                  8 |
| 2                  |                  9 |                 10 |                 11 |                 12 |
| 3                  |                 13 |                 14 |                 15 |                 16 |
| 4                  |                 17 |                 18 |                 19 |                 20 |
| 5                  |                 21 |                 22 |                 23 |                 24 |
| 6                  |                 25 |                 26 |                 27 |                 28 |
| 7                  |                 29 |                 30 |                 31 |                 32 |
| 8                  |                 33 |                 34 |                 35 |                 36 |
| 9                  |                 37 |                 38 |                 39 |                 40 |

En el gráfico vemos que los encabezados de las columnas y las filas de la tabla están activos y reaccionan al pasar el puntero del ratón por encima. En el próximo artículo hablaremos sobre cómo gestionar la interacción del usuario con la tabla.


Conclusión

Hemos aprendido a crear tablas sencillas y a representarlas en el gráfico. Pero, por ahora, se trata solo de una tabla estática que muestra datos obtenidos una sola vez. El próximo artículo estará dedicado a hacer dinámicas las tablas: a modificar y visualizar datos dinámicos, así como a interactuar con el usuario para configurar la visualización de las filas y columnas de la tabla. Además, en el próximo artículo simplificaremos aún más la creación de tablas.

Programas utilizados en el artículo:

#
Nombre Tipo
Descripción
1 Tables.mqh Biblioteca de clases Clases para crear el modelo de la tabla
2 Base.mqh Biblioteca de clases Clases para crear el objeto base de los controles
3 Controls.mqh Biblioteca de clases Clases de controles
4 iTestTable.mq5 Indicador de prueba Indicador para probar el funcionamiento del control TableView
5 MQL5.zip Archivo comprimido Archivo comprimido con los archivos mostrados anteriormente, para descomprimir en el directorio MQL5 del terminal de cliente

Todos los archivos creados se adjuntan al artículo para su estudio independiente. El archivo comprimido se puede descomprimir en la carpeta del terminal, y todos los archivos quedarán ubicados en la carpeta correspondiente: \MQL5\Indicators\Tables\.

El código fuente completo del proyecto, con todos los archivos descritos en el artículo, está disponible en el repositorio.


Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/19288

Archivos adjuntos |
Tables.mqh (261.78 KB)
Base.mqh (294.37 KB)
Controls.mqh (650.82 KB)
iTestTable.mq5 (10.71 KB)
MQL5.zip (111.29 KB)
Alireza
Alireza | 29 dic 2025 en 04:45

Excelente explicación del modelo MVC, pero ¿cómo mejora exactamente esta complejidad el trading, que es para lo que realmente estamos aquí?

¿Se han resuelto ya todos los demás retos del trading, quedando solo la interfaz de usuario?

A veces olvidamos para qué sirve realmente una herramienta —o nos frustramos cuando no funciona como queremos— y acabamos intentando que haga cosas para las que nunca fue diseñada y que nunca hará.

Artyom Trishkin
Artyom Trishkin | 29 dic 2025 en 09:02
Alireza #:

Es una descripción excelente del modelo MVC, pero ¿cómo mejora exactamente esta complejidad el comercio, que es, en realidad, el motivo por el que nos hemos reunido aquí?

¿Acaso se han resuelto todos los demás problemas del comercio y solo queda la interfaz de usuario?

A veces olvidamos para qué sirve realmente una herramienta, o nos frustramos cuando no funciona como queremos y, al final, intentamos obligarla a hacer algo para lo que nunca fue diseñada y que nunca hará.

Sí, claro, las ganancias no las genera la interfaz. Pero la interfaz es como el panel de instrumentos de un avión: no te enseña a volar, pero te ayuda a no estrellarte en la niebla. Cuando los problemas clave del trading ya están resueltos, la arquitectura del código y la facilidad de uso de la plataforma se convierten en el factor que permite ampliar el éxito, en lugar de ahogarse en la rutina.
Asesor experto de Forex basado en la red neuronal N-BEATS Asesor experto de Forex basado en la red neuronal N-BEATS
Hoy veremos la implementación de la arquitectura N-BEATS para el trading de Forex en MetaTrader 5, con predicción cuantílica y gestión adaptativa del riesgo. La arquitectura se ha adaptado mediante normalización bilineal y funciones de pérdida especializadas para datos financieros. Las pruebas realizadas con datos de 2025 han puesto de manifiesto la incapacidad de generar beneficios, lo que confirma la brecha existente entre los logros teóricos y la eficacia práctica del trading.
Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 34): Conversión de datos brutos del mercado en modelos predictivos mediante un pipeline avanzado de ingesta Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 34): Conversión de datos brutos del mercado en modelos predictivos mediante un pipeline avanzado de ingesta
¿Alguna vez te has perdido un repunte repentino del mercado o te ha pillado desprevenido cuando ocurrió uno? La mejor manera de anticiparse a los eventos en tiempo real es aprender de los patrones históricos. Con el objetivo de entrenar un modelo de aprendizaje automático, este artículo comienza mostrándole cómo crear un script en MetaTrader 5 que procese datos históricos y los envíe a Python para su almacenamiento, sentando así las bases de su sistema de detección de picos. Sigue leyendo para ver cada paso en acción.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Redes neuronales en el trading: Modelo de difusión adaptativa sobre grafos (SAGDFN) Redes neuronales en el trading: Modelo de difusión adaptativa sobre grafos (SAGDFN)
En este artículo presentamos en detalle la arquitectura de SAGDFN, un framework moderno capaz de transformar el enfoque del procesamiento de datos espaciotemporales. Conserva la información clave incluso en grafos complejos y, al mismo tiempo, reduce los costos computacionales.