Interfaces gráficas X: Actualizaciones para la tabla dibujada y optimización del código (build 10)

Anatoli Kazharski | 6 marzo, 2017


Í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.

Seguiremos completando la tabla dibujada (CCanvasTable) con nuevas funcionalidades. Esta vez, vamos a añadir las siguientes posibilidades:

  • Resalto de la fila de la tabla al situar el cursor encima.
  • Posibilidad de agregar el array de imágenes para cada celda y el método para su conmutación.
  • Posibilidad de establecer y editar el texto de las celdas durante la ejecución del programa. 

Aparte de eso, optimizaremos el código y algunos algoritmos para acelerar el redibujo de la tabla. 

 

Coordenadas relativas del cursor en el lienzo especificado para el dibujo

Con el fin de evitar el código repetitivo en muchos métodos y clases para el cálculo de las coordenadas relativas en el lienzo para el dibujo, ahora la clase CMouse va a incluir los métodos adicionales CMouse::RelativeX() y CMouse::RelativeY() para obtener estas coordenadas. La referencia al objeto tipo CRectCanvas debe ser pasada a estos métodos para poder calcular la coordenada relativa, tomando en cuenta el desplazamiento actual del área de visibilidad del lienzo para el dibujo.

//+-------------------------------------------------------------------+
//| Clase para obtener los parámetros del ratón                      |
//+------------------------------------------------------------------+
class CMouse
  {
public:
   //--- Devuelve las coordenadas relativas del cursor del objeto del lienzo pasado para el dibujo
   int               RelativeX(CRectCanvas &object);
   int               RelativeY(CRectCanvas &object);
  };
//+------------------------------------------------------------------+
//| Devuelve la coordenada relativa X del cursor del ratón           |
//| del objeto del lienzo pasado para el dibujo                      |
//+------------------------------------------------------------------+
int CMouse::RelativeX(CRectCanvas &object)
  {
   return(m_x-object.X()+(int)object.GetInteger(OBJPROP_XOFFSET));
  }
//+------------------------------------------------------------------+
//| Devuelve la coordenada relativa Y del cursor del ratón           |
//| del objeto del lienzo pasado para el dibujo                      |
//+------------------------------------------------------------------+
int CMouse::RelativeY(CRectCanvas &object)
  {
   return(m_y-object.Y()+(int)object.GetInteger(OBJPROP_YOFFSET));
  }

Durante el proceso del desarrollo de nuestra librería, estos métodos van a usarse para obtener las coordenadas relativas de todos los controles dibujados. 

 

Cambios en la estructura de la tabla

Para optimizar al máximo la ejecución del código de la tabla dibujada, tenía que modificar un poco y completar la estructura de la tabla tipo CTOptions, así como añadir nuevas estructuras que permiten construir los arrays multidimensionales. Nuestra tarea consiste en hacer que algunos fragmentos de la tabla se redibujen según los valores calculados anteriormente. Por ejemplo, puede tratarse de las coordenadas de los límites de columnas y filas.

Como ejemplo, calcular y guardar las coordenadas X de los límites de las columnas conviene en el método CCanvasTable::DrawGrid(), que se usa para trazar la cuadrícula, y solamente al dibujar la tabla entera. Y cuando el usuario selecciona alguna fila de la tabla, se puede usar los valores ya preparados. Lo mismo se refiere al resalto de las filas de la tabla al situar el cursor encima (lo veremos a continuación). 

Para guardar las coordenadas Y de las filas de la tabla, y tal vez en el futuro, otras propiedades de las filas, crearemos una estructura separada (CTRowOptions) y declararemos el array de sus instancias. Las coordenadas Y de las filas se calculan en el método CCanvasTable::DrawRows(), que sirve para dibujar el fondo de las filas. Puesto que este método se invoca antes del trazado de la cuadrícula, en el método CCanvasTable::DrawGrid() se utilizan los valores ya calculados desde la estructura CTRowOptions

Para almacenar los valores y las propiedades de las celdas de la tabla, vamos a crear una estructura separada tipo CTCell. Precisamente con este tipo se declara el array de las instancias en la estructura CTRowOptions, como array de las filas de la tabla. En esta estructura van a almacenarse:

  • Arrays de imágenes
  • Arrays de tamaños de imágenes
  • Indice de la imagen seleccionada (mostrada) en la celda
  • Texto completo
  • Texto reducido
  • Color del texto

Puesto que cada imagen representa un array de los píxeles, necesitaremos una estructura separada (CTImage) con el array dinámico para su almacenamiento. A continuación, se muestra el código de las estructuras mencionadas:

class CCanvasTable : public CElement
  {
private:
   //--- Array de los píxeles de la imagen
   struct CTImage { uint m_image_data[]; };
   //--- Propiedades de las celdas de la tabla
   struct CTCell
     {
      CTImage           m_images[];       // Arrays de imágenes
      uint              m_image_width[];  // Array del ancho de las imágenes
      uint              m_image_height[]; // Array del alto de las imágenes
      int               m_selected_image; // indice de la imagen seleccionada (mostrada)
      string            m_full_text;      // Texto completo
      string            m_short_text;     // Texto reducido
      color             m_text_color;     // Color del texto
     };
   //--- Array de las filas y propiedades de las columnas de la tabla
   struct CTOptions
     {
      int               m_x;             // Coordenada X del lado izquierdo de la columna
      int               m_x2;            // Coordenada X del lado derecho de la columna

      int               m_width;         // Ancho de la columna
      ENUM_ALIGN_MODE   m_text_align;    // Modo de alineación del texto en las celdas de la columna
      int               m_text_x_offset; // Sangrado del texto
      string            m_header_text;   // Texto del encabezado de la columna
      CTCell            m_rows[];        // Array de las filas de la tabla
     };
   CTOptions         m_columns[];
   //--- Array de las propiedades de las filas de la tabla
   struct CTRowOptions
     {
      int               m_y;  // Coordenada Y del lado superior de la fila
      int               m_y2; // Coordenada Y del lado inferior de la fila
     };
   CTRowOptions      m_rows[];

  };

Las correcciones correspondientes han sido introducidas en todos los métodos donde se utilizan estos tipos de datos. 

 

Determinación del rango de las filas dentro del área de visibilidad

Dado que la tabla puede contener un gran numero de filas, la búsqueda del foco en una de las filas con el posterior redibujo de la tabla puede ralentizar considerablemente el proceso. Lo mismo se refiere a la selección de la fila y a la corrección del largo del texto al cambiar el ancho de la columna manualmente. Para evitar la ralentización, es necesario determinar el primer y el último índice en el área visible de la tabla y organizar el ciclo del repaso precisamente en este rango. Para eso disponemos del método CCanvasTable::VisibleTableIndexes(). Primero, en este método se determinan los límites del área visible. El límite superior es el desplazamiento del área visible por el eje Y, y el límite inferior se determina como el límite superior + el tamaño del área visible por el eje Y.

Para determinar los índices de la fila superior e inferior del área visible de la tabla, será suficiente dividir los valores de los límites obtenidos por el alto de la fila establecido en las propiedades de la tabla. Al final del método, se realiza la corrección si hemos salido fuera de los límites de la última fila de la tabla.

class CCanvasTable : public CElement
  {
private:
  //--- Para determinar los índices de la parte visible de la tabla
   int               m_visible_table_from_index;
   int               m_visible_table_to_index;
   //---
private:
  //--- Determinación de los índices del área visible de la tabla
   void              VisibleTableIndexes(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CCanvasTable::CCanvasTable(void) : m_visible_table_from_index(WRONG_VALUE),
                                   m_visible_table_to_index(WRONG_VALUE)
  {
...
  }
//+------------------------------------------------------------------+
//| Determinación de los índices del área visible de la tabla        |
//+------------------------------------------------------------------+
void CCanvasTable::VisibleTableIndexes(void)
  {
//--- Determinamos los límites teniendo en cuenta el desplazamiento del área visible de la tablla
   int yoffset1 =(int)m_table.GetInteger(OBJPROP_YOFFSET);
   int yoffset2 =yoffset1+m_table_visible_y_size;
//--- Determinamos el primer y el último índice del área visible de la tabla
   m_visible_table_from_index =int(double(yoffset1/m_cell_y_size));
   m_visible_table_to_index   =int(double(yoffset2/m_cell_y_size));

//--- El índice inferior es más grande a uno si no salimos fuera del rango
   m_visible_table_to_index=(m_visible_table_to_index+1>m_rows_total)? m_rows_total : m_visible_table_to_index+1;
  }

La determinación de los índices va a realizarse en el método CCanvasTable::DrawTable(). Ahora a este método se le puede pasar el argumento para indicar que es necesario redibujar solamente la parte visible de la tabla. Por defecto, el valor del argumento es igual a false, lo que indica en el redibujo de la tabla entera. Abajo se muestra la versión reducida de este método.

//+-------------------------------------------------------------------+
//| Dibuja la tabla                                                  |
//+------------------------------------------------------------------+
void CCanvasTable::DrawTable(const bool only_visible=false)
  {
//--- Si no está indicado redibujar sólo la parte visible de la tabla
   if(!only_visible)
     {
      //--- Determinamos los índices de las filas de la tabla entera desde el principio hasta el final
      m_visible_table_from_index =0;
      m_visible_table_to_index   =m_rows_total;

     }
//--- Obtenemos los índices de las filas de la parte visible de la tabla
   else
      VisibleTableIndexes();
//--- Dibujar el fondo de las filas de la tabla
//--- Dibujar la fila seleccionada
//--- Dibujar la cuadrícula
//--- Dibujar la imagen
//--- Dibujar el texto
//--- Mostrar los últimos cambios dibujados
//--- Actualizar los encabezados si están activados
//--- Corrección de la tabla respecto a la barras de desplazamiento
  }

La llamada al método CCanvasTable::VisibleTableIndexes() también es necesaria en el método para la determinación del foco en las filas de la tabla: 

//+-------------------------------------------------------------------+
//| Comprobación del foco en las filas de la tabla                   |
//+------------------------------------------------------------------+
int CCanvasTable::CheckRowFocus(void)
  {
   int item_index_focus=WRONG_VALUE;
//--- Obtenemos la coordenada relativa Y debajo del cursor del ratón
   int y=m_mouse.RelativeY(m_table);
///--- Obtenemos los índices del área local de la tabla
   VisibleTableIndexes();
//--- Buscamos el foco
   for(int i=m_visible_table_from_index; i<m_visible_table_to_index; i++)
     {
      //--- Si el foco de la fila ha cambiado
      if(y>m_rows[i].m_y && y<=m_rows[i].m_y2)
        {
         item_index_focus=i;
         break;
        }
     }
//--- Devolvemos el índice de la fila en el foco
   return(item_index_focus);
  }

 

 

Imágenes en las celdas de la tabla

A cada celda se le puede vincular varias imágenes que se podrá ir cambiando en el proceso de ejecución del programa. Vamos a añadir los campos y los métodos para establecer los márgenes de la imagen desde el lado izquierdo de la celda:

class CCanvasTable : public CElement
  {
private:
   //--- Márgenes para las imágenes desde los bordes de la celda
   int               m_image_x_offset;
   int               m_image_y_offset;
   //---
public:
   //--- Márgenes para las imágenes desde los bordes de la celda
   void              ImageXOffset(const int x_offset)     { m_image_x_offset=x_offset;       }
   void              ImageYOffset(const int y_offset)     { m_image_y_offset=y_offset;       }
  };

Para colocar las imágenes dentro de la celda especificada, hay que pasar el array con su ubicación en el directorio local del terminal. Antes de eso,  deben estar conectadas a la aplicación MQL como recursos (#resource). Para eso se utiliza el método CCanvasTable::SetImages(). Aquí, si ha sido pasado el array vacío o se ha detectado la salida fuera de los límites del array, el programa sale del método.

Si las comprobaciones han sido superadas, a los arrays de la celda se le asigna nuevo tamaño y luego, usando el método ::ResourceReadImage(), leemos en el ciclo el contenido de la imagen al array unidimensional, guardando dentro los colores de todos los píxeles. Los tamaños de la imagen se guardan en los arrays correspondientes. Serán necesarios para organizar los ciclos en los que las imágenes van a dibujarse sobre el lienzo. Por defecto, en la celda se elegirá la primera imagen del array.

class CCanvasTable : public CElement
  {
public:
   //--- Coloca las imágenes en la celda especificada
   void              SetImages(const uint column_index,const uint row_index,const string &bmp_file_path[]);
  };
//+------------------------------------------------------------------+
//| Coloca las imágenes en la celda especificada                     |
//+------------------------------------------------------------------+
void CCanvasTable::SetImages(const uint column_index,const uint row_index,const string &bmp_file_path[])
  {
   int total=0;
//--- Salir si ha sido pasado el array con el tamaño cero
   if((total=CheckArraySize(bmp_file_path))==WRONG_VALUE)
      return;
//--- Comprobar la superación del rango
   if(!CheckOutOfRange(column_index,row_index))
      return;
//--- Establecer nuevo tamaño para los arrays
   ::ArrayResize(m_columns[column_index].m_rows[row_index].m_images,total);
   ::ArrayResize(m_columns[column_index].m_rows[row_index].m_image_width,total);
   ::ArrayResize(m_columns[column_index].m_rows[row_index].m_image_height,total);
//---
   for(int i=0; i<total; i++)
     {
      Por defecto, se elige la primera imagen del array
.       m_columns[column_index].m_rows[row_index].m_selected_image=0;
      //--- Guardar la imagen traspasada en el array y recordar sus tamaños
      if(!ResourceReadImage(bmp_file_path[i],m_columns[column_index].m_rows[row_index].m_images[i].m_image_data,
         m_columns[column_index].m_rows[row_index].m_image_width[i],
         m_columns[column_index].m_rows[row_index].m_image_height[i]))

        {
         Print(__FUNCTION__," > error: ",GetLastError());
         return;
        }
     }
  }

Para saber cuántas imágenes contiene una u otra celda, usamos el método CCanvasTable::ImagesTotal():

class CCanvasTable : public CElement
  {
public:
   //--- Devuelve el número de imágenes dentro de la celda especificada
   int               ImagesTotal(const uint column_index,const uint row_index);
  };
//+------------------------------------------------------------------+
//| Devuelve el número de imágenes dentro de la celda especificada   |
//+------------------------------------------------------------------+
int CCanvasTable::ImagesTotal(const uint column_index,const uint row_index)
  {
//--- Comprobar la superación del rango
   if(!CheckOutOfRange(column_index,row_index))
      return(WRONG_VALUE);
//--- Devolvemos el tamaño del array de imágenes
   return(::ArraySize(m_columns[column_index].m_rows[row_index].m_images));
  }

Ahora veremos los métodos que van a aplicarse para dibujar las imágenes. En primer lugar, a la clase CColors ha sido añadido el nuevo método CColors::BlendColors() que permitirá mezclar correctamente el color superior e inferior, tomando en cuenta la transparencia de la imagen solapada encima. También ha sido agregado el método adicional CColors::GetA() para obtener el valor de la transparencia del color pasado.

En el método CColors::BlendColors(), los colores pasados primero se dividen en los componentes RGB, y del color superior se extrae el canal alfa. El canal alfa se convierte en los valores de cero a uno. Si en el color pasado no hay transparencia, el mezclado no se realiza. En caso si hay transparencia, cada componente de dos colores pasados se mezcla tomando en cuenta la transparencia del color superior. Después de eso, los valores de los componentes obtenidos se corrigen, en caso de salir fuera del diapasón (255). 

//+-------------------------------------------------------------------+
//| Clase para trabajar con el color                                 |
//+------------------------------------------------------------------+
class CColors
  {
public:
   double            GetA(const color aColor);
   color             BlendColors(const uint lower_color,const uint upper_color);
  };
//+------------------------------------------------------------------+
//| Obtención del valor del componente A                             |
//+------------------------------------------------------------------+
double CColors::GetA(const color aColor)
  {
   return(double(uchar((aColor)>>24)));
  }
//+----------------------------------------------------------------------------------------+
//| Mezcla de dos colores tomando en cuenta la transparencia del color superior            |
//+----------------------------------------------------------------------------------------+
color CColors::BlendColors(const uint lower_color,const uint upper_color)
  {
   double r1=0,g1=0,b1=0;
   double r2=0,g2=0,b2=0,alpha=0;
   double r3=0,g3=0,b3=0;
//--- Conversión de color en el formato ARGB
   uint pixel_color=::ColorToARGB(upper_color);
//--- Obtenemos los componentes del color inferior y superior
   ColorToRGB(lower_color,r1,g1,b1);
   ColorToRGB(pixel_color,r2,g2,b2);
//--- Obtenemos el porciento de 0,00 a 1,00
   alpha=GetA(upper_color)/255.0;
//--- Si hay transparencia
   if(alpha<1.0)
     {
      //--- Mezclamos los componentes tomando en cuenta el canal alfa
      r3=(r1*(1-alpha))+(r2*alpha);
      g3=(g1*(1-alpha))+(g2*alpha);
      b3=(b1*(1-alpha))+(b2*alpha);

      //--- Corrección de valores obtenidos
      r3=(r3>255)? 255 : r3;
      g3=(g3>255)? 255 : g3;
      b3=(b3>255)? 255 : b3;
     }
   else
     {
      r3=r2;
      g3=g2;
      b3=b2;
     }
//--- Juntar los componentes obtenidos y devolver el color
   return(RGBToColor(r3,g3,b3));
  }

Ahora, no es tan complicado escribir el método para dibujar la imagen. Abajo se muestra el código del método CCanvasTable::DrawImage(). Hay que pasarle los índices de la celda de la tabla en la que es necesario dibujar la imagen. Al principio del método, obtenemos las coordenadas de la imagen teniendo en cuenta los márgenes, así como el índice de la celda seleccionada y sus tamaños. Luego, la imagen se muestra en el ciclo píxel por píxel. Si el píxel especificado está vacío, es decir no tiene color, se produce el cambio al siguiente. Si hay color, determinamos el color del fondo de la celda y el color del píxel actual, y luego mezclamos estos colores, tomando en cuenta la transparencia del color solapado, y dibujamos el color obtenido en el lienzo.

class CCanvasTable : public CElement
  {
private:
   //--- Dibuja la imagen dentro de la celda especificada
   void              DrawImage(const int column_index,const int row_index);
  };
//+------------------------------------------------------------------+
//| Dibuja la imagen dentro de la celda especificada                 |
//+------------------------------------------------------------------+
void CCanvasTable::DrawImage(const int column_index,const int row_index)
  {
//--- Cálculo de coordenadas
   int x =m_columns[column_index].m_x+m_image_x_offset;
   int y =m_rows[row_index].m_y+m_image_y_offset;
//--- Imagen seleccionada en la celda y sus tamaños
   int  selected_image =m_columns[column_index].m_rows[row_index].m_selected_image;
   uint image_height   =m_columns[column_index].m_rows[row_index].m_image_height[selected_image];
   uint image_width    =m_columns[column_index].m_rows[row_index].m_image_width[selected_image];
//--- Dibujamos
   for(uint ly=0,i=0; ly<image_height; ly++)
     {
      for(uint lx=0; lx<image_width; lx++,i++)
        {
         //--- Si no hay color, ir al siguiente píxel
         if(m_columns[column_index].m_rows[row_index].m_images[selected_image].m_image_data[i]<1)
            continue;

         //--- Obtenemos el color de la capa inferior (fondo de la celda) y el color del píxel indicado de la imagen
         uint background  =(row_index==m_selected_item)? m_selected_row_color : m_table.PixelGet(x+lx,y+ly);
         uint pixel_color =m_columns[column_index].m_rows[row_index].m_images[selected_image].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_table.PixelSet(x+lx,y+ly,foreground);
        }
     }
  }

Para dibujar todas las imágenes de la tabla a la vez, tomando en cuenta cuando es necesario dibujar solamente el área visible de la tabla, se usa el método CCanvasTable::DrawImages(). En la versión actual de la tabla, se puede dibujar las imágenes sólo si el texto en la columna está alineado por la izquierda. Aparte de eso, en cada iteración se comprueba si la imagen está colocada en la celda, así como si el array de sus píxeles no está vacío. Si todas las comprobaciones han sido superadas, llamamos al método CCanvasTable::DrawImage() para dibujar la imagen

class CCanvasTable : public CElement
  {
private:
   //--- Dibuja todas las imágenes de la tabla
   void              DrawImages(void);
  };
//+------------------------------------------------------------------+
//| Dibuja todas las imágenes de la tabla                            |
//+------------------------------------------------------------------+
void CCanvasTable::DrawImages(void)
  {
//--- Para calcular las coordenadas
   int x=0,y=0;
//--- Columnas
   for(int c=0; c<m_columns_total; c++)
     {
      //--- Si la alineación del texto no ha sido hecha por la izquierda, ir a la siguiente columna
      if(m_columns[c].m_text_align!=ALIGN_LEFT)
         continue;
      //--- Filas
      for(int r=m_visible_table_from_index; r<m_visible_table_to_index; r++)
        {
        //--- Ir al siguiente si en esta celda no hay imágenes
         if(ImagesTotal(c,r)<1)
            continue;
         //--- Imagen seleccionada en la celda (por defecto, está seleccionada la primera [0])
         int selected_image=m_columns[c].m_rows[r].m_selected_image;
        //--- Ir al siguiente si el array de los píxeles está vacío
         if(::ArraySize(m_columns[c].m_rows[r].m_images[selected_image].m_image_data)<1)
            continue;
         //--- Dibujar la imagen
         DrawImage(c,r);
        }
     }
  }

En la captura de pantalla de abajo se muestra la tabla con imágenes dentro de las celdas:

 Fig. 1. Tabla con imágenes dentro de las celdas.

Fig. 1. Tabla con imágenes dentro de las celdas. 


 

Resalto de la fila de la tabla al situar el cursor encima

Para que las filas de la tabla dibujada se resalten al situar el cursor encima de ellas, vamos a necesitar los campos y métodos adicionales. Utilice el método CCanvasTable::LightsHover() para activar el modo del resalto. El color de la fila puede establecerse a través del métod CCanvasTable::CellColorHover().

class CCanvasTable : public CElement
  {
private:
   //--- Color de las celdas en diferentes estados
   color             m_cell_color;
   color             m_cell_color_hover;
   //--- Modo para resaltar la fila al situar el cursor encima
   bool              m_lights_hover;
   //---
public:
   //--- Color de las celdas en diferentes estados
   void              CellColor(const color clr)           { m_cell_color=clr;                }
   void              CellColorHover(const color clr)      { m_cell_color_hover=clr;          }
   //--- Modo para resaltar las filas al situar el cursor encima
   void              LightsHover(const bool flag)         { m_lights_hover=flag;             }
  };

Para resaltar la fila, no es necesario redibujar la tabla entera una y otra vez al mover el cursor. Es más, no se recomienda insistentemente hacerlo, porque eso ralentizará considerablemente la aplicación y ocupará muchísimos recursos de la CPU. Con la primera entrada del cursor dentro del área de la tabla, será suficiente buscar el foco de la fila sólo una vez (repasando en el ciclo el array entero de las filas). Para eso se utiliza el método CCanvasTable::CheckRowFocus(). Después de encontrar el foco y guardar el índice de la fila, al mover el cursor sólo hay que comprobar si ha cambiado el foco sobre la fila con el índice guardado. El algoritmo descrito está implementado en el método CCanvasTable::ChangeRowsColor(), cuyo código se muestra más abajo. Para cambiar el color de la fila, se usa el método CCanvasTable::RedrawRow(), cuyo código conoceremos más tarde. El método CCanvasTable::ChangeRowsColor() se invoca dentro del método CCanvasTable::ChangeObjectsColor() para el cambio de los objetos de la tabla. 

class CCanvasTable : public CElement
  {
private:
   //--- Para determinar el foco de la fila
   int               m_item_index_focus;
   //--- Para determinar el momento cuando el cursor pasa de una fila a otra
   int               m_prev_item_index_focus;
   //---
private:
   //--- Cambio de color de las filas al situar el cursor encima
   void              ChangeRowsColor(void);
  };
//+------------------------------------------------------------------+
//| Cambio de color de las filas al situar el cursor encima          |
//+------------------------------------------------------------------+
void CCanvasTable::ChangeRowsColor(void)
  {
//--- Salir si el resalto de las filas al situar el cursor encima está desactivado
   if(!m_lights_hover)
      return;
//--- Si no se encuentra en el foco
   if(!m_table.MouseFocus())
     {
      //--- Si todavía no está indicado que no se encuentra en el foco
      if(m_prev_item_index_focus!=WRONG_VALUE)
        {
         m_item_index_focus=WRONG_VALUE;
         //--- Cambiar el color
         RedrawRow();
         m_table.Update();
         //--- Resetear el foco
         m_prev_item_index_focus=WRONG_VALUE;
        }
     }
//--- Si se encuentra en el foco
   else
     {
     }   //--- Comprobar el foco sobre las filas
      if(m_item_index_focus==WRONG_VALUE)
        {
        //--- Obtenemos el índice de la fila en el foco
         m_item_index_focus=CheckRowFocus();
        //--- Cambiar el color de la fila
         RedrawRow();
         m_table.Update();
         //--- Guardar como el índice anterior en el foco
         m_prev_item_index_focus=m_item_index_focus;
         return;
        }
      //--- Obtenemos la coordenada relativa Y debajo del cursor del ratón
      int y=m_mouse.RelativeY(m_table);
      //--- Comprobación del foco
      bool condition=(y>m_rows[m_item_index_focus].m_y && y<=m_rows[m_item_index_focus].m_y2);
      //--- Si el foco ha cambiado
      if(!condition)
        {
        //--- Obtenemos el índice de la fila en el foco
         m_item_index_focus=CheckRowFocus();
        //--- Cambiar el color de la fila
         RedrawRow();
         m_table.Update();
         //--- Guardar como el índice anterior en el foco
         m_prev_item_index_focus=m_item_index_focus;
        }
     }
  }

El método para redibujar rápidamente una fila de la tabla CCanvasTable::RedrawRow() trabaja en dos modos:

  •  al seleccionar la fila
  •  en el modo para resaltar la fila al situar el cursor encima.

Para indicar el modo, es necesario pasar el valor correspondiente del argumento del método. Por defecto, el argumento tiene asignado el valor false, lo que significa el uso del método en el modo del resalto de las filas de la tabla. Para ambos modos, en la clase hay campos especiales para determinar la fila seleccionada/resaltada actual y anterior. De esta manera, para marcar la siguiente fila, se redibuja sólo la fila anterior y la fila actual, en vez de la tabla entera.

El programa sale del método si los índices no están determinados (WRONG_VALUE). Luego, hay que determinar el número de índices determinados. Si es la primera entrada en la tabla y ha sido determinado un solo índice (actual), el color será cambiado, por consiguiente, sólo para una fila, la fila actual. Si volvemos a entrar en la tabla, el color va a cambiarse para las dos filas (actual y anterior). 

Ahora, hay que determinar en qué sucesión va a cambiar el color de las filas. Si el índice de la fila actual es mayor que el de la fila anterior, eso significa que el cursor se desplazará hacia abajo. Entonces, primero cambiamos el color en el índice anterior, y luego en el actual. De lo contrario, hacemos al revés. Aquí también se toma en cuenta el momento de la salida del área de la tabla, cuando el índice de la fila actual no está determinado, y el índice de la fila anterior todavía existe.

Después de que todas las variables locales para el trabajo hayan sido inicializadas, se dibuja el fondo de las filas, la cuadrícula, las imágenes y el texto, en estricta sucesión.

class CCanvasTable : public CElement
  {
private:
   //--- Redibuja la fila indicada de acuerdo con el modo especificado
   void              RedrawRow(const bool is_selected_row=false);
  };
//+------------------------------------------------------------------+
//| Redibuja la fila indicada de acuerdo con el modo especificado    |
//+------------------------------------------------------------------+
void CCanvasTable::RedrawRow(const bool is_selected_row=false)
  {
//--- Indice actual y anterior de las filas
   int item_index      =WRONG_VALUE;
   int prev_item_index =WRONG_VALUE;
//--- Inicialización de los índices de las filas respecto al modo especificado
   if(is_selected_row)
     {
      item_index      =m_selected_item;
      prev_item_index =m_prev_selected_item;
     }

   else
     {
      item_index      =m_item_index_focus;
      prev_item_index =m_prev_item_index_focus;
     }

//--- Salir si los índices no están definidos
   if(prev_item_index==WRONG_VALUE && item_index==WRONG_VALUE)
      return;
//--- Número de las filas y columnas para el dibujo
   int rows_total    =(item_index!=WRONG_VALUE && prev_item_index!=WRONG_VALUE)? 2 : 1;
   int columns_total =m_columns_total-1;
//--- Coordenadas
   int x1=1,x2=m_table_x_size;
   int y1[2]={0},y2[2]={0};
//--- Array para los valores en una determinada sucesión
   int indexes[2];
//--- Si (1) el cursor se ha desplazado hacia abajo o (2) se encuentra aquí por primera vez
   if(item_index>m_prev_item_index_focus || item_index==WRONG_VALUE)
     {
      indexes[0]=(item_index==WRONG_VALUE || prev_item_index!=WRONG_VALUE)? prev_item_index : item_index;
      indexes[1]=item_index;
     }
//--- Si el cursor se ha desplazado hacia arriba
   else
     {
      indexes[0]=item_index;
      indexes[1]=prev_item_index;
     }
//--- Dibujamos el fondo de las filas
   for(int r=0; r<rows_total; r++)
     {
      //--- Cálculo de las coordenadas del límite superior e inferior de la fila
      y1[r]=m_rows[indexes[r]].m_y+1;
      y2[r]=m_rows[indexes[r]].m_y2-1;
      //--- Determinamos el foco en la fila respecto al modo del resalto
      bool is_item_focus=false;
      if(!m_lights_hover)
         is_item_focus=(indexes[r]==item_index && item_index!=WRONG_VALUE);
      else
         is_item_focus=(item_index==WRONG_VALUE)?(indexes[r]==prev_item_index) :(indexes[r]==item_index);
      //--- Dibujar el fondo de la fila
      m_table.FillRectangle(x1,y1[r],x2,y2[r],RowColorCurrent(indexes[r],is_item_focus));
     }
//--- Color de la cuadrícula
   uint clr=::ColorToARGB(m_grid_color);
//--- Dibujamos los bordes
   for(int r=0; r<rows_total; r++)
     {
      for(int c=0; c<columns_total; c++)
         m_table.Line(m_columns[c].m_x2,y1[r],m_columns[c].m_x2,y2[r],clr);
     }
//--- Dibujamos las imágenes
   for(int r=0; r<rows_total; r++)
     {
      for(int c=0; c<m_columns_total; c++)
        {
         //--- Dibujamos la imagen si (1) ella se encuentra en esta celda y si (2) el texto se alinea por la izquierda en esta columna
         if(ImagesTotal(c,r)>0 && m_columns[c].m_text_align==ALIGN_LEFT)
            DrawImage(c,indexes[r]);
        }
     }
//--- Para calcular las coordenadas
   int x=0,y=0;
//--- Modo de alineación del texto
   uint text_align=0;
//--- Dibujamos el texto
   for(int c=0; c<m_columns_total; c++)
     {
      //--- Obtenemos (1) la coordenada X del texto y (2) el modo de alineación del texto
      x          =TextX(c);
      text_align =TextAlign(c,TA_TOP);
      //---
      for(int r=0; r<rows_total; r++)
        {
         //--- (1) Calcular la coordenada y (2) dibujar e texto
         y=m_rows[indexes[r]].m_y+m_text_y_offset;
         m_table.TextOut(x,y,m_columns[c].m_rows[indexes[r]].m_short_text,TextColor(c,indexes[r]),text_align);
        }
     }
  }

Al final, obtenemos el siguiente resultado:

 Fig. 2. Demostración del resalto de las filas de la tabla al situar el cursor encima.

Fig. 2. Demostración del resalto de las filas de la tabla al situar el cursor encima. 

 

 

Métodos para redibujar rápidamente la celda de la tabla

Hemos considerado el método para redibujar rápidamente las filas de la tabla. Ahora voy a demostrar los métodos para el redibujo rápido de la celda. Por ejemplo, si hace falta cambiar el texto, su color o la imagen en una celda de la tabla, será suficiente redibujar sólo esta celda, en vez de la tabla entera. Para eso se utiliza el método privado CCanvasTable::RedrawCell(). Se redibuja sólo el contenido de la celda, y el borde no se actualiza. El color del fondo se determina tomando en cuenta el modo del resalto, si éste está activado. Una vez determinados los valores e inicializadas las variables locales, en la celda se dibuja el fondo, la imagen (si está establecida y la alineación es por la izquierda) y el texto.

class CCanvasTable : public CElement
  {
private:
   //--- Redibuja la celda especificada de la tabla
   void              RedrawCell(const int column_index,const int row_index);
  };
//+------------------------------------------------------------------+
//| Redibuja la celda especificada de la tabla                       |
//+------------------------------------------------------------------+
void CCanvasTable::RedrawCell(const int column_index,const int row_index)
  {
//--- Coordenadas
   int x1=m_columns[column_index].m_x+1;
   int x2=m_columns[column_index].m_x2-1;
   int y1=m_rows[row_index].m_y+1;
   int y2=m_rows[row_index].m_y2-1;
//--- Para calcular las coordenadas
   int  x=0,y=0;
//--- Para comprobar el foco
   bool is_row_focus=false;
//--- Si el modo del resalto de las filas está activado
   if(m_lights_hover)
     {
      //--- (1) Obtenemos la coordenada relativa Y del cursor y (2) el foco en la fila especificada
      y=m_mouse.RelativeY(m_table);
      is_row_focus=(y>m_rows[row_index].m_y && y<=m_rows[row_index].m_y2);
     }

//--- Dibujar el fondo de la celda
   m_table.FillRectangle(x1,y1,x2,y2,RowColorCurrent(row_index,is_row_focus));
//--- Dibujamos la imagen si (1) ella se encuentra en esta celda y si (2) el texto se alinea en esta columna por la izquierda
   if(ImagesTotal(column_index,row_index)>0 && m_columns[column_index].m_text_align==ALIGN_LEFT)
      DrawImage(column_index,row_index);
//--- Obtenemos el modo de alineación del texto
   uint text_align=TextAlign(column_index,TA_TOP);
//--- Dibujamos el texto
   for(int c=0; c<m_columns_total; c++)
     {
       //--- Obtenemos la coordenada X para el texto
      x=TextX(c);
      //--- Detenemos el ciclo
      if(c==column_index)
         break;
     }
//--- (1) Calcular la coordenada Y y (2) dibujar el texto
   y=y1+m_text_y_offset-1;
   m_table.TextOut(x,y,m_columns[column_index].m_rows[row_index].m_short_text,TextColor(column_index,row_index),text_align);
  }

Ahora vamos a analizar los métodos a través de los cuales se puede cambiar el texto, color del texto y la imagen (seleccionar entre las establecidas) dentro de la celda. Para establecer el texto y su color, hay que usar los métodos públicos CCanvasTable::SetValue() y CCanvasTable::TextColor(). A estos métodos se les pasan los índices de la celda (columna y fila) y el valor que hay que establecer. Para el método CCanvasTable::SetValue() es un valor string que va a mostrarse en la celda. Aquí, en los campos correspondientes de la estructura de la celda (CTCell) se guarda la línea entera que ha sido pasada y su versión reducida si la línea entera no cabe en la celda por su ancho. Para el método CCanvasTable::TextColor(), es necesario pasar el color del texto. En ambos métodos, en calidad del cuarto parámetro, se puede indicar si es necesario redibujar la celda enseguida o eso se hará más tarde usando la llamada al método CCanvasTable::UpdateTable().

class CCanvasTable : public CElement
  {
private:
   //--- Establece el valor en la celda especificada de la tabla
   void              SetValue(const uint column_index,const uint row_index,const string value,const bool redraw=false);
   //--- Establece el color del texto en la celda especificada de la tabla
   void              TextColor(const uint column_index,const uint row_index,const color clr,const bool redraw=false);
  };
//+------------------------------------------------------------------+
//| Rellena el array según los índices especificados                 |
//+------------------------------------------------------------------+
void CCanvasTable::SetValue(const uint column_index,const uint row_index,const string value,const bool redraw=false)
  {
//--- Comprobar la superación del rango
   if(!CheckOutOfRange(column_index,row_index))
      return;
//--- Colocar el valor en el array
   m_columns[column_index].m_rows[row_index].m_full_text=value;
//--- Corregir y guardar el texto si no cabe en la celda
   m_columns[column_index].m_rows[row_index].m_short_text=CorrectingText(column_index,row_index);
//--- Redibujar la celda si se especifica
   if(redraw)
      RedrawCell(column_index,row_index);

  }
//+------------------------------------------------------------------+
//| Llena el array con el color del texto                            |
//+------------------------------------------------------------------+
void CCanvasTable::TextColor(const uint column_index,const uint row_index,const color clr,const bool redraw=false)
  {
//--- Comprobar la superación del rango
   if(!CheckOutOfRange(column_index,row_index))
      return;
//--- Establecemos el color del texto en el array común
   m_columns[column_index].m_rows[row_index].m_text_color=clr;
//--- Redibujar la celda si se especifica
   if(redraw)
      RedrawCell(column_index,row_index);

  }

Para cambiar la imagen de la celda, puede usar el método CCanvasTable::ChangeImage(). Como el tercer parámetro, aquí se debe indicar el índice de la imagen a la que queremos cambiar. Además, igual que en los métodos para el cambio de las propiedades de la celda que hemos descrito antes, se puede indicar el redibujo ahora o más tarde

class CCanvasTable : public CElement
  {
private:
   //--- Cambia la imagen en la celda especificada
   void              ChangeImage(const uint column_index,const uint row_index,const uint image_index,const bool redraw=false);
  };
//+------------------------------------------------------------------+
//| Cambia la imagen en la celda especificada                        |
//+------------------------------------------------------------------+
void CCanvasTable::ChangeImage(const uint column_index,const uint row_index,const uint image_index,const bool redraw=false)
  {
//--- Comprobar la superación del rango
   if(!CheckOutOfRange(column_index,row_index))
      return;
//--- Obtenemos el número de imágenes de la celda
   int images_total=ImagesTotal(column_index,row_index);
//--- Salir si (1) no hay imágenes o (2) se supera el rango
   if(images_total==WRONG_VALUE || image_index>=(uint)images_total)
      return;
//--- Salir si la imagen especificada coincide con la seleccionada
   if(image_index==m_columns[column_index].m_rows[row_index].m_selected_image)
      return;
//--- Guardar el índice de la imagen seleccionada de la celda
   m_columns[column_index].m_rows[row_index].m_selected_image=(int)image_index;
//--- Redibujar la celda si se especifica
   if(redraw)
      RedrawCell(column_index,row_index);

  }

Vamos a necesitar otro método público para redibujar la tabla entera, CCanvasTable::UpdateTable(). Puede invocarse en dos modos: 

  1. Cuando es necesario simplemente actualizar la tabla para mostrar los últimos cambios con los métodos descritos antes.
  2. Cuando es necesario redibujar completamente la tabla si han sido introducidos algunos cambios.

Por defecto, el único argumento del método tiene asignado el valor false, lo que significa la actualización sin la necesidad de redibujar. 

class CCanvasTable : public CElement
  {
private:
   //--- Actualización de la tabla
   void              UpdateTable(const bool redraw=false);
  };
//+------------------------------------------------------------------+
//| Actualización de la tabla                                        |
//+------------------------------------------------------------------+
void CCanvasTable::UpdateTable(const bool redraw=false)
  {
//--- Redibujar la tabla si está especificado
   if(redraw)
      DrawTable();
//--- Actualizar la tabla
   m_table.Update();
  }

Abajo se muestra el resultado de nuestro trabajo:

 Fig. 3. Demostración de nuevas posibilidades de la tabla dibujada.

Fig. 3. Demostración de nuevas posibilidades de la tabla dibujada.


Se puede descargar el EA que demuestra este resultado de los archivos adjuntos al artículo. Durante la ejecución del programa, las imágenes de todas las celdas de la tabla (5 columnas y 30 filas) van a cambiar con una frecuencia de 100 milisegundos. En la captura de pantalla de abajo se muestra la carga sobre la CPU sin la interacción del usuario con la interfaz gráfica de la aplicación MQL. La carga sobre la CPU con la frecuencia de la actualización de 100 milisegundos no supera el 3%.

 Fig. 4. Carga sobre la CPU durante la ejecución de la aplicación MQL de prueba.

Fig. 4. Carga sobre la CPU durante la ejecución de la aplicación MQL de prueba. 

 

 

Aplicación para la prueba del control

La versión actual de la tabla dibujada ya es bastante «inteligente» para crear las mismas tablas, por ejemplo, en la ventana «Observación del Mercado». Intentaremos demostrarlo. Como ejemplo, crearemos una tabla que se compone de 5 columnas y 25 filas. Serán 25 símbolo que están presentes en el servidor MetaQuotes-Demo. Los datos serán los siguientes:

  • Symbol – instrumentos financieros (pares de divisas).
  • Bid – precios Bid.
  • Ask – precios Ask.
  • Spread (!) – diferencia entre los precios Bid y Ask.
  • Time – hora de la llegada de la última cotización.

Para mostrar la última dirección en el cambio del precio, vamos a preparar las mismas imágenes que figuran en la tabla de la ventana «Observación del Mercado» La primera actualización de las celdas de la tabla va a ejecutarse inmediatamente en el método de la creación del control y realizarse con la llamada al método auxiliar de la clase personalizada CProgram::InitializingTable(). 

//+------------------------------------------------------------------+
//| Clase para crear la aplicación                                     |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Inicialización de la tabla
   void              InitializingTable(void);
  };
//+------------------------------------------------------------------+
//| Inicialización de la tabla                                            |
//+------------------------------------------------------------------+
void CProgram::InitializingTable(void)
  {
//--- Array de los nombres de encabezados
   string text_headers[COLUMNS1_TOTAL]={"Symbol","Bid","Ask","!","Time"};
//--- Array de los símbolos
   string text_array[25]=
     {
      "AUDUSD","GBPUSD","EURUSD","USDCAD","USDCHF","USDJPY","NZDUSD","USDSEK","USDHKD","USDMXN",
      "USDZAR","USDTRY","GBPAUD","AUDCAD","CADCHF","EURAUD","GBPCHF","GBPJPY","NZDJPY","AUDJPY",
      "EURJPY","EURCHF","EURGBP","AUDCHF","CHFJPY"
  //--- arrays    string image_array[3]=
     {
      "::Images\\EasyAndFastGUI\\Icons\\bmp16\\circle_gray.bmp",
      "::Images\\EasyAndFastGUI\\Icons\\bmp16\\arrow_up.bmp",
      "::Images\\EasyAndFastGUI\\Icons\\bmp16\\arrow_down.bmp"
     };
//---
   for(int c=0; c<COLUMNS1_TOTAL; c++)
     {
      //--- Establecemos los nombres para encabezados
      m_canvas_table.SetHeaderText(c,text_headers[c]);
      //---
      for(int r=0; r<ROWS1_TOTAL; r++)
        {
         //--- Establecemos imágenes
         m_canvas_table.SetImages(c,r,image_array);
         //--- Establecemos los nombres de símbolo
         if(c<1)
            m_canvas_table.SetValue(c,r,text_array[r]);
         //--- Valor predefinido para todas las celdas
         else
            m_canvas_table.SetValue(c,r,"-");
        }
     }
  }

En el proceso del trabajo, los valores de las celdas de nuestra tabla van a actualizarse cada 16 milisegundos usando el temporizador. Para eso, ha sido creado otro método auxiliar, CProgram::UpdateTable(). Aquí el programa sale del método si es el fin de semana (sábado o domingo). Luego, en el ciclo doble se hace el repaso de todas las columnas y filas de la tabla donde obtenemos dos últimos ticks para cada símbolo, y a base del análisis del cambio de precios establecemos los valores correspondientes. 

class CProgram : public CWndEvents
  {
private:
   //--- Inicialización de la tabla
   void              InitializingTable(void);
  };
//+------------------------------------------------------------------+
//| Actualización de los valores de la tabla                         |
//+------------------------------------------------------------------+
void CProgram::UpdateTable(void)
  {
   MqlDateTime check_time;
   ::TimeToStruct(::TimeTradeServer(),check_time);
//--- Salir si no es sábado o domingo
   if(check_time.day_of_week==0 || check_time.day_of_week==6)
      return;

//---
   for(int c=0; c<m_canvas_table.ColumnsTotal(); c++)
     {
      for(int r=0; r<m_canvas_table.RowsTotal(); r++)
        {
         //--- Símbolo para el que obtenemos los daros
         string symbol=m_canvas_table.GetValue(0,r);
         //--- Obtenemos los datos de dos últimos ticks
         MqlTick ticks[];
         if(::CopyTicks(symbol,ticks,COPY_TICKS_ALL,0,2)<2)
            continue;
         //--- Establecer el array, como series temporales
         ::ArraySetAsSeries(ticks,true);
         //--- Columna de los símbolos - Symbol. Determinamos la dirección del precio.
         if(c==0)
           {
            int index=0;
            //--- Si los precios no han cambiado
            if(ticks[0].ask==ticks[1].ask && ticks[0].bid==ticks[1].bid)
               index=0;
            //--- Si el precio Bid ha cambiado para arriba
            else if(ticks[0].bid>ticks[1].bid)
               index=1;
            //--- Si el precio Bid ha cambiado para abajo
            else if(ticks[0].bid<ticks[1].bid)
               index=2;
             //--- Establecemos la imagen correspondiente
            m_canvas_table.ChangeImage(c,r,index,true);
           }
         else
           {
            //--- Columna de la diferencia de precios - Spread (!)
            if(c==3)
              {
               //--- Obtenemos y establecemos el tamaño del spread en puntos
               int spread=(int)::SymbolInfoInteger(symbol,SYMBOL_SPREAD);
               m_canvas_table.SetValue(c,r,string(spread),true);
               continue;
              }
             //--- Obtenemos el número de dígitos después de la coma
            int digit=(int)::SymbolInfoInteger(symbol,SYMBOL_DIGITS);
            //--- Columna de precios Bid
            if(c==1)
              {
               m_canvas_table.SetValue(c,r,::DoubleToString(ticks[0].bid,digit));
               //--- Si el precio ha cambiado, establecemos el color correspondiente para la dirección
               if(ticks[0].bid!=ticks[1].bid)
                  m_canvas_table.TextColor(c,r,(ticks[0].bid<ticks[1].bid)? clrRed : clrBlue,true);
               //---
               continue;
              }
            //--- Columna de precios Ask
            if(c==2)
              {
               m_canvas_table.SetValue(c,r,::DoubleToString(ticks[0].ask,digit));
               //--- Si el precio ha cambiado, establecemos el color correspondiente para la dirección
               if(ticks[0].ask!=ticks[1].ask)
                  m_canvas_table.TextColor(c,r,(ticks[0].ask<ticks[1].ask)? clrRed : clrBlue,true);
               //---
               continue;
              }
            //--- Columna de la hora de la última llegada de los precios del símbolo
            if(c==4)
              {
               long   time     =::SymbolInfoInteger(symbol,SYMBOL_TIME);
               string time_msc =::IntegerToString(ticks[0].time_msc);
               int    length   =::StringLen(time_msc);
               string msc      =::StringSubstr(time_msc,length-3,3);
               string str      =::TimeToString(time,TIME_MINUTES|TIME_SECONDS)+"."+msc;
               //---
               color clr=clrBlack;
              //--- Si los precios no han cambiado
               if(ticks[0].ask==ticks[1].ask && ticks[0].bid==ticks[1].bid)
                  clr=clrBlack;
               //--- Si el precio Bid ha cambiado para arriba
               else if(ticks[0].bid>ticks[1].bid)
                  clr=clrBlue;
              //--- Si el precio Bid ha cambiado para abajo
               else if(ticks[0].bid<ticks[1].bid)
                  clr=clrRed;
               //--- Establecemos el valor y el color del texto
               m_canvas_table.SetValue(c,r,str);
               m_canvas_table.TextColor(c,r,clr,true);
               continue;
              }
           }
        }
     }
//--- Actualizar la tabla
   m_canvas_table.UpdateTable();
  }

Este es el resultado que hemos obtenido:

Fig. 5. Comparación entre los datos en la ventana «Observación del Mercado» y el análogo del usuario.

Fig. 5. Comparación entre los datos en la ventana «Observación del Mercado» y el análogo del usuario. 


Puede descargar esta aplicación de prueba al final del artículo para estudiarla más detalladamente. 

 

Conclusión

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

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

Fig. 6. 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.