English Русский 中文 Deutsch 日本語 Português
Interfaces gráficas X: Ordenamiento, reconstrucción de la tabla y controles en las celdas (build 11)

Interfaces gráficas X: Ordenamiento, reconstrucción de la tabla y controles en las celdas (build 11)

MetaTrader 5Ejemplos | 29 marzo 2017, 09:32
1 008 0
Anatoli Kazharski
Anatoli Kazharski

Índice

‌‌

Introducción

El primer artículo de la serie nos cuenta con más detalles para qué sirve esta librería: Interfaces gráficas I: Preparación de la estructura de la librería (Capítulo 1). Al final de cada artículo de la serie se muestra la lista completa de los capítulos con los enlaces. Además, se puede descargar la versión completa de la librería en la fase actual del desarrollo del proyecto. Es necesario colocar los ficheros en los mismos directorios, tal como están ubicados en el archivo.

Seguimos con el desarrollo de lla tabla dibujada. Vamos a nombrar sus nuevas funcionalidades.

  • Ordenamiento de datos de la tabla.
  • Control del número de las columnas y filas: inserción y eliminación de las columnas y filas según el índice especificado; vaciado completo de la tabla (queda sólo una columna y una fila); reconstrucción de la tabla (vaciado completo y establecimiento de la nueva dimensionalidad).
  • Vamos a ampliar las posibilidades del control de usuario de la interfaz gráfica: se añade el procesamiento del clic izquierdo del ratón.
  • Empezaremos a añadir los controles a las celdas de la tabla: casillas de verificación (checkbox) y botones.

Si Usted ya utiliza esta librería para diseñar las interfaces gráficas, y aplica la tabla tipo CTable para visualizar los datos, ahora se le propone que pase a la tabla tipo CCanvasTable. A partir de este artículo, se ha igualado por completo con los demás tipos de las tablas de esta librería, e incluso las ha superado en algunos parámetros. 

Ordenamiento de la tabla

La mayoría de los métodos para el ordenamiento de los datos de la tabla se han quedado los mismos que en CTable. El artículo Interfaces gráficas X: Control «Hora», control «Lista de las casillas de verificación» y ordenamiento (sort) de la tabla describe detalladamente cómo está organizado todos esto. Aquí hablaremos un poco sobre los cambios y adiciones referentes a la tabla dibujada tipo CCanvasTable.

Para dibujar el indicio de la tabla ordenada, vamos a necesitar un array estático tipo CTImage compuesto de dos elementos. Ahí van a guardarse las rutas hacia los imágenes que deben reflejar la dirección del ordenamiento. Si el usuario no establece su versión de las imágenes, se utilizarán los iconos predefinidos. La inicialización con valores predefinidos y el llenado de los arrays de las imágenes se realiza en el método de la creación de encabezados CCanvasTable::CreateHeaders(). 

//+------------------------------------------------------------------+
//| Clase par crear la tabla dibujada                                |
//+------------------------------------------------------------------+
class CCanvasTable : public CElement
  {
private:
   //--- Iconos para el indicio de datos ordenados
   CTImage           m_sort_arrows[2];
   //---
public:
   //--- Establecimiento de imágenes  para el indicio de datos ordenados
   void              SortArrowFileAscend(const string path)  { m_sort_arrows[0].m_bmp_path=path; }
   void              SortArrowFileDescend(const string path) { m_sort_arrows[1].m_bmp_path=path; }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CCanvasTable::CCanvasTable(void) 
  {
...
//--- Inicialización de la estructura del indicio de ordenamiento
   m_sort_arrows[0].m_bmp_path="";
   m_sort_arrows[1].m_bmp_path="";
  }
//+------------------------------------------------------------------+
//| Crea encabezados de la tabla                                     |
//+------------------------------------------------------------------+
bool CCanvasTable::CreateHeaders(void)
  {
//--- Salir si los encabezados están desactivados
   if(!m_show_headers)
      return(true);
//--- Formación del nombre del objeto
   string name=CElementBase::ProgramName()+"_table_headers_"+(string)CElementBase::Id();
//--- Coordenadas
   int x =m_x+1;
   int y =m_y+1;
//--- Determinamos las imágenes como el indicio de la posibilidad del ordenamiento de la tabla
   if(m_sort_arrows[0].m_bmp_path=="")
      m_sort_arrows[0].m_bmp_path="::Images\\EasyAndFastGUI\\Controls\\SpinInc.bmp";
   if(m_sort_arrows[1].m_bmp_path=="")
      m_sort_arrows[1].m_bmp_path="::Images\\EasyAndFastGUI\\Controls\\SpinDec.bmp";
//---
   for(int i=0; i<2; i++)
     {
      ::ResetLastError();
      if(!::ResourceReadImage(m_sort_arrows[i].m_bmp_path,m_sort_arrows[i].m_image_data,
         m_sort_arrows[i].m_image_width,m_sort_arrows[i].m_image_height))
        {
         ::Print(__FUNCTION__," > error: ",::GetLastError());
        }
     }
//--- Creación del objeto
   ::ResetLastError();
   if(!m_headers.CreateBitmapLabel(m_chart_id,m_subwin,name,x,y,m_table_x_size,m_header_y_size,COLOR_FORMAT_ARGB_NORMALIZE))
     {
      ::Print(__FUNCTION__," > Fallo al crear el lienzo para dibujar los encabezados de la tabla: ",::GetLastError());
      return(false);
     }
//--- Adjuntar al gráfico
//--- Establecemos las propiedades
//--- Coordenadas
//--- Guardamos los tamaños
//--- Márgenes desde el punto extremo del panel
//--- Guardamos el puntero del objeto
//--- Establecemos el área visible
//--- Establecemos el desplazamiento del frame dentro de la imagen por los ejes X y Y
...
   return(true);
  }

El método CCanvasTable::DrawSignSortedData() va a utilizarse para dibujar el indicio del array ordenado.Este elemento se dibuja sólo si (1) el modo del ordenamiento está activado y (2) los datos de la tabla ya han sido ordenados. Los márgenes se puede controlar a través de los métodos CCanvasTable::SortArrowXGap() y CCanvasTable::SortArrowYGap(). La imagen significa que el ordenamiento ascendiente va a tener el índice 0, y el descendiente, el índice 1. A continuación, sigue el ciclo doble del dibujo de la imagen. Este tema ya ha sido considerado detalladamente.‌

class CCanvasTable : public CElement
  {
private:
   //--- Márgenes para el icono de indicio de datos ordenados
   int               m_sort_arrow_x_gap;
   int               m_sort_arrow_y_gap;
   //---
public:
   //--- Iconos para el indicio de tabla ordenada
   void              SortArrowXGap(const int x_gap)          { m_sort_arrow_x_gap=x_gap;         }
   void              SortArrowYGap(const int y_gap)          { m_sort_arrow_y_gap=y_gap;         }
   //---
private:
  //--- Dibuja el indicio de la posibilidad de ordenar la tabla
   void              DrawSignSortedData(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CCanvasTable::CCanvasTable(void) : m_sort_arrow_x_gap(20),
                                   m_sort_arrow_y_gap(6)
  {
...
  }
//+------------------------------------------------------------------+
//| Dibuja el indicio de la posibilidad de ordenar la tabla          |
//+------------------------------------------------------------------+
void CCanvasTable::DrawSignSortedData(void)
  {
//--- Salir si (1) el ordenamiento está desactivado o (2) no ha sido realizado todavía
   if(!m_is_sort_mode || m_is_sorted_column_index==WRONG_VALUE)
      return;
//--- Cálculo de coordenadas
   int x =m_columns[m_is_sorted_column_index].m_x2-m_sort_arrow_x_gap;
   int y =m_sort_arrow_y_gap;
//--- Imagen seleccionada en dirección del ordenamiento
   int image_index=(m_last_sort_direction==SORT_ASCEND)? 0 : 1;
//--- Dibujamos
   for(uint ly=0,i=0; ly<m_sort_arrows[image_index].m_image_height; ly++)
     {
      for(uint lx=0; lx<m_sort_arrows[image_index].m_image_width; lx++,i++)
        {
         //--- Si no hay color, ir al siguiente píxel
         if(m_sort_arrows[image_index].m_image_data[i]<1)
            continue;
         //--- Obtenemos el color de la capa inferior (fondo del encabezado) y el color del píxel indicado de la imagen
         uint background  =m_headers.PixelGet(x+lx,y+ly);
         uint pixel_color =m_sort_arrows[image_index].m_image_data[i];
         //--- Mezclamos los colores
         uint foreground=::ColorToARGB(m_clr.BlendColors(background,pixel_color));
         //--- Dibujamos el píxel de la imagen solapada
         m_headers.PixelSet(x+lx,y+ly,foreground);
        }
     }
  }

Durante el ordenamiento de los valores de la tabla, se utiliza el método CCanvasTable::Swap() con el fin de solaparlos. Su particularidad consiste en la necesidad de desplazar no sólo el texto de la celda y/o la imagen, sino también la mayoría de las propiedades de la celda.  Al desplazar las imágenes, para evitar los fragmentos repetidos del código, necesitaremos el método adicional CCanvasTable::ImageCopy(). Se le envían dos arrays tipo CCanvasTable::ImageCopy(), receptor y fuente de datos. Como el tercer argumento, se envía el índice de la imagen copiada, porque en una celda no pueden haber varias imágenes.

class CCanvasTable : public CElement
  {
private:
   //--- Copia los datos de la imagen de un array a otro
   void              ImageCopy(CTImage &destination[],CTImage &source[],const int index);
  };
//+------------------------------------------------------------------+
//| Copia los datos de la imagen de un array a otro                  |
//+------------------------------------------------------------------+
void CCanvasTable::ImageCopy(CTImage &destination[],CTImage &source[],const int index)
  {
//--- Copiamos los píxeles de la imagen
   ::ArrayCopy(destination[index].m_image_data,source[index].m_image_data);
//--- Copiamos las propiedades de la imagen
   destination[index].m_image_width  =source[index].m_image_width;
   destination[index].m_image_height =source[index].m_image_height;
   destination[index].m_bmp_path     =source[index].m_bmp_path;
  }

Al final, el código del método CCanvasTable::Swap() es como se muestra en el listado del código de abajo. Antes de mover las imágenes, primero es necesario comprobar si están presentes, y si no, pasar a la siguiente columna. Si en una de las celdas hay imágenes, se desplazan a través del método CCanvasTable::ImageCopy(). Es la misma operación como en el caso del desplazamiento de otras propiedades de la celda. Es decir, primero memorizamos la propiedad de la primera celda. Luego, copiamos la misma propiedad de la segunda celda en lugar de la primera. Y finalmente, en lugar de la segunda celda colocamos el valor guardado anteriormente de la primera.

class CCanvasTable : public CElement
  {
private:
   //--- Alternar los valores de las celdas especificadas
   void              Swap(uint r1,uint r2);
  };
//+------------------------------------------------------------------+
//| Alterna los elementos                                            |
//+------------------------------------------------------------------+
void CCanvasTable::Swap(uint r1,uint r2)
  {
//--- Recorremos en el ciclo todas las columnas
   for(uint c=0; c<m_columns_total; c++)
     {
      //--- Alternamos el texto completo
      string temp_text                    =m_columns[c].m_rows[r1].m_full_text;
      m_columns[c].m_rows[r1].m_full_text =m_columns[c].m_rows[r2].m_full_text;
      m_columns[c].m_rows[r2].m_full_text =temp_text;
      //--- Alternamos el texto breve
      temp_text                            =m_columns[c].m_rows[r1].m_short_text;
      m_columns[c].m_rows[r1].m_short_text =m_columns[c].m_rows[r2].m_short_text;
      m_columns[c].m_rows[r2].m_short_text =temp_text;
       //--- Alternamos el número de dígitos después de la coma
      uint temp_digits                 =m_columns[c].m_rows[r1].m_digits;
      m_columns[c].m_rows[r1].m_digits =m_columns[c].m_rows[r2].m_digits;
      m_columns[c].m_rows[r2].m_digits =temp_digits;
      //--- Alternamos el color del texto
      color temp_text_color                =m_columns[c].m_rows[r1].m_text_color;
      m_columns[c].m_rows[r1].m_text_color =m_columns[c].m_rows[r2].m_text_color;
      m_columns[c].m_rows[r2].m_text_color =temp_text_color;
      //--- Alternamos el índice de la imagen seleccionada
      int temp_selected_image                  =m_columns[c].m_rows[r1].m_selected_image;
      m_columns[c].m_rows[r1].m_selected_image =m_columns[c].m_rows[r2].m_selected_image;
      m_columns[c].m_rows[r2].m_selected_image =temp_selected_image;
      Comprobamos si hay imágenes en las celdas
      int r1_images_total=::ArraySize(m_columns[c].m_rows[r1].m_images);
      int r2_images_total=::ArraySize(m_columns[c].m_rows[r2].m_images);
      //--- Pasamos a la siguiente columna si ambas celdas no contienen imágenes
      if(r1_images_total<1 && r2_images_total<1)
         continue;
      //--- Alternamos las imágenes
      CTImage r1_temp_images[];
      //---
      ::ArrayResize(r1_temp_images,r1_images_total);
      for(int i=0; i<r1_images_total; i++)
         ImageCopy(r1_temp_images,m_columns[c].m_rows[r1].m_images,i);
      //---
      ::ArrayResize(m_columns[c].m_rows[r1].m_images,r2_images_total);
      for(int i=0; i<r2_images_total; i++)
         ImageCopy(m_columns[c].m_rows[r1].m_images,m_columns[c].m_rows[r2].m_images,i);
      //---
      ::ArrayResize(m_columns[c].m_rows[r2].m_images,r1_images_total);
      for(int i=0; i<r1_images_total; i++)
         ImageCopy(m_columns[c].m_rows[r2].m_images,r1_temp_images,i);
     }
  }

El clic en uno de los encabezados de la tabla llama al método CCanvasTable::OnClickHeaders(), que inicia el ordenamiento de datos. En esta clase, este método es mucho más sencillo que en la tabla tipo CTable: compare y asegúrese por sí mismo.

class CCanvasTable : public CElement
  {
private:
  //--- Procesamiento del clic en el encabezado
   bool              OnClickHeaders(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| Procesamiento del clic en el encabezado                          |
//+------------------------------------------------------------------+
bool CCanvasTable::OnClickHeaders(const string clicked_object)
  {
//--- Salir si (1) el modo de ordenamiento está desactivado o (2) se ejecuta el proceso del cambio del ancho de la columna
   if(!m_is_sort_mode || m_column_resize_control!=WRONG_VALUE)
      return(false);
//--- Salimos si la barra de desplazamiento se encuentra en modo activo
   if(m_scrollv.ScrollState() || m_scrollh.ScrollState())
      return(false);
//--- Salimos si el nombre del objeto no coincide
   if(m_headers.Name()!=clicked_object)
      return(false);
//--- Para la determinación del índice de la columna
   uint column_index=0;
//--- Obtenemos la coordenada relativa X debajo del cursor del ratón
   int x=m_mouse.RelativeX(m_headers);
//--- Determinamos el encabezado en el que hemos pulsado
   for(uint c=0; c<m_columns_total; c++)
     {
      //--- Si el encabezado ha sido encontrado, recordamos su índice
      if(x>=m_columns[c].m_x && x<=m_columns[c].m_x2)
        {
         column_index=c;
         break;
        }
     }
//--- Ordenamiento de datos para la columna especificada
   SortData(column_index);
//--- Enviamos el mensaje sobre ello
   ::EventChartCustom(m_chart_id,ON_SORT_DATA,CElementBase::Id(),m_is_sorted_column_index,::EnumToString(DataType(column_index)));
   return(true);
  }

Los demás métodos para el ordenamiento de datos han quedado sin cambiar y no se diferencian en nada de los considerados anteriormente  Por eso vamos a considerar sólo la lista de las declaraciones de estos campos y métodos en la clase CCanvasTable:‌

class CCanvasTable : public CElement
  {
private:
   //--- Modo de ordenamiento de datos por las columnas
   bool              m_is_sort_mode;
   //--- Indice de la columna ordenada (WRONG_VALUE – tabla no ordenada)
   int               m_is_sorted_column_index;
   //--- Ultima dirección del ordenamiento
   ENUM_SORT_MODE    m_last_sort_direction;
   //---
public:
   //--- Modo de datos ordenados
   void              IsSortMode(const bool flag)             { m_is_sort_mode=flag;              }

   //--- Obtención/establecimiento del tipo de datos
   ENUM_DATATYPE     DataType(const uint column_index);
   void              DataType(const uint column_index,const ENUM_DATATYPE type);
   //--- Ordenar los datos para la columna especificada
   void              SortData(const uint column_index=0);
   //---
private:
  //--- Procesamiento del clic en el encabezado
   bool              OnClickHeaders(const string clicked_object);

   //--- Método del ordenamiento rápido
   void              QuickSort(uint beg,uint end,uint column,const ENUM_SORT_MODE mode=SORT_ASCEND);
   //--- Comprobación de las condiciones del ordenamiento
   bool              CheckSortCondition(uint column_index,uint row_index,const string check_value,const bool direction);
  };

Abajo, se puede ver cómo funciona el ordenamiento en este tipo de tablas:

 Fig. 1. Demostración del ordenamiento en la tabla tipo CCanvasTable.‌

Fig. 1. Demostración del ordenamiento en la tabla tipo CCanvasTable.‌


Inserción y eliminación de columnas y filas

En uno de los artículos anteriores para las tablas tipo CTable, hemos considerado los métodos para insertar y eliminar las columnas y filas. En aquella versión, la inserción era posible sólo al final de la tabla. Ahora vamos a quitar este inconveniente enojoso: pues, hagamos que se pueda insertar una columna o una fila al especificar el índice del lugar de su inserción. 

Ejemplo: vamos a insertar una columna al principio de la tabla de datos, es decir, la nueva columna debe convertirse en la primera (índice 0). El array de las columnas debe aumentarse a un elemento, mientras que las propiedades y los valores de todas las celdas de la tabla deben desplazarse a un elemento a la derecha, dejando las celdas de la columna nueva en blanco. El mismo principio actúa también durante la inserción de las filas de la tabla. 

Cuando hay que eliminar una columna o una fila, funciona el principio reverso. Intentaremos quitar la primera columna de nuestro ejemplo anterior. Primero, los datos y las propiedades de las celdas serán desplazados a un elemento a la izquierda, y después de eso, el tamaño del array de las columnas se reducirá a un elemento.

Aquí, han sido implementados los métodos adicionales para que sea más cómodo crear los métodos de la inserción y eliminación de columnas y filas. Para inicilizar las celdas, columnas y filas insertadas con valores predefinidos, van a utilizarse los métodos CCanvasTable::ColumnInitialize() y CCanvasTable::CellInitialize().

class CCanvasTable : public CElement
  {
private:
  //--- Inicialización de la columna especificada con valores predefinidos
   void              ColumnInitialize(const uint column_index);
  //--- Inicialización de la fila especificada con valores predefinidos
   void              CellInitialize(const uint column_index,const uint row_index);
  };
//+--------------------------------------------------------------------+
//| Inicialización de la columna especificada con valores predefinidos |
//+--------------------------------------------------------------------+
void CCanvasTable::ColumnInitialize(const uint column_index)
  {
//--- Inicialización de las propiedades de las columnas con valores predefinidos
   m_columns[column_index].m_x              =0;
   m_columns[column_index].m_x2             =0;
   m_columns[column_index].m_width          =100;
   m_columns[column_index].m_type           =TYPE_STRING;
   m_columns[column_index].m_text_align     =ALIGN_CENTER;
   m_columns[column_index].m_text_x_offset  =m_text_x_offset;
   m_columns[column_index].m_image_x_offset =m_image_x_offset;
   m_columns[column_index].m_image_y_offset =m_image_y_offset;
   m_columns[column_index].m_header_text    ="";
  }
//+------------------------------------------------------------------+
//| Inicialización de la fila especificada con valores predefinidos  |
//+------------------------------------------------------------------+
void CCanvasTable::CellInitialize(const uint column_index,const uint row_index)
  {
   m_columns[column_index].m_rows[row_index].m_full_text      ="";
   m_columns[column_index].m_rows[row_index].m_short_text     ="";
   m_columns[column_index].m_rows[row_index].m_selected_image =0;
   m_columns[column_index].m_rows[row_index].m_text_color     =m_cell_text_color;
   m_columns[column_index].m_rows[row_index].m_digits         =0;
   m_columns[column_index].m_rows[row_index].m_type           =CELL_SIMPLE;
//--- Por defecto, la celda no tiene imágenes
   ::ArrayFree(m_columns[column_index].m_rows[row_index].m_images);s
  }

Para copiar las propiedades de una columna a la otra, hay que usar el método CCanvasTable::ColumnCopy(), cuyo código viene a continuación:‌

class CCanvasTable : public CElement
  {
private:
   //--- Hace la copia de la columna especificada (source) insertándola en nuevo lugar (dest.)
   void              ColumnCopy(const uint destination,const uint source);
  };
//+-----------------------------------------------------------------------------------------+
//| Hace la copia de la columna especificada (source) insertándola en nuevo lugar (dest.)   |
//+-----------------------------------------------------------------------------------------+
void CCanvasTable::ColumnCopy(const uint destination,const uint source)
  {
   m_columns[destination].m_header_text    =m_columns[source].m_header_text;
   m_columns[destination].m_width          =m_columns[source].m_width;
   m_columns[destination].m_type           =m_columns[source].m_type;
   m_columns[destination].m_text_align     =m_columns[source].m_text_align;
   m_columns[destination].m_text_x_offset  =m_columns[source].m_text_x_offset;
   m_columns[destination].m_image_x_offset =m_columns[source].m_image_x_offset;
   m_columns[destination].m_image_y_offset =m_columns[source].m_image_y_offset;
  }

El método CCanvasTable::CellCopy() copia una celda especificada a la otra. Para eso, hay que pasar los índices de la columna y fila de la celda de destino y los índices de la columna y fila de la celda fuente.‌

class CCanvasTable : public CElement
  {
private:
   //--- Hace la copia de la celda especificada (source) insertándola en nuevo lugar (dest.)
   void              CellCopy(const uint column_dest,const uint row_dest,const uint column_source,const uint row_source);
  };
//+-----------------------------------------------------------------------------------------+
//| Hace la copia de la celda especificada (source) insertándola en nuevo lugar (dest.)     |
//+-----------------------------------------------------------------------------------------+
void CCanvasTable::CellCopy(const uint column_dest,const uint row_dest,const uint column_source,const uint row_source)
  {
   m_columns[column_dest].m_rows[row_dest].m_type           =m_columns[column_source].m_rows[row_source].m_type;
   m_columns[column_dest].m_rows[row_dest].m_digits         =m_columns[column_source].m_rows[row_source].m_digits;
   m_columns[column_dest].m_rows[row_dest].m_full_text      =m_columns[column_source].m_rows[row_source].m_full_text;
   m_columns[column_dest].m_rows[row_dest].m_short_text     =m_columns[column_source].m_rows[row_source].m_short_text;
   m_columns[column_dest].m_rows[row_dest].m_text_color     =m_columns[column_source].m_rows[row_source].m_text_color;
   m_columns[column_dest].m_rows[row_dest].m_selected_image =m_columns[column_source].m_rows[row_source].m_selected_image;
//--- Copiamos el tamaño del array desde la fuente al receptor
   int images_total=::ArraySize(m_columns[column_source].m_rows[row_source].m_images);
   ::ArrayResize(m_columns[column_dest].m_rows[row_dest].m_images,images_total);
//---
   for(int i=0; i<images_total; i++)
     {
      //--- Copiar si hay imágenes
      if(::ArraySize(m_columns[column_source].m_rows[row_source].m_images[i].m_image_data)<1)
         continue;
      //--- Hacemos la copia de la imagen
      ImageCopy(m_columns[column_dest].m_rows[row_dest].m_images,m_columns[column_source].m_rows[row_source].m_images,i);
     }
  }

Con el fin de evitar la repetición continua del código, ha sido implementado otro método que será llamado después de las modificaciones introducidas en la reconstrucción de la tabla. Cada vez tras la inserción y eliminación de las columnas y filas, es necesario recontar y luego establecer nuevos tamaños de la tabla, y después de eso redibujar la tabla para que los cambios hagan efecto. Para eso, se usa el método CCanvasTable::RecalculateAndResizeTable(). Aquí, como único parámetro, se indica si es necesario redibujar la tabla entera (true), o simplemente actualizarla (false).

class CCanvasTable : public CElement
  {
private:
  //--- Cálculo tomando en cuenta las últimas modificaciones, y cambio de los tamaños de la tabla
   void              RecalculateAndResizeTable(const bool redraw=false);
  };
//+-------------------------------------------------------------------------------------------+
//| Cálculo tomando en cuenta las últimas modificaciones, y cambio de los tamaños de la tabla |
//+-------------------------------------------------------------------------------------------+
void CCanvasTable::RecalculateAndResizeTable(const bool redraw=false)
  {
//--- Calcular los tamaños de la tabla
   CalculateTableSize();
//--- Establecer nuevo tamaño de la tabla
   ChangeTableSize();
//--- Actualizar la tabla
   UpdateTable(redraw);
  }

Los métodos para insertar y eliminar las columnas y filas son mucho más simples y claros que las versiones de estos métodos en la tabla tipo CTable. Usted mismo puede comparar el código de estos métodos, y aquí como ejemplo mostraremos sólo el código para insertar y eliminar las columnas. El código para trabajar con las filas tiene la misma secuencia de acciones que ha sido descrita al principio de esta sección. Sólo voy a remarcar que el indicio de la tabla ordenada va a desplazarse junto con la columna ordenada, tanto en el caso de la inserción, como en el caso de la eliminación. Cuando la columna ordenada se elimina, el campo que guarda el índice de la columna ordenada, se pone a cero.

class CCanvasTable : public CElement
  {
public:
   //--- Inserta la columna en la tabla según el índice especificado
   void              AddColumn(const int column_index,const bool redraw=false);
   //--- Elimina la columna de la tabla según el índice especificado
   void              DeleteColumn(const int column_index,const bool redraw=false);
  };
//+------------------------------------------------------------------+
//| Inserta la columna en la tabla según el índice especificado      |
//+------------------------------------------------------------------+
void CCanvasTable::AddColumn(const int column_index,const bool redraw=false)
  {
//--- Aumentamos el tamaño del array a un elemento
   int array_size=(int)ColumnsTotal();
   m_columns_total=array_size+1;
   ::ArrayResize(m_columns,m_columns_total);
//--- Establecer el tamaño para los arrays de filas
   ::ArrayResize(m_columns[array_size].m_rows,m_rows_total);
//--- Corrección del índice en caso de exceder el rango
   int checked_column_index=(column_index>=(int)m_columns_total)? (int)m_columns_total-1 : column_index;
//--- Desplazar todas las columnas (movemos desde el fin del array hacia el índice de la columna añadida)
   for(int c=array_size; c>=checked_column_index; c--)
     {
      //--- Desplazar el indicio del array ordenado
      if(c==m_is_sorted_column_index && m_is_sorted_column_index!=WRONG_VALUE)
         m_is_sorted_column_index++;
      //--- Indice de la columna anterior
      int prev_c=c-1;
      //--- En columna nueva, inicialización con valores predefinidos
      if(c==checked_column_index)
         ColumnInitialize(c);
      //--- Movemos los datos de la columna anterior a la actual
      else
         ColumnCopy(c,prev_c);
      //---
      for(uint r=0; r<m_rows_total; r++)
        {
         //--- Inicialización de las celdas de nueva columna con valores por defecto
         if(c==checked_column_index)
           {
            CellInitialize(c,r);
            continue;
           }
        //--- Movemos los datos desde la celda de la columna anterior a la celda de la actual
         CellCopy(c,r,prev_c,r);
        }
     }
//--- Calcular y establecer nuevos tamaños de la tabla
   RecalculateAndResizeTable(redraw);
  }
//+------------------------------------------------------------------+
//| Elimina la columna de la tabla según el índice especificado      |
//+------------------------------------------------------------------+
void CCanvasTable::DeleteColumn(const int column_index,const bool redraw=false)
  {
//--- Obtenemos el tamaño del array de columnas
   int array_size=(int)ColumnsTotal();
//--- Corrección del índice en caso de exceder el rango
   int checked_column_index=(column_index>=array_size)? array_size-1 : column_index;
//--- Desplazar otras columnas (nos movemos del índice especificado hasta la última columna)
   for(int c=checked_column_index; c<array_size-1; c++)
     {
      //--- Desplazar el indicio del array ordenado
      if(c!=checked_column_index)
        {
         if(c==m_is_sorted_column_index && m_is_sorted_column_index!=WRONG_VALUE)
            m_is_sorted_column_index--;
        }
      //--- Poner a cero si la columna ordenada ha sido eliminada
      else
         m_is_sorted_column_index=WRONG_VALUE;
      //--- Indice de la siguiente columna
      int next_c=c+1;
      //--- Movemos los datos de la siguiente columna a la actual
      ColumnCopy(c,next_c);
      //--- Movemos los datos desde las celdas de la siguiente columna a las celdas de la actual
      for(uint r=0; r<m_rows_total; r++)
         CellCopy(c,r,next_c,r);
     }
//--- Reducimos el array de columnas a un elemento
   m_columns_total=array_size-1;
   ::ArrayResize(m_columns,m_columns_total);
//--- Calcular y establecer nuevos tamaños de la tabla
   RecalculateAndResizeTable(redraw);
  }

Los métodos para la reconstrucción completa y el vaciado de la tabla también son muy simples, a diferencia de los métodos análogos en la tabla tipo CTable. Aquí basta con establecer el nuevo tamaño a través de la llamada al método CCanvasTable::TableSize(). Al vaciar la tabla, se establece el tamaño mínimo, cuando queda sólo una columna y una fila. Durante el vaciado también se resetean los campos donde se guardan los valores de la fila seleccionada, dirección del ordenamiento y el índice de la columna ordenada. Al final de ambos métodos se calculan y se establecen nuevos tamaños de la tabla.

class CCanvasTable : public CElement
  {
public:
   //--- Reconstrucción de la tabla
   void              Rebuilding(const int columns_total,const int rows_total,const bool redraw=false);   
   //--- Vacía la tabla. Queda sólo una columna y una fila.
   void              Clear(const bool redraw=false);
  };
//+------------------------------------------------------------------+
//| Reconstrucción de la tabla                                       |
//+------------------------------------------------------------------+
void CCanvasTable::Rebuilding(const int columns_total,const int rows_total,const bool redraw=false)
  {
//--- Establecer nuevo tamaño
   TableSize(columns_total,rows_total);
//--- Calcular y establecer nuevos tamaños de la tabla
   RecalculateAndResizeTable(redraw);
  }
//+------------------------------------------------------------------+
//| Vacía la tabla. Queda sólo una columna y una fila.               |
//+------------------------------------------------------------------+
void CCanvasTable::Clear(const bool redraw=false)
  {
//--- Establecer nuevo tamaño 1x1
   TableSize(1,1);
//--- Establecer los valores por defecto
   m_selected_item_text     ="";
   m_selected_item          =WRONG_VALUE;
   m_last_sort_direction    =SORT_ASCEND;
   m_is_sorted_column_index =WRONG_VALUE;
//--- Calcular y establecer nuevos tamaños de la tabla
   RecalculateAndResizeTable(redraw);
  }

Es funciona así:

 Fig. 2. Demostración del control de la dimensionalidad de la tabla.

Fig. 2. Demostración del control de la dimensionalidad de la tabla.


Al final del artículo, se puede descargar la aplicación de prueba que se muestra en la animación anterior.


Evento del clic izquierdo del ratón

A veces, puede ser necesario activar alguna acción haciendo doble clic con ratón. Vamos a implementar la generación de tal evento en nuestra librería. Para eso, añadimos el identificador ON_DOUBLE_CLICK al archivo Defines.mqh para el evento del doble clic izquierdo del ratón:

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
...
#define ON_DOUBLE_CLICK             (34) // Doble clic izquierdo del ratón
...

El evento con el identificador ON_DOUBLE_CLICK va a generarse en la clase CMouse. En el Sistema operativo, este evento suele generarse al hacer las acciones «pulsar-soltar-pulsar» con el botón izquierdo del ratón. Pero el testeo en el entorno del terminal ha mostrado que no siempre se puede detectar momentáneamente el evento del clic izquierdo del ratón cuando llega el evento CHARTEVENT_MOUSE_MOVE (parámetro sparam). Por eso, se ha decidido implementar la generación del evento a través de las acciones «pulsar-soltar-pulsar-soltar». Es posible detectar con precisión estas acciones cuando ocurre el evento CHARTEVENT_CLICK

Por defecto, debe pasar no menos de 300 milisegundos entre dos pulsaciones. Con el fin de seguir el evento CHARTEVENT_CLICK, en el manejador de eventos del ratón va a invocarse el método CMouse::CheckDoubleClick(). En el inicio de este método, se declaran dos variables estáticas, en los cuales, cada vez que se llame al método, van a actualizarse los valores del tick anterior y actual (número de milisegundos transcurridos desde el momento del inicio del sistema). Si entre estos valores hay menos milisegundos que figuran en el campo m_pause_between_clicks, entonces se generará el evento del doble clic del ratón. ‌

//+------------------------------------------------------------------+
//| Clase para obtener los parámetros del ratón                      |
//+------------------------------------------------------------------+
class CMouse
  {
private:
  Pausa entre las pulsaciones con el botón izquierdo del ratón (para determinar el doble clic)
   uint              m_pause_between_clicks;
   //---
private:
   //--- Comprobación del doble clic izquierdo del ratón
   bool              CheckDoubleClick(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CMouse::CMouse(void) : m_pause_between_clicks(300)
  {
...
  }
//+------------------------------------------------------------------+
//| Procesamiento de eventos del ratón                               |
//+------------------------------------------------------------------+
void CMouse::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- Procesamiento del evento del clic en el gráfico
   if(id==CHARTEVENT_CLICK)
     {
      //--- Comprobamos el doble clic izquierdo del ratón
      CheckDoubleClick();
      return;
     }
  }
//+------------------------------------------------------------------+
//| Comprobación del doble clic izquierdo del ratón                  |
//+------------------------------------------------------------------+
void CMouse::CheckDoubleClick(void)
  {
   static uint prev_depressed =0;
   static uint curr_depressed =::GetTickCount();
//--- Actualizamos los valores
   prev_depressed =curr_depressed;
   curr_depressed =::GetTickCount();
//--- Determinamos el tiempo entre las pulsaciones
   uint counter=curr_depressed-prev_depressed;
//--- Si entre las pulsaciones ha pasado menos tiempo del que está especificado, enviamos el mensaje sobre el doble clic
   if(counter<m_pause_between_clicks)
      ::EventChartCustom(m_chart.ChartId(),ON_DOUBLE_CLICK,counter,0.0,"");
  }

De esta manera, en los manejadores de eventos de todas las clases de la librería se puede monitorear el doble clic izquierdo del ratón en cualquier lugar del área del gráfico, independientemente de que si haya o no un objeto gráfico debajo del cursor. 


Controles en las celdas de la tabla

En este artículo, vamos a abrir el tema de los controles en las celdas de la tabla. Por ejemplo, esta posibilidad puede ser oportuna cuando es necesario crear un sistema comercial de parámetros múltiples. Es conveniente implementar la interfaz gráfica de este sistema en forma de una tabla. Vamos a empezar a insertar estos controles en las celdas de la tabla, comenzando con las casillas de verificación (checkbox) y los botones. 

En primer lugar, nos hará falta la nueva enumeración ENUM_TYPE_CELL, donde van a determinarse los tipos de las celdas:

//+------------------------------------------------------------------+
//| Enumeración de los tipos de las celdas                           |
//+------------------------------------------------------------------+
enum ENUM_TYPE_CELL
  {
   CELL_SIMPLE   =0,
   CELL_BUTTON   =1,
   CELL_CHECKBOX =2
  };

Por defecto, durante la inicialización, a las celdas se les asigna el tipo simple, CELL_SIMPLE, lo que significa «celda sin control». Para establecer u obtener el tipo de la celda, utilice los métodos CCanvasTable::CellType(), cuyo código se muestra a continuación. Una vez establecido el tipo CELL_CHECKBOX (chechbox) para la celda, también hay que establecer los iconos para el estado del checkbox. El número mínimo de iconos para este tipo de celdas son dos. Se puede establecer más de dos imágenes, entonces habrá posibilidad de trabajar con los checkbox de parámetros múltiples.

class CCanvasTable : public CElement
  {
   //--- Establecimiento/obtención del tipo de la celda
   void              CellType(const uint column_index,const uint row_index,const ENUM_TYPE_CELL type);
   ENUM_TYPE_CELL    CellType(const uint column_index,const uint row_index);
  };
//+------------------------------------------------------------------+
//| Establece el tipo de la celda                                    |
//+------------------------------------------------------------------+
void CCanvasTable::CellType(const uint column_index,const uint row_index,const ENUM_TYPE_CELL type)
  {
//--- Comprobar la superación del rango
   if(!CheckOutOfRange(column_index,row_index))
      return;
//--- Establecer el tipo de la celda
   m_columns[column_index].m_rows[row_index].m_type=type;
  }
//+------------------------------------------------------------------+
//| Obtención del tipo de la celda                                   |
//+------------------------------------------------------------------+
ENUM_TYPE_CELL CCanvasTable::CellType(const uint column_index,const uint row_index)
  {
//--- Comprobar la superación del rango
   if(!CheckOutOfRange(column_index,row_index))
      return(WRONG_VALUE);
//--- Devolver el tipo de datos para la columna especificada
   return(m_columns[column_index].m_rows[row_index].m_type);
  }

Las series de los iconos para los checkbox y botones pueden ser de tamaños diferentes en cada columna, por eso se necesita la posibilidad de establecer los márgenes X y Y de los iconos para cada columna individualmente. Se puede hacer eso usando los métodos CCanvasTable::ImageXOffset() y CCanvasTable::ImageYOffset():‌

class CCanvasTable : public CElement
  {
public:
   //--- Desplazamiento de imágenes por los ejes X y Y
   void              ImageXOffset(const int &array[]);
   void              ImageYOffset(const int &array[]);
  };

Otro complemento es un modo cuya activación permitirá excluir la cancelación de la selección de la fila en caso de la pulsación reiterativa. Este modo tendrá efecto sólo si el modo de la selección de la fila está activado.

class CCanvasTable : public CElement
  {
private:
   //--- Sin cancelación de la selección de la fila cuando se vuelve a pulsar
   bool              m_is_without_deselect;
   //---
public:
   //--- Modo «Sin cancelación de la selección de la fila cuando se vuelve a pulsar»
   void              IsWithoutDeselect(const bool flag)   { m_is_without_deselect=flag;      }
  };

Para determinar el índice de la fila o columna que ha sido pulsada, ahora se utilizan los métodos separados CCanvasTable::PressedRowIndex() y CCanvasTable::PressedCellColumnIndex(). Ya han sido considerados antes como bloques del método CCanvasTable::OnClickTable(). Puede estudiar su código completo con nuevas adiciones en los archivos adjuntos al artículo. Sólo quería subrayar que en total, a través de estos métodos se puede detectar la celda en la que ha sido hecho el clic izquierdo. A continuación, vamos a ver a dónde serán enviados los índices de la columna y fila obtenidos.

class CCanvasTable : public CElement
  {
private:
   //--- Devuelve el índice de la fila pulsada
   int               PressedRowIndex(void);
   //--- Devuelve el índice de la columna con la celda pulsada
   int               PressedCellColumnIndex(void);
  };

Al hacer clic en la celda de la tabla, hay que obtener su tipo, y si contiene un control, también es necesario determinar si ha sido activado. Para este propósito, ha sido implementada una serie de métodos privados, el principal de los cuales es CCanvasTable::CheckCellElement(). Precisamente, a este método se traspasan los índices de la columna y fila obtenidos a través de los métodos CCanvasTable::PressedRowIndex() y CCanvasTable::PressedCellColumnIndex(). Este método es de dos modos. Dependiendo del evento cuyo procesamiento acciona la llamada a este método, se utiliza el tercer parámetro que permite especificar si este evento ha sido un doble clic del ratón.

Luego, se verifica el tipo de la celda, y en función de su tipo, se llama al método correspondiente. Si la celda contiene el control «Botón», se invocará el método CCanvasTable::CheckPressedButton(). Para las celdas con checkbox se usa el método CCanvasTable::CheckPressedCheckBox().‌

class CCanvasTable : public CElement
  {
private:
   //--- Comprueba si ha sido empleado el control de la celda al hacer clic
   bool              CheckCellElement(const int column_index,const int row_index,const bool double_click=false);
  };
//+-------------------------------------------------------------------------+
//| Comprueba si ha sido empleado el control de la celda al hacer clic      |
//+-------------------------------------------------------------------------+
bool CCanvasTable::CheckCellElement(const int column_index,const int row_index,const bool double_click=false)
  {
//--- Salir si en la celda no hay control
   if(m_columns[column_index].m_rows[row_index].m_type==CELL_SIMPLE)
      return(false);
//---
   switch(m_columns[column_index].m_rows[row_index].m_type)
     {
      //--- Si es una celda con botón
      case CELL_BUTTON :
        {
         if(!CheckPressedButton(column_index,row_index,double_click))
            return(false);
         //---
         break;
        }
      //--- Si es una celda con checkbox
      case CELL_CHECKBOX :
        {
         if(!CheckPressedCheckBox(column_index,row_index,double_click))
            return(false);
         //---
         break;
        }
     }
//---
   return(true);
  }

Vamos a ver cómo están organizados los métodos CCanvasTable::CheckPressedButton() y CCanvasTable::CheckPressedCheckBox(). Al principio de ambos métodos se encuentra la comprobación del número de imágenes en la celda. En las celdas de botones debe haber como mínimo un icono, en las celdas de checkbox, como mínimo dos iconos. Luego, hay que determinar si ha tenido lugar el clic precisamente en este icono. En caso con checkbox, implementamos dos modos de su conmutación. Una pulsación solitaria va a tener efecto sólo si hemos clic en el icono con la imagen del checkbox. Un doble clic conmuta el checkbox en cualquier parte de la celda. En ambos métodos, cuando todas las condiciones están cumplidas, se genera el evento con el identificador correspondiente al control. Como parámetro double se envía el índice de la imagen, mientras que en el parámetro string se forma una cadena con los índices de la columna y la fila.‌

class CCanvasTable : public CElement
  {
private:
   //--- Comprueba si el clic ha sido hecho en el botón de la celda
   bool              CheckPressedButton(const int column_index,const int row_index,const bool double_click=false);
   //--- Comprueba si el clic ha sido hecho en el checkbox de la celda
   bool              CheckPressedCheckBox(const int column_index,const int row_index,const bool double_click=false);
  };
//+------------------------------------------------------------------+
//| Comprueba si el clic ha sido hecho en el botón de la celda       |
//+------------------------------------------------------------------+
bool CCanvasTable::CheckPressedButton(const int column_index,const int row_index,const bool double_click=false)
  {
//--- Salir si en la celda no hay imágenes
   if(ImagesTotal(column_index,row_index)<1)
     {
      ::Print(__FUNCTION__," > ¡Establezca como mínimo un icono para la celda de botón!");
      return(false);
     }
//--- Obtenemos las coordenadas relativas debajo del cursor del ratón
   int x=m_mouse.RelativeX(m_table);
//--- Obtenemos el borde derecho de la imagen
   int image_x  =int(m_columns[column_index].m_x+m_columns[column_index].m_image_x_offset);
   int image_x2 =int(image_x+m_columns[column_index].m_rows[row_index].m_images[0].m_image_width);
//--- Salir si el clic no ha sido hecho en la imagen
   if(x>image_x2)
      return(false);
   else
     {
      //--- Si no es el doble clic, enviamos el mensaje
      if(!double_click)
        {
         int image_index=m_columns[column_index].m_rows[row_index].m_selected_image;
         ::EventChartCustom(m_chart_id,ON_CLICK_BUTTON,CElementBase::Id(),image_index,string(column_index)+"_"+string(row_index));
        }
     }
//---
   return(true);
  }
//+------------------------------------------------------------------+
//| Comprueba si el clic ha sido hecho en el checkbox de la celda    |
//+------------------------------------------------------------------+
bool CCanvasTable::CheckPressedCheckBox(const int column_index,const int row_index,const bool double_click=false)
  {
//--- Salir si en la celda no hay imágenes
   if(ImagesTotal(column_index,row_index)<2)
     {
      ::Print(__FUNCTION__," > ¡Establezca como mínimo dos iconos para la celda de checkbox!");
      return(false);
     }
//--- Obtenemos las coordenadas relativas debajo del cursor del ratón
   int x=m_mouse.RelativeX(m_table);
//--- Obtenemos el borde derecho de la imagen
   int image_x  =int(m_columns[column_index].m_x+m_image_x_offset);
   int image_x2 =int(image_x+m_columns[column_index].m_rows[row_index].m_images[0].m_image_width);
//--- Salir si (1) el clic no ha sido hecho en la imagen y (2) no es el doble clic
   if(x>image_x2 && !double_click)
      return(false);
   else
     {
      //--- Indice actual de la imagen seleccionada
      int image_i=m_columns[column_index].m_rows[row_index].m_selected_image;
      //--- Determinamos el siguiente índice para la imagen
      int next_i=(image_i<ImagesTotal(column_index,row_index)-1)? ++image_i : 0;
      //--- Seleccionar la siguiente imagen y actualizar la tabla
      ChangeImage(column_index,row_index,next_i,true);
      m_table.Update(false);
      //--- Enviamos el mensaje sobre ello
      ::EventChartCustom(m_chart_id,ON_CLICK_CHECKBOX,CElementBase::Id(),next_i,string(column_index)+"_"+string(row_index));
     }
//---
   return(true);
  }

Como resultado, el código de los métodos CCanvasTable::OnClickTable() y CCanvasTable::OnDoubleClickTable() que procesan el clic en la tabla ahora es mucho más claro y legible (véase el código de abajo). Dependiendo de los modos activados y el tipo de la celda pulsada, se genera el evento correspondiente.

class CCanvasTable : public CElement
  {
private:
  //--- Procesamiento del clic en la tabla
   bool              OnClickTable(const string clicked_object);
  //--- Procesamiento del doble clic en la tabla
   bool              OnDoubleClickTable(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| Procesamiento del clic en la tabla                               |
//+------------------------------------------------------------------+
bool CCanvasTable::OnClickTable(const string clicked_object)
  {
//--- Salir si (1) el modo de selección de la fila está desactivado o (2) se ejecuta el proceso del cambio del ancho de la columna
   if(m_column_resize_control!=WRONG_VALUE)
      return(false);
//--- Salimos si la barra de desplazamiento se encuentra en modo activo
   if(m_scrollv.ScrollState() || m_scrollh.ScrollState())
      return(false);
//--- Salimos si el nombre del objeto no coincide
   if(m_table.Name()!=clicked_object)
      return(false);
//--- Determinamos la fila en la que hemos pulsado
   int r=PressedRowIndex();
//--- Determinamos la celda en la que hemos pulsado
   int c=PressedCellColumnIndex();
//--- Comprobamos si ha sido empleado el control en la celda
   bool is_cell_element=CheckCellElement(c,r);
//--- Si (1) el modo de selección de la fila está activado y (2) el control en la celda no ha sido empleado
   if(m_selectable_row && !is_cell_element)
     {
      //--- Cambiar el color
      RedrawRow(true);
      m_table.Update();
      //--- Enviamos el mensaje sobre ello
      ::EventChartCustom(m_chart_id,ON_CLICK_LIST_ITEM,CElementBase::Id(),m_selected_item,string(c)+"_"+string(r));
     }
//---
   return(true);
  }
//+------------------------------------------------------------------+
//| Procesamiento del doble clic en la tabla                         |
//+------------------------------------------------------------------+
bool CCanvasTable::OnDoubleClickTable(const string clicked_object)
  {
   if(!m_table.MouseFocus())
      return(false);
//--- Determinamos la fila en la que hemos pulsado
   int r=PressedRowIndex();
//--- Determinamos la celda en la que hemos pulsado
   int c=PressedCellColumnIndex();
//--- Comprobamos si ha sido empleado el control en la celda y devolvemos el resulltado
   return(CheckCellElement(c,r,true));
  }

El procesamiento del doble clic en la celda, se realiza en el manejador del control cuando ocurre el evento ON_DOUBLE_CLICK

//+------------------------------------------------------------------+
//| Manejador de eventos                                             |
//+------------------------------------------------------------------+
void CCanvasTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- Procesamiento del doble clic izquierdo del ratón
   if(id==CHARTEVENT_CUSTOM+ON_DOUBLE_CLICK)
     {
      //--- Clic en la tabla
      if(OnDoubleClickTable(sparam))
         return;
      //---
      return;
     }
  }

Al fin y al cabo, todo funciona así:

 Fig. 3. Demostración de la interacción con los controles en las celdas de la tabla.

Fig. 3. Demostración de la interacción con los controles en las celdas de la tabla.


Use los archivos adjuntos para descargar las aplicaciones demostradas en este artículo a su ordenador.


Conclusión

En esta fase del desarrollo de la librería para la creación de las interfaces gráficas, su esquema tiene el siguiente aspecto:

 Fig. 4. Estructura de la librería en la fase actual del desarrollo.

Fig. 4. Estructura de la librería en la fase actual del desarrollo.


Abajo puede descargar la última versión de la librería y los archivos para las pruebas.

Si le surgen algunas preguntas sobre el uso del material de estos archivos, puede dirigirse a la descripción detallada del proceso de desarrollo de la librería en uno de los artículos de esta serie, o bien hacer su pregunta en los comentarios para el artículo. 

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

Archivos adjuntos |
Interfaces gráficas X: Algoritmo del traslado de palabras en el campo de edición multilínea (build 12) Interfaces gráficas X: Algoritmo del traslado de palabras en el campo de edición multilínea (build 12)
Sigamos desarrollando el control «Campo de edición multilínea». Esta vez, nuestra tarea consiste en configurar el traslado automático de palabras a la siguiente línea si no encajan en el campo de edición, o el traslado inverso a la línea anterior si aparece esta posibilidad.
¡Visualice esto! La biblioteca gráfica en MQL5 como un análogo de plot en el lenguaje R ¡Visualice esto! La biblioteca gráfica en MQL5 como un análogo de plot en el lenguaje R
A la hora de investigar y estudiar patrones, la representación visual con la ayuda de gráficos juega un papel fundamental. En los lenguajes populares de programación en la comunidad científica, tales como R y Python, para la visualización se usa la función especial plot. Con su ayuda, es posible dibujar líneas, gráficos de dispersión e histogramas para visualizar patrones. En MQL5 usted puede hacer lo mismo con la ayuda de la clase CGraphics.
Cálculo del coeficiente de Hurst Cálculo del coeficiente de Hurst
En este artículo se explica detalladamente el sentido del exponente de Hurst, la interpretación de sus valores y el algoritmo del cálculo. Se muestran los resultados del análisis de algunos segmentos de los mercados financieros y se presenta el método de trabajo con los productos informáticos de MetaTrader 5 que implementan la idea del análisis fractal.
ZUP - zigzag universal con patrones de Pesavento. Interfaz gráfica ZUP - zigzag universal con patrones de Pesavento. Interfaz gráfica
Durante diez años pasados desde el momento del lanzamiento de la primera versión de la plataforma ZUP, había muchas actualizaciones y mejoras. Como resultado, tenemos a nuestra disposición un complemento gráfico único para MetaTrader 4, que permite realizar el análisis de la información del mercado de una manera rápida y cómoda. Este artículo nos cuenta sobre cómo se puede trabajar con la interfaz gráfica de la plataforma ZUP de indicadores.