Utilización de layouts y contenedores en los controles GUI: la clase CGrid

Enrico Lambino | 23 febrero, 2016

Tabla de contenidos


1. Introducción

La clase CGrid es el gestor de plantillas (layout manager) que utilizan los controles GUI en las ventanas de diálogo de MetaTrader. Esta clase contenedora personalizada permite diseñar GUIs sin necesidad de recurrir al posicionamiento absoluto.

Recomiendo echar un vistazo al artículo de la clase CBox antes de continuar con los conceptos que examinaremos en este artículo.


2. Objetivos

La clase CBox es suficiente para diseñar las ventanas de diálogo más sencillas. Sin embargo, a medida que crece el número de controles en la ventana, el uso de los contenedores CBox presenta una serie de inconvenientes:

La mayoría de los problemas que plantea la clase CBox se puede solucionar poniendo los controles en una cuadrícula (grid) en vez de en contenedores individuales. Los objetivos del presente artículo son pues los siguientes:

De modo parecido a la clase CBox, tenemos que conseguir estos objetivos:

En este artículo nos proponemos crear un layout manager que implemente los objetivos descritos arriba utilizando la clase CGrid.


3. La clase CGrid

La clase CGrid crea un contenedor para uno o más controles GUI y los dispone en forma de cuadrícula. La siguiente figura muestra un ejemplo de instancia de la clase CGrid:

Layout CGrid

Figura 1. Plantilla cuadriculada

Esta clase es conveniente, especialmente si los controles a añadir a la cuadrícula tienen las mismas dimensiones; por ejemplo, un conjunto de botones o unos campos de edición dentro del área cliente.

El ejemplo de arriba es una cuadrícula de 4x4 celdas (4 columnas y 4 filas). Sin embargo, nuestro objetivo es desarrollar una clase que pueda albergar cualquier número de filas y columnas.

Declararemos la clase CGrid como clase hija de CBox. De este modo podremos reemplazar fácilmente las funciones virtuales de la clase padre. Por otro lado, así podremos manipular las instancias de esta clase como si fueran instancias de la clase CBox:

#include "Box.mqh"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CGrid : public CBox
  {
protected:
   int               m_cols;
   int               m_rows;
   int               m_hgap;
   int               m_vgap;
   CSize             m_cell_size;
public:
                     CGrid();
                     CGrid(int rows,int cols,int hgap=0,int vgap=0);
                    ~CGrid();
   virtual int       Type() const {return CLASS_LAYOUT;}
   virtual bool      Init(int rows,int cols,int hgap=0,int vgap=0);
   virtual bool      Create(const long chart,const string name,const int subwin,
                            const int x1,const int y1,const int x2,const int y2);
   virtual int       Columns(){return(m_cols);}
   virtual void      Columns(int cols){m_cols=cols;}
   virtual int       Rows(){return(m_rows);}
   virtual void      Rows(int rows){m_rows=rows;}
   virtual int       HGap(){return(m_hgap);}
   virtual void      HGap(int gap){m_hgap=gap;}
   virtual int       VGap(){return(m_vgap);}
   virtual void      VGap(int gap){m_vgap=gap;}
   virtual bool      Pack();
protected:
   virtual void      CheckControlSize(CWnd *control);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CGrid::CGrid()
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CGrid::CGrid(int rows,int cols,int hgap=0,int vgap=0)
  {
   Init(rows,cols,hgap,vgap);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CGrid::~CGrid()
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CGrid::Init(int rows,int cols,int hgap=0,int vgap=0)
  {
   Columns(cols);
   Rows(rows);
   HGap(hgap);
   VGap(vgap);
   return(true);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CGrid::Create(const long chart,const string name,const int subwin,
                   const int x1,const int y1,const int x2,const int y2)
  {
   return(CBox::Create(chart,name,subwin,x1,y1,x2,y2));
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CGrid::Pack()
  {
   CSize size=Size();
   m_cell_size.cx = (size.cx-((m_cols+1)*m_hgap))/m_cols;
   m_cell_size.cy = (size.cy-((m_rows+1)*m_vgap))/m_rows;
   int x=Left(),y=Top();
   int cnt=0;
   for(int i=0;i<ControlsTotal();i++)
     {
      CWnd *control=Control(i);
      if(control==NULL)
         continue;
      if(control==GetPointer(m_background))
         continue;
      if(cnt==0 || Right()-(x+m_cell_size.cx)<m_cell_size.cx+m_hgap)
        {
         if(cnt==0)
            y+=m_vgap;            
         else y+=m_vgap+m_cell_size.cy;
         x=Left()+m_hgap;
        }
      else x+=m_cell_size.cx+m_hgap;    
      CheckControlSize(control);
      control.Move(x,y);
      if(control.Type()==CLASS_LAYOUT)
        {
         CBox *container=control;
         container.Pack();
        }
      cnt++;
     }   
   return(true);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CGrid::CheckControlSize(CWnd *control)
  {
   control.Size(m_cell_size.cx,m_cell_size.cy);
  }
//+------------------------------------------------------------------+


3.1. Inicialización

De modo parecido a otros contenedores y controles, creamos la cuadrícula por medio del método Create() de la clase. Sin embargo, especificar la posición del control es opcional en este punto, como en cualquier instancia de la clase CBox. Simplemente declaramos la anchura y la altura del control con las propiedades x2 e y2. Si la cuadrícula es el único contenedor (contenedor principal) que se adjunta al área cliente, podemos utilizar este código (siendo m_main una instancia de CGrid):

if(!m_main.Create(chart,name+"main",subwin,0,0,CDialog::m_client_area.Width(),CDialog::m_client_area.Height()))
      return(false);

Justo después de la creación tenemos que inicializar la cuadrícula llamando al método Init(). Para inicializar una instancia de CGrid tenemos que especificar el número de filas y columnas en que se dividirá el área cliente principal (o una sección de la misma), así como el espacio horizontal y vertical entre las celdas de la rejilla. Finalmente, no olvide inicializar la cuadrícula en el método Init(). Este código crea una cuadrícula 4x4 con espacios horizontales y verticales de 2 píxeles entre cada celda:

m_main.Init(4,4,2,2);

El método Init() tiene 4 parámetros:

  1. número de filas;
  2. número de columnas;
  3. espacio horizontal (en píxeles);
  4. espacio vertical (en píxeles).

Los espacios horizontales y verticales entre celdas son parámetros opcionales. Estos valores valen cero de forma predeterminada a no ser que se inicialicen con valores personalizados.


3.2. Espacio entre los controles

Los parámetros hgap (espacio horizontal) y vgap (espacio vertical) determinan el espacio entre las celdas de la rejilla. La rejilla maximiza el uso del área cliente o contenedor; la siguiente fórmula muestra el espacio que queda para los controles en cualquier disposición horizontal o vertical:

espacio total que queda para los controles = espacio del área total - (espacio * (número de celdas+1))

Esta fórmula se utiliza en la función Pack() de la clase.


3.3. Cambio de tamaño del control

En la clase CGrid el tamaño de los controles de la rejilla cambiará para ocupar el espacio completo de la celda. Así, con este diseño, es aceptable crear o inicializar elementos de control de tamaño cero. El tamaño del control cambiará posteriormente durante la creación de la ventana del diálogo principal (CDialog o CAppDialog) cuando se llame al método Pack() de la instancia de la clase.

El tamaño total (horizontal o vertical) calculado en la fórmula de la sección anterior determinará el tamaño x o y de las celdas de la cuadrícula. La rejilla utiliza estas fórmulas para calcular el tamaño de las celdas:

xsize = tamaño total para los controles / número total de columnas

ysize = tamaño total para los controles / número total de filas

El cambio de tamaño se lleva a cabo en el método CheckControlSize() de la clase.


4. Ejemplo #1: Una cuadrícula de botones sencilla

Vamos a ilustrar un ejemplo sencillo de clase CGrid con una cuadrícula de botones sencilla. Esta es una captura de pantalla de la GUI:

Una cuadrícula de botones sencilla

Figura 2. Una cuadrícula de botones sencilla

Como podemos ver, el cuadro de diálogo que se muestra arriba contiene una cuadrícula de 3x3 celdas, cada una de las cuales contiene un botón. Cada botón se coloca uniformemente a través de la rejilla, que ocupa todo el área cliente de la ventana de diálogo.

Para crear esta cuadrícula necesitamos construir un EA o un indicador que siga el formato descrito en el artículo sobre CBox, que es muy parecido a los controles de ejemplo de MetaTrader. Es decir, declaramos un archivo fuente principal que contiene la declaración de una instancia de ventana CAppDialog personalizada (junto con otros manejadores de eventos), y la enlazamos con un archivo de cabecera que contiene la declaración real de la clase.

La rejilla 3x3 necesita declarar como miembro de la clase una instancia de la clase CGrid y un conjunto de 9 botones, 1 para cada celda de la cuadrícula:

class CGridSampleDialog : public CAppDialog
  {
protected:
   CGrid             m_main;
   CButton           m_button1;
   CButton           m_button2;
   CButton           m_button3;
   CButton           m_button4;
   CButton           m_button5;
   CButton           m_button6;
   CButton           m_button7;
   CButton           m_button8;
   CButton           m_button9;
public:
                     CGridSampleDialog();
                    ~CGridSampleDialog();
  };

El siguiente paso consiste en sobrescribir las funciones virtuales públicas de la clase CAppDialog.

public:
                     CGridSampleDialog();
                    ~CGridSampleDialog();
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);
   virtual bool      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
bool CGridSampleDialog::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2)
  {
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      return(false);
   if(!CreateMain(chart,name,subwin))
      return(false);   
   for(int i=1;i<=9;i++)
     {
      if(!CreateButton(i,chart,"button",subwin))
         return(false);
     }   
   if(!m_main.Pack())
      return(false);
   if(!Add(m_main))
      return(false);
   return(true);
  }
EVENT_MAP_BEGIN(CGridSampleDialog)
EVENT_MAP_END(CAppDialog)

En este ejemplo el mapa de evento está vacío porque no asignaremos cualquier gestión de evento a los botones.

El último paso es declarar las funciones protegidas de la clase, que serán las que realmente se utilizarán para construir la rejilla con los controles:

protected:
   virtual bool      CreateMain(const long chart,const string name,const int subwin);
   virtual bool      CreateButton(const int button_id,const long chart,const string name,const int subwin);

En este ejemplo se observan las ventajas que tiene CGrid sobre CBox. CBox necesitaría 4 contenedores diferentes para construir un layout parecido. Esto es porque CBox solo puede manejar una sola columna o fila. Por otro lado, con CGrid hemos reducido el número de contenedores de 4 a 1, lo que conlleva utilizar menos líneas de código.

bool CGridSampleDialog::CreateMain(const long chart,const string name,const int subwin)
  {
   if(!m_main.Create(chart,name+"main",subwin,0,0,CDialog::m_client_area.Width(),CDialog::m_client_area.Height()))
      return(false);
   m_main.Init(3,3,5,5);
   return(true);
  }

El método de la clase CreateMain() es el encargado de construir el control de la rejilla. Trabaja de forma parecida cuando crea el control de CBox. La única diferencia es que CGrid requiere un método adicional: Init(). Y CBox no lo necesita.

El siguiente código muestra la implementación del miembro de la clase CreateButton():

bool CGridSampleDialog::CreateButton(const int button_id,const long chart,const string name,const int subwin)
  {
   CButton *button;
   switch(button_id)
     {
      case 1: button = GetPointer(m_button1); break;
      case 2: button = GetPointer(m_button2); break;
      case 3: button = GetPointer(m_button3); break;
      case 4: button = GetPointer(m_button4); break;
      case 5: button = GetPointer(m_button5); break;
      case 6: button = GetPointer(m_button6); break;
      case 7: button = GetPointer(m_button7); break;
      case 8: button = GetPointer(m_button8); break;
      case 9: button = GetPointer(m_button9); break;
      default: return(false);
     }
   if (!button.Create(chart,name+IntegerToString(button_id),subwin,0,0,100,100))
      return(false);
   if (!button.Text(name+IntegerToString(button_id)))
      return(false);
   if (!m_main.Add(button))
      return(false);
   return(true);
  }

Como los procesos de creación de botones son bastante parecidos, en lugar de utilizar un método para crear un botón utilizamos una función genérica que crea todos los botones. Esto lo hacemos con el método CreateButton() implementado anteriormente, al que llamaremos dentro del método virtual Create() justo después de crear la ventana de diálogo en la cuadrícula. Como se observa en el code snippet del método Create(), hemos implementado un bucle for para conseguir nuestro objetivo. Como los botones se declaran estáticamente dentro de la clase, ya están creados en la declaración, así que no hace falta utilizar el operador new. Tan solo obtenemos el puntero (automático) de cada botón y luego llamamos a sus métodos Create() correspondientes.


5. Ejemplo #2: Rompecabezas deslizante

Nuestro segundo ejemplo es un juego que se llama rompecabezas deslizante. En este ejemplo presentamos al usuario un conjunto de números comprendidos entre 1 y 15, dispuestos en una cuadrícula 4 x 4. El usuario tiene que reorganizar las baldosas para que los números aparezcan ordenados de izquierda a derecha y de arriba abajo. El juego se considera completo tan pronto como el usuario ordena las baldosas numeradas en el orden correcto, tal y como se ilustra en la siguiente figura:

Rompecabezas deslizante

Figure 3. Rompecabezas deslizante

Además de los métodos de la clase que participan en la construcción de la ventana de diálogo, estas son otras características adicionales que necesitamos:


5.1. Creación de la ventana de diálogo

Declaramos la clase como una extensión de CAppDialog, con sus miembros protegidos o privados, constructor y destructor correspondientes:

class CSlidingPuzzleDialog : public CAppDialog
  {
protected:
   CGrid             m_main;
   CButton           m_button1;
   CButton           m_button2;
   CButton           m_button3;
   CButton           m_button4;
   CButton           m_button5;
   CButton           m_button6;
   CButton           m_button7;
   CButton           m_button8;
   CButton           m_button9;
   CButton           m_button10;
   CButton           m_button11;
   CButton           m_button12;
   CButton           m_button13;
   CButton           m_button14;
   CButton           m_button15;
   CButton           m_button16;
   CButton          *m_empty_cell;
public:
                     CSlidingPuzzleDialog();
                    ~CSlidingPuzzleDialog();   
  };

El siguiente código muestra la implementación del método Create().

Declaración (definición de la clase, funciones miembro públicas):

virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);

Implementación:

bool CSlidingPuzzleDialog::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2)
  {
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      return(false);
   if(!CreateMain(chart,name,subwin))
      return(false);
   for(int i=1;i<=16;i++)
     {
      if(!CreateButton(i,chart,"button",subwin))
         return(false);
     }
   m_empty_cell=GetPointer(m_button16);
   if(!m_main.Pack())
      return(false);
   if(!Add(m_main))
      return(false);
   Shuffle();
   return(true);
  }

Como vemos, la función CreateMain() del diálogo se utiliza para construir la rejilla, y CreateButton() crea los botones en un bucle for. Obsérvese que los controles se recolocan mediante el método Pack() de la instancia CGrid, y el método Add() enlaza la rejilla al área cliente principal. El juego se inicializa en el método Shuffle().


5.2. Botones

Este es el código del método CreateButton():

bool CSlidingPuzzleDialog::CreateButton(const int button_id,const long chart,const string name,const int subwin)
  {
   CButton *button;
   switch(button_id)
     {
      case 1: button = GetPointer(m_button1); break;
      case 2: button = GetPointer(m_button2); break;
      case 3: button = GetPointer(m_button3); break;
      case 4: button = GetPointer(m_button4); break;
      case 5: button = GetPointer(m_button5); break;
      case 6: button = GetPointer(m_button6); break;
      case 7: button = GetPointer(m_button7); break;
      case 8: button = GetPointer(m_button8); break;
      case 9: button = GetPointer(m_button9); break;
      case 10: button = GetPointer(m_button10); break;
      case 11: button = GetPointer(m_button11); break;
      case 12: button = GetPointer(m_button12); break;
      case 13: button = GetPointer(m_button13); break;
      case 14: button = GetPointer(m_button14); break;
      case 15: button = GetPointer(m_button15); break;
      case 16: button = GetPointer(m_button16); break;
      default: return(false);
     }
   if(!button.Create(chart,name+IntegerToString(button_id),subwin,0,0,100,100))
      return(false);
   if(button_id<16)
     {
      if(!button.Text(IntegerToString(button_id)))
         return(false);
     }
   else if(button_id==16)
     {
      button.Hide();
     }
   if(!m_main.Add(button))
      return(false);
   return(true);
  }

Este método es parecido al método CreateButton() del ejemplo anterior. Básicamente asigna a cada celda un valor inicial comprendido entre 1 y 16. También esconde la celda número 16 porque tiene que estar vacía.


5.3. Comprobación de baldosas adyacentes

Tenemos que comprobar si existe una baldosa adyacente en la dirección determinada. De lo contrario, la celda vacía intercambiará valores con un botón que no existe. La comprobación de las baldosas adyacentes se realiza con las funciones HasNorth(), HasSouth(), HasEast() y HasSouth(). Este es el código del método HasNorth():

bool CSlidingPuzzleDialog::HasNorth(CButton *button,int id,bool shuffle=false)
  {
   if(id==1 || id==2 || id==3 || id==4)
      return(false);
   CButton *button_adj=m_main.Control(id-4);
   if(!CheckPointer(button_adj))
      return(false);
   if(!shuffle)
     {
      if(button_adj.IsVisible())
         return(false);
     }
   return(true);
  }

Estas funciones comprueban si el botón en cuestión (o una celda vacía) se puede mover en la dirección de los puntos cardinales, que también son las direcciones donde la celda vacía puede ir libremente. Si un botón determinado se encuentra en el centro de la rejilla, se podría mover libremente en las cuatro direcciones. Sin embargo, si está en uno de los lados, entonces hay baldosas que no existen. Por ejemplo, sin tener en cuenta las celdas vacías, la primera celda de la rejilla se puede mover a la derecha o hacia abajo pero no se puede mover a la izquierda o hacia arriba, mientras que la sexta celda puede moverse libremente en las cuatro direcciones.


5.4. Mezclando las baldosas

Este es el código del método Shuffle():

void CSlidingPuzzleDialog::Shuffle(void)
  {
   m_empty_cell=m_main.Control(16);
   for(int i=1;i<m_main.ControlsTotal()-1;i++)
     {
      CButton *button=m_main.Control(i);
      button.Text((string)i);
     }
   MathSrand((int)TimeLocal());
   CButton *target=NULL;
   for(int i=0;i<30;i++)
     {
      int empty_cell_id=(int)StringToInteger(StringSubstr(m_empty_cell.Name(),6));
      int random=MathRand()%4+1;
      if(random==1 && HasNorth(m_empty_cell,empty_cell_id,true))
         target= m_main.Control(empty_cell_id-4);
      else if(random==2 && HasEast(m_empty_cell,empty_cell_id,true))
         target=m_main.Control(empty_cell_id+1);
      else if(random==3 && HasSouth(m_empty_cell,empty_cell_id,true))
         target=m_main.Control(empty_cell_id+4);
      else if(random==4 && HasWest(m_empty_cell,empty_cell_id,true))
         target=m_main.Control(empty_cell_id-1);
      if(CheckPointer(target))
         Swap(target);
     }
  }

El proceso tiene que implementar algún tipo de aleatoriedad en el momento de barajar las baldosas. De otro modo, las baldosas siempre se dispondrían en el mismo orden. Utilizaremos las funciones MathSrand y MathRand para conseguir nuestro objetivo, y estableceremos la hora local como semilla inicial.

En primer lugar, antes de comenzar a barajar, tenemos que inicializar los valores de los botones a sus valores por defecto. Esto evita cualquier evento donde el rompecabezas es irresoluble o demasiado difícil de resolver. Para ello, reasignamos la celda vacía a la baldosa 16, asignando los valores correspondientes. También asignamos la celda 16 al puntero de celda vacía (miembro de clase) que hemos declarado anteriormente.

Las baldosas se ordenan al final del método. En realidad los botones no cambian. Sus valores simplemente se intercambian, ofreciendo así una ilusión de movimiento. Como vemos, esta es la aproximación más sencilla. Cada bucle comprueba si hay una baldosa adyacente; si la baldosa es una celda vacía entonces se intercambian los valores del botón vacío y del botón seleccionado al azar.

También hay un valor predeterminado que indica cuántas veces se intercambian las baldosas. El valor predeterminado es 30, pero este valor se puede cambiar para incrementar o disminuir la dificultad. La mezcla puede ser más o menos difícil que el ajuste de dificultad, dependiendo de si el botón destino obtuvo un puntero válido para cada iteración.


5.5. Evento de clic de botón

Para procesar los eventos clic de los botones tendríamos que declarar un manejador de evento clic. Sin embargo, con el objetivo de reducir el código duplicado, vamos a declarar un método para procesar los eventos clic del botón:

CSlidingPuzzleDialog::OnClickButton(CButton *button)
  {
   if(IsMovable(button))
     {
      Swap(button);
      Check();
     }
  }

La función IsMovable() comprueba si hay una baldosa vacía al lado de una baldosa numerada, y para ello utiliza las funciones de los puntos cardinales, esto es, HasNorth(), HasSouth(). Si hay una baldosa vacía adyacente al botón, entonces se puede mover, y, en consecuencia, llamamos a la función Swap() para intercambiar el valor del botón con el de la celda vacía. También llamamos a la función Check() justo después de hacer el intercambio.

Y a continuación creamos manejadores de evento separados para cada botón. Este es un ejemplo del manejador de evento del primer botón:

CSlidingPuzzleDialog::OnClickButton1(void)
  {
   OnClickButton(GetPointer(m_button1));
  }

Cada uno de estos manejadores de evento terminará llamando a OnClickButton() en algún momento. También necesitamos declarar estos métodos de clase en el mapa de eventos:

EVENT_MAP_BEGIN(CSlidingPuzzleDialog)
   ON_EVENT(ON_CLICK,m_button1,OnClickButton1)
   ON_EVENT(ON_CLICK,m_button2,OnClickButton2)
   ON_EVENT(ON_CLICK,m_button3,OnClickButton3)
   ON_EVENT(ON_CLICK,m_button4,OnClickButton4)
   ON_EVENT(ON_CLICK,m_button5,OnClickButton5)
   ON_EVENT(ON_CLICK,m_button6,OnClickButton6)
   ON_EVENT(ON_CLICK,m_button7,OnClickButton7)
   ON_EVENT(ON_CLICK,m_button8,OnClickButton8)
   ON_EVENT(ON_CLICK,m_button9,OnClickButton9)
   ON_EVENT(ON_CLICK,m_button10,OnClickButton10)
   ON_EVENT(ON_CLICK,m_button11,OnClickButton11)
   ON_EVENT(ON_CLICK,m_button12,OnClickButton12)
   ON_EVENT(ON_CLICK,m_button13,OnClickButton13)
   ON_EVENT(ON_CLICK,m_button14,OnClickButton14)
   ON_EVENT(ON_CLICK,m_button15,OnClickButton15)
   ON_EVENT(ON_CLICK,m_button16,OnClickButton16)
EVENT_MAP_END(CAppDialog)

Como alternativa podemos invocar el manejador de evento clic en cada uno de los botones del mismo mapa de eventos, así evitamos tener que declarar miembros manejadores de eventos para cada botón.

Finalmente añadimos la función pública OnEvent() a la declaración de la clase:

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


5.6. Comprobación

Tenemos que comprobar el orden de las celdas en cada clic de botón para comprobar si el rompecabezas ya se ha solucionado. Esto se lleva a cabo con la función Check():

bool CSlidingPuzzleDialog::Check(void)
  {
   for(int i=1;i<m_main.ControlsTotal()-1;i++)
     {
      CButton *button=m_main.Control(i);
      if(CheckPointer(button))
        {
         if(button.Text()!=IntegerToString(i))
           {
            Print("estado: no solucionado: "+button.Text()+" "+IntegerToString(i));
            return(false);
           }
        }
     }
   Print("estado: solucionado");
   return(true);
  }

La comprobación se realiza solamente desde el segundo control hasta el penúltimo. El primer control siempre se corresponde con el fondo, que no es un botón, mientras que el control final es la celda vacía, que ya no necesita comprobarse.


6. La clase CGridTk


6.1. Problemas de la clase CGrid

El uso de la clase CGrid plantea varios problemas:

Al colocar celdas vacías a veces nos puede interesar que un control determinado se posicione lejos de sus hermanos (ya sea horizontalmente, verticalmente, o de las dos formas), probablemente más lejos que los huecos horizontales y verticales disponibles en la cuadrícula. Un ejemplo sería separar un conjunto de botones de otro conjunto de botones, o colocar un botón en el lado izquierdo del área cliente (tirar a la izquierda) junto con otro en el otro lado (tirar a la derecha). Estos diseños GUI suelen aplicarse en las páginas web.

Podemos solucionar el problema mencionado arriba creando controles vacíos. Tiene que ser un control sin demasiados componentes superficiales, por así decir, tales como un botón o una etiqueta. Además, podemos renderizar esos controles invisibles llamando al método Hide() de forma parecida a lo que hacemos en la celda 16 del primer ejemplo. Y por último, ponemos los controles en una celda de la rejilla allá donde quisiéramos disponer del espacio adicional. Todo esto proporcionará una ilusión de espacio. Pero en realidad un control invisible ocupa la celda.

Esta solución es útil aplicada en ventanas simples, pero en cuadros de diálogo más complejos puede resultar ineficaz y poco práctica. El código tiende a ser más largo debido al número de controles que se tienen que declarar, especialmente si hay más de una celda vacía. Además, el mantenimiento del código se complica a medida que el número de celdas vacías aumenta, por ejemplo, si hay una fila o columna enteras de celdas vacías.

El segundo problema está relacionado con la posición y el tamaño de los controles. Con respecto a la colocación de los controles individuales en las celdas, el problema no existe si todos los controles son del mismo tamaño y tienen la misma distancia los unos con los otros. Pero si no es así entonces hay que implementar una aproximación diferente. Probablemente, la solución consiste en sacar los controles asimétricos de la cuadrícula y colocarlos en otro lugar mediante el posicionamiento absoluto. Como alternativa pueden colocarse en otro contenedor como CBox, o en otra instancia de CGrid.


6.2. CGridTk, una CGrid mejorada

La clase estándar CGrid tiene muchas aplicaciones prácticas. Sin embargo sus capacidades como contenedor son bastante limitadas. Teniendo en cuenta los problemas que plantea el uso de la clase estándar CGrid presentada en la sección anterior, ahora vamos a crear una clase mejorada a partir de la misma con las siguientes características:

Con estas características se solucionan los problemas que hemos presentado en la sección anterior. Ahora tenemos más libertad para posicionar las celdas, de forma parecida al posicionamiento absoluto. No obstante, a diferencia del posicionamiento absoluto, utilizamos el tamaño de la celda como unidad básica de posicionamiento en vez de un píxel. De nuevo, sacrificamos algo de precisión en beneficio del diseño; es más fácil visualizar el tamaño de una celda de la cuadrícula que, por ejemplo, 100 píxeles de la pantalla.

Renombramos pues la clase a GridTk. Este es el código correspondiente:

#include "Grid.mqh"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CGridTk : public CGrid
  {
protected:
   CArrayObj         m_constraints;
public:
                     CGridTk();
                    ~CGridTk();
   bool              Grid(CWnd *control,int row,int column,int rowspan,int colspan);
   bool              Pack();
   CGridConstraints     *GetGridConstraints(CWnd *control);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CGridTk::CGridTk(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CGridTk::~CGridTk(void)
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CGridTk::Grid(CWnd *control,int row,int column,int rowspan=1,int colspan=1)
  {
   CGridConstraints *constraints=new CGridConstraints(control,row,column,rowspan,colspan);
   if(!CheckPointer(constraints))
      return(false);
   if(!m_constraints.Add(constraints))
      return(false);
   return(Add(control));
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CGridTk::Pack()
  {
   CGrid::Pack();
   CSize size=Size();
   m_cell_size.cx = (size.cx-(m_cols+1)*m_hgap)/m_cols;
   m_cell_size.cy = (size.cy-(m_rows+1)*m_vgap)/m_rows;   
   for(int i=0;i<ControlsTotal();i++)
     {
      int x=0,y=0,sizex=0,sizey=0;
      CWnd *control=Control(i);
      if(control==NULL)
         continue;
      if(control==GetPointer(m_background))
         continue;
      CGridConstraints *constraints = GetGridConstraints(control);
      if (constraints==NULL)
         continue;   
      int column = constraints.Column();
      int row = constraints.Row();
      x = (column*m_cell_size.cx)+((column+1)*m_hgap);
      y = (row*m_cell_size.cy)+((row+1)*m_vgap);
      int colspan = constraints.ColSpan();
      int rowspan = constraints.RowSpan();
      control.Size(colspan*m_cell_size.cx+((colspan-1)*m_hgap),rowspan*m_cell_size.cy+((rowspan-1)*m_vgap));
      control.Move(x,y);
      if(control.Type()==CLASS_LAYOUT)
        {
         CBox *container=control;
         container.Pack();
        }
     }
   return(true);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CGridConstraints *CGridTk::GetGridConstraints(CWnd *control)
  {
   for(int i=0;i<m_constraints.Total();i++)
     {
      CGridConstraints *constraints=m_constraints.At(i);
      CWnd *ctrl=constraints.Control();
      if(ctrl==NULL)
         continue;
      if(ctrl==control)
         return(constraints);
     }
   return (NULL);
  }

Además del método Add(), introducimos un método nuevo para añadir controles a la rejilla: el método Grid(). Con este método se puede asignar una posición y un tamaño predeterminados al control, en base a un múltiplo del tamaño de una celda.

Tal y como veremos un poco más adelante, la clase tiene un miembro de la clase CConstraints.


6.2.1. Extensión de las filas y columnas

La anchura y la longitud del control se pueden definir extendiendo la fila y la columna. Esto proporciona una mejora con respecto a tener que manejar celdas de tamaño predeterminado, pero es menos preciso que el posicionamiento absoluto. Sin embargo vale la pena si tenemos en cuenta que la clase CGridTk ya no utiliza el método CheckControlSize() de CBox y CGrid. El cambio de tamaño de los controles se lleva a cabo en el mismo método Pack().


6.2.2. La clase CConstraints

Para cada control, necesitamos definir un conjunto de restricciones que definen cómo se colocan en la rejilla, qué celdas deben ocupar, y cómo se tiene que redimensionar. Podemos cambiar el lugar y la posición de los controles tan pronto como se añaden con el método Grid() de CGridTk. Sin embargo, por cuestiones de consistencia, retrasamos estos cambios hasta hacer la llamada al método Pack(), de forma parecida a como sucede en la clase CBox. Para ello tenemos que guardar las restricciones en la memoria, y este es el objetivo de la clase CConstraints:

class CGridConstraints : public CObject
  {
protected:
   CWnd             *m_control;
   int               m_row;
   int               m_col;
   int               m_rowspan;
   int               m_colspan;
public:
                     CGridConstraints(CWnd *control,int row,int column,int rowspan=1,int colspan=1);
                    ~CGridConstraints();
   CWnd             *Control(){return(m_control);}
   int               Row(){return(m_row);}
   int               Column(){return(m_col);}
   int               RowSpan(){return(m_rowspan);}
   int               ColSpan(){return(m_colspan);}
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CGridConstraints::CGridConstraints(CWnd *control,int row,int column,int rowspan=1,int colspan=1)
  {
   m_control = control;
   m_row = MathMax(0,row);
   m_col = MathMax(0,column);
   m_rowspan = MathMax(1,rowspan);
   m_colspan = MathMax(1,colspan);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CGridConstraints::~CGridConstraints()
  {
  }

Echando un vistazo al constructor de la clase veremos que CConstraints almacena las filas, columnas, rowspan y colspan de cada control. Esto solo es posible al llamar al método Grid(), como se observa en la implementación de la clase CGridTk. Además, dicha clase solo almacena la información. La clase CGridTk muestra cómo se utiliza la información.


6.3.3. Posicionamiento predeterminado

Si un control determinado no se añade a la rejilla mediante el método Grid(), entonces se aplica el posicionamiento predeterminado. En tal caso, el control se ha añadido a la rejilla con el método Add(), lo que significa que la rejilla no tiene restricciones, es decir, no hay ningún objeto CGridConstraints almacenado en la instancia de la cuadrícula. Por consiguiente, los métodos actualizados en CGridTk no pueden hacer nada con esos controles en lo que se refiere al posicionamiento o al cambio de tamaño. El método de colocación es parecido al método de la clase CGrid, como solución alternativa o como método de posicionamiento predeterminado. Es decir, dichos controles se apilan como los ladrillos de una pared, comenzando por la parte superior izquierda del área cliente, como muestra el primer ejemplo.


7. Ejemplo #3: Rompecabezas deslizante (mejorado)

Tenemos que hacer algunos cambios en el segundo ejemplo para mejorar el rompecabezas deslizante:

  1. Crear un botón "Nuevo juego", para que el Asesor Experto no tenga que reiniciarse cada vez que empieza un juego nuevo.
  2. Crear un control en la ventana que muestre el estado del juego, para eliminar la necesidad de abrir la pestaña "Diario" de la ventana del terminal.
  3. Implementar un tamaño diferente para los nuevos controles.
  4. Realizar algunos cambios superficiales, por ejemplo, pintar las baldosas y mostrar todas las baldosas en la rejilla (opcional).

Esta figura muestra el rompecabezas deslizante mejorado:

Rompecabezas deslizante (mejorado)

Figura 4. Rompecabezas deslizante (mejorado)

Como vemos en la captura de pantalla, hemos añadido nuevos componentes a la ventana de diálogo. Hay un botón que permite crear un nuevo juego (reorganizar) así como un cuadro de texto que muestra el estado actual del juego. Ahora no nos interesa que estos botones cambien su tamaño a una celda de la rejilla, como los otros 16 botones. Eso causaría confusión en los usuarios porque les sería difícil ver las descripciones y los textos de los controles.

Para construir este cuadro de diálogo tenemos que extender la clase del segundo ejemplo, o bien copiar dicha clase y modificarla a continuación. Nosotros hemos decidido copiar la clase.

Hemos añadido dos controles adicionales al nuevo diálogo. Vamos a declarar dos funciones miembro para crear estos controles, a saber, CreateButtonNew() y CreateLabel(). En primer lugar, pues, las declaramos como miembros de la clase:

protected:
   //métodos protegidos

   virtual bool      CreateButtonNew(const long chart,const string name,const int subwin);
   virtual bool      CreateLabel(const long chart,const string name,const int subwin);

   //más métodos protegidos abajo

A continuación se muestra la implementación real de las funciones:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CSlidingPuzzleDialog::CreateButtonNew(const long chart,const string name,const int subwin)
  {
   if(!m_button_new.Create(chart,name+"buttonnew",m_subwin,0,0,101,101))
      return(false);
   m_button_new.Text("New");
   m_button_new.ColorBackground(clrYellow);
   if(!m_main.Grid(GetPointer(m_button_new),4,0,1,2))
      return(false);
   return(true);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CSlidingPuzzleDialog::CreateLabel(const long chart,const string name,const int subwin)
  {
   if(!m_label.Create(chart,name+"labelnew",m_subwin,0,0,102,102))
      return(false);
   m_label.Text("click new");
   m_label.ReadOnly(true);
   m_label.TextAlign(ALIGN_CENTER);
   if(!m_main.Grid(GetPointer(m_label),4,2,1,2))
      return(false);
   return(true);
  }

También tenemos que modificar ligeramente algunas funciones. Como hemos añadido nuevos controles a la rejilla, las funciones tales como Check(), HasNorth(), HasSouth(), HasWest(), y HasEast() se tienen que cambiar. Esto es para asegurarnos de que las baldosas reales no intercambian valores con el control equivocado. En primer lugar, pondremos el prefijo 'block' a las baldosas numeradas (como argumento de CreateButton()), y luego utilizaremos este prefijo para identificar si el control seleccionado es realmente una baldosa numerada. El siguiente código muestra la función Check() actualizada:

bool CSlidingPuzzleDialog::Check(void)
  {
   for(int i=0;i<m_main.ControlsTotal();i++)
     {
      CWnd *control=m_main.Control(i);
      if(StringFind(control.Name(),"block")>=0)
        {
         CButton *button=control;
         if(CheckPointer(button))
           {
            if(button.Text()!=IntegerToString(i))
              {
               m_label.Text("no solucionado");
               return(false);
              }
           }
        }
     }
   m_label.Text("solucionado");
   m_solved=true;
   return(true);
  }

Aquí, utilizamos la función StringFind para asegurarnos de que el control seleccionado es efectivamente un botón, y que se trata de una baldosa numerada. Esto es necesario, de otro modo obtendremos errores como "conversión de punteros incorrecta" si asignamos el control a una instancia de CButton, lo que se hace en una de las líneas de código que siguen. En este código también observamos que en lugar de utilizar la función Print para mostrar el estado en la ventana del terminal, simplemente editamos el texto del control CEdit.


8. Anidación de contenedores

Es posible colocar una rejilla dentro de otro contenedor, por ejemplo, un contenedor caja o una rejilla más grande. Cuando la colocamos en un contenedor CBox, la cuadrícula entera sigue el diseño y la alineación de su contenedor padre. Sin embargo, como cualquier otro control o contenedor colocado en una instancia CBox, debería asignarse a la rejilla una altura y anchura preferentes. Por otro lado, el tamaño de la cuadrícula se calcula automáticamente cuando se coloca dentro de otra.


9. Ventajas e inconvenientes

Ventajas:

Inconvenientes:


10. Conclusión

En este artículo hemos analizado la utilización de una plantilla cuadriculada o rejilla (grid layout) para construir y diseñar paneles gráficos. Esta clase layout proporciona una herramienta adicional que ayuda a construir más fácilmente controles GUI en MetaTrader. Hemos visto las ventajas que en algunos casos ofrece nuestra clase layout con respecto al layout de caja estándar.

Y hemos presentado dos clases para crear una rejilla: CGrid y CGridTk. La clase CGrid es un control auxiliar que hace de contenedor para los controles principales de un panel GUI. Añade controles fundamentales como componentes hijo y los reorganiza en una cuadrícula ordenada. La clase CGridTk es una extensión de CGrid que ofrece más funciones de control personalizado, posicionamiento y cambio de tamaño. Estas clases son como bloques fundamentales que podemos utilizar para crear fácilmente controles gráficos en MetaTrader.