English Русский Español Deutsch 日本語
Usando Layouts e Containers para Controles da Interface Gráfica do Usuário (GUI): A Classe CGrid

Usando Layouts e Containers para Controles da Interface Gráfica do Usuário (GUI): A Classe CGrid

MetaTrader 5Exemplos | 5 abril 2016, 14:03
2 049 0
Enrico Lambino
Enrico Lambino

Índice


1. Introdução

A classe CGrid é um gerenciador de layout usado no projeto dos controles da Interface Gráfica do Usuário (GUI) para janelas de diálogo no MetaTrader. É uma das classes containers personalizada que pode ser usada na concepção da GUI sem depender de um posicionamento absoluto.

É altamente recomendável ler o artigo sobre a classe CBox, antes de prosseguir com os conceitos discutidos neste artigo.


2. Objetivos

O uso da classe CBox é o suficiente para a maioria das janelas simples de diálogo. No entanto, com o número de controles aumentando na janela de diálogo, o uso de vários containers CBox têm as seguintes desvantagens:

  • Mais profunda nidificação de controles.
  • Mais controles necessários do container no layout.
  • Mais linhas no código para obter solução de algumas coisas simples.

A maioria (se não todos) destes problemas com a classe CBox podem ser evitados se os controles forem colocados numa grade, em vez de caixas individuais de containers. Os objetivos deste artigo são:

  • Implementar uma classe para organizar controles em uma grade pré-definida.
  • Implementar uma alternativa mais fácil de containers Cbox aninhados.

E semelhante à classe CBox, os seguintes objetivos também precisam ser cumpridos:

  • O código deve ser reutilizável.
  • Alterar uma parte da interface deve ter um impacto mínimo sobre outros componentes.
  • O posicionamento dos componentes no interior da interface deve ser calculado automaticamente.

Neste artigo, na utilização da classe CGrid, pretendemos definir um gerenciador de layout que permite atingir os objetivos mencionados acima.


3. A Classe CGrid

A classe CGrid cria um container para um ou mais controles da GUI e os apresent num arranjo de grade. Um exemplo layout de uma instância da classe CGrid é mostrado na ilustração a seguir:

Layout da classe CGrid

Figura 1. Layout da classe CGrid

Usar esta classe pode ser conveniente, especialmente se os controles a serem adicionados à rede têm dimensões idênticas, tais como um conjunto de botões ou caixas de edição dentro da área de cliente.

O exemplo acima é uma grade de células 4x4 (4 colunas e 4 linhas). No entanto, o nosso objetivo foi desenvolver uma classe que fosse capaz de acomodar qualquer número de linhas e colunas numa grade.

Vamos declarar a classe CGrid como uma ramificação da classe CBox. Com isso, nós podemos substituir facilmente as funções virtuais da classe principal. Além disso, isso nos dará a capacidade de manipular as instâncias desta classe, como as instâncias da CBox:

#include "Box.mqh"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CGrid : public CBox
  {
protegido:
   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();
protegido:
   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. Inicialização

Semelhante a outros containers e controles, criamos a grade atual chamando o método Create() da classe. No entanto, semelhante a uma instância do CBox, especificar a posição do controle é opcional neste ponto. Nós podemos simplesmente declarar a largura e altura do controle usando as propriedades x2 e y2. Se a grade é o único container (container principal) para anexar a área do cliente, o seguinte código será utilizado (com m_main como uma instância de CGrid):

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

Logo após a criação da grade, para inicializar teremos que chamar o método Init(). Para inicializar uma instância de CGrid, vamos precisar especificar o número de colunas e linhas, dividida através da área principal do cliente (ou uma subseção do mesmo), bem como o espaço (horizontal e vertical) entre cada célula na grade. Para realmente inicializar a grade, precisamos chamar o método Init() no código fonte. O código a seguir criará uma grade 4x4 com aberturas horizontais e verticais entre cada célula de 2 pixels:

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

O método Init() possui 4 parâmetros:

  1. numero de linhas;
  2. número de colunas;
  3. lacuna horizontal (em pixels);
  4. lacuna vertical (em pixels).

As lacunas horizontais e verticais entre as células são parâmetros opcionais. Por padrão, esses valores seriam zero, a menos que inicializados com valores personalizados.


3.2. Espaço Entre os Controles

Os parâmetros hgap (horizontal gap) e vgap (vertical gap) determinam o espaçamento entre cada célula na grade. Uma vez que a grade maximiza a utilização da área inteira do cliente ou do container, o espaço restante para os controles de qualquer orientação horizontal ou vertical é mostrada na seguinte fórmula:

total size left for controls = total area space - (gap * (number of cells+1))

A fórmula acima é usada na função Pack() da classe.


3.3. Redimensionamento do Controle

Na classe CGrid, o tamanho de cada controle na grade irá ser redimensionado para ocupar o tamanho completo da célula. Assim, usando esse layout, é aceitável criar ou inicializar os elementos de controle com tamanho zero. O controle será redimensionado mais tarde, durante a criação da janela do diálogo principal (CDialog ou CAppDialog), pois o método Pack() é chamado para a instância da classe.

O tamanho total restante (horizontal ou vertical), calculado na fórmula indicada na seção anterior determina o tamanho y ou x de qualquer célula particular dentro da grade. Para o tamanho de cada célula, a grade irá usar as seguintes fórmulas:

xsize = total size left for controls / total number of columns

ysize = total size left for controls / total number of rows

O redimensionamento real é feito dentro do método CheckControlSize() da classe.


4. Exemplo #1: Uma Grade Simples de Botões

Para ilustrar um exemplo básico de como usar a classe CGrid, apresentamos uma grade simples de botões. Um print da Interface Gráfica do Usuário (GUI) é mostrado a seguir:

Uma Grade Simples de Botões

Figure 2. Uma Grade Simples de Botões

Como podemos ver, o diálogo mostrado acima contém uma grade de células 3x3, em que cada célula contém um botão. Cada botão é colocado de maneira uniforme em toda a grade, que ocupa toda a área do cliente na janela de diálogo.

A fim de criar esta rede, precisamos construir um EA ou indicador seguindo o formato descrito no artigo sobre o CBox, que também é essencialmente similar ao exemplo de controles no MetaTrader. Ou seja, nós declaramos um arquivo de origem principal, que contém a declaração de uma instância de uma janela CAppDialog personalizada (juntamente com outros manipuladores de eventos) e vinculamos com um arquivo de cabeçalho, contendo a declaração real da classe que está sendo usada.

Para a grade 3x3, precisamos ter uma instância da classe CGrid como um membro da classe, juntamente com um conjunto de 9 teclas (1 para cada célula da grelha):

class CGridSampleDialog : public CAppDialog
  {
protegido:
   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();
  };

O próximo passo seria a substituição das funções virtuais públicas da classe 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)

O mapa do evento está vazio para este exemplo, uma vez que não atribuirá qualquer manipulação de eventos aos botões.

A etapa final seria declarar as funções de proteção da classe, que serão efetivamente utilizadas na construção da grade com seus controles:

protegido:
   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);

Usando este exemplo, podemos ver algumas das vantagens do CGrid sobre o CBox. A fim de construir um layout similar, usando a classe CBox sozinha exigiria 4 containers diferentes. Isto é porque CBox só pode lidar com uma única coluna ou uma única fileira. Com a CGrid, por outro lado, reduzimos o número de containers de 4 pra 1, diminuindo as declarações e linhas do 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);
  }

O método de classe CreateMain() é responsável por construir o próprio controle da grade. Ele funciona semelhantemente a criação do controle na CBox. A única diferença é que a classe CGrid requer um método adicional, o Init(). Por outro lado, a CBox não precisa disto.

A implementação na classe CreateButton() é mostrada no trecho do código abaixo:

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

Uma vez que os processos da criação dos botões são bastante semelhantes, em vez de utilizar um método para a criação de cada tecla, vamos usar uma função genérica para criar todos os botões. Isso é feito pelo método de classificação CreateButton() implementado acima. Vamos chamar esse método dentro do método de classe virtual Create(), após a criação da janela de diálogo e da grade. Como mostrado no trecho do código ao método virtual Create(), implementamos um loop for, a fim de realizar este método. Uma vez que os botões são estaticamente declarados dentro da classe, os botões já estão criadas mediante a declaração, por isso não há necessidade de usar o novo operador. Nós simplesmente obtemos o ponteiro (automático) de cada botão e então chamamos cada um dos seus métodos Create().


5. Exemplo #2: Quebra-cabeça Deslizante

Nosso segundo exemplo envolve um jogo chamado de Quebra-cabeça Deslizante. Neste jogo, o usuário obtém um conjunto de números que variam de 1 a 15 numa grade 4 x 4. A meta do usuário é reorganizar os tijolos para que os números sejam dispostos ordenbamente, da esquerda para a direita e de cima para baixo. O jogo é considerado completo, assim que o usuário tenha classificado os tijolos em números e na ordem correta, como mostrado na imagem seguinte:

Quebra-cabeça Deslizante

Figura 3. Quebra-cabeça Deslizante

Além dos métodos de classe envolvidos na construção da janela de diálogo, criar esta aplicação exigiria os seguintes recursos adicionais:

  • método para criação de botões;
  • processo para embaralhar aleatoriamente os tijolos;
  • método para verificar se uma determinada célula está ao lado de um tijolo vazio;
  • método para verificar se o quebra-cabeças já está resolvido ou não;
  • método de evento click para cada botão na grade.


5.1. Criação da Janela de Diálogo

Declaramos a classe como uma extensão da classe CAppDialog, com os seus membros protegidos (ou privados), construtores e destruidores:

class CSlidingPuzzleDialog : public CAppDialog
  {
protegido:
   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();   
  };

O código a seguir mostra o método Create() na classe.

Declaração (no âmbito da definição de classe, as funções de membro público):

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

Implementação:

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

A partir daqui, podemos ver que o diálogo tem as funções CreateMain(), utilizadas n construção da grade e função CreateButton() num loop for, usado para criar os botões na grade. Também podemos ver aqui o método Pack() da instância CGrid sendo chamado (para o reposicionamento dos controles) e a grade sendo anexada na área principal do cliente, utilizando o método de classe Add(). A inicialização do jogo também está presente através do método shuffle().


5.2. Botões

A seguir, o trecho de código para o método de classe 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);
  }

Aqui vemos que o método de classe é semelhante ao método CreateButton() no exemplo anterior. Segundo este método, vamos atribuir a cada célula um valor inicial (de 1 a 16). Nós também ocultamos a célula 16, uma vez que serviria como uma célula vazia.


5.3. Verificação dos Tijolos Adjacentes

É necessário verificar se existe um tijolo adjacente numa determinada direção . Caso contrário, a célula vazia trocará valores com um botão que não existe. A verificação real para tijolos adjacentes é feita usando as funções HasNorth(), HasSouth(), HasEast(), e HasSouth(). O seguinte trecho do código mostra o 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 funções verificam se existem ou não um botão (ou uma célula vazia) autorizado a mover nos pontos cardeais, que também são as direções, onde a célula vazia pode andar livremente. Se um determinado botão é encontrado em torno do centro da grade, então está livre para se mover em todos os quatro sentidos. No entanto, se for encontrado num dos lados, então haveria alguns tijolos que não existiriam. Por exemplo, não considerando as células vazias, a primeira célula na grade pode mover para a direita ou para baixo, mas não pode se mover a sua esquerda ou a parte superior, enquanto que a sexta célula pode mover-se livremente em todas as quatro direções.


5.4. Embaralhando os Tijolos

O seguinte trecho de código mostra o método Shuffle() da classe:

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

Quando embaralhar os tijolos, o processo deverá envolver uma forma aleatória. Caso contrário, devem sempre embaralhar na mesma ordem. Usaremos as funções MathSrand e MathRand, a fim de realizar isto e usar a hora local como o ponto inicial.

Antes de qualquer embaralhamento ocorrer, precisamos inicializar os valores dos botões com seus primeiros valores padrões. Isso impede qualquer evento onde o quebra-cabeça se tornaria insolúvel, ou talvez muito difícil de resolver. Fazemos isso pela atribuição a célula vazia no tijolo 16 e atribuímos os valores em conformidade. Nós também atribuímos a 16ª célula ao ponteiro da célula vazia (membro da classe) declarada anteriormente.

No final do método da classe, a triagem dos tijolos é realizada. Os botões não são realmente ligados. Em vez disso, seus valores são simplesmente trocados, dando a ilusão de movimento. E como podemos ver, esta é a abordagem mais fácil. Cada loop iria verificar se existe um tijolo adjacente e se o tijolo é uma célula vazia, então os valores do botão vazio e do botão selecionado aleatoriamente, serão trocados.

Também indicamos um valor padrão de quantas vezes a troca de tijolos ocorre. O valor padrão é 30, mas este valor pode ser alterado, a fim de aumentar ou diminuir a dificuldade. O embaralhamento pode ser mais ou menos difícil do que o nível de dificuldade configurado, dependendo se o botão alvo adquiriu ou não um ponteiro válido para cada iteração.


5.5. Evento de Botão Click

A fim de processar os eventos de clique para cada botão, seria preciso declarar um manipulador de eventos click. No entanto, a fim de diminuir a duplicação do código, vamos declarar um método de classe que processa todos os eventos click do botão:

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

A função IsMovable() verifica se apenas um certo número de tijolos tenha algum outro adajacente vazio a ele, utilizando as funções que envolvem pontos cardeais (por exemplo HasNorth(), HasSouth()). Se o botão tem um tijolo adajacente vazio a ele que é móvel, e por conseguinte, a função Swap() é chamada, troca-se o valor do botão com o da célula vazia. Ele também chama a função Check() logo após cada troca bem sucedida.

Em seguida, vamos criar manipuladores de eventos separados para cada botão. Aqui está um exemplo do manipulador de eventos para o primeiro botão:

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

Cada um desses manipuladores de eventos acabaria por chamar OnClickButton() em algum ponto. Também precisamos declarar esses métodos de classe no mapa do evento:

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, é possível chamar o manipulador de eventos click para cada um dos botões do mapa do evento em si, evitando a declaração dos membros da classe do manipulador de eventos separados para cada botão.

Finalmente, adicione a função OnEvent() de membro público para a declaração de classe:

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


5.6. Checagem

Teremos que verificar a ordem das células em cada clique de botão, verificando se o quebra-cabeça já está resolvido. Isto é realizado pela função de membro 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("status: not solved: "+button.Text()+" "+IntegerToString(i));
            return(false);
           }
        }
     }
   Print("status: solved");
   return(true);
  }

A verificação apenas é executada a partir do 2º controle até o último controle. O primeiro controle seria sempre o plano de fundo, que não é um botão, enquanto o controle final seria a célula vazia e que já não precisa ser verificada.


6. A Classe CGridTk


6.1. Problemas com a classe CGrid

Nós encontramos vários problemas ao usar a classe CGrid:

  • Espaço vazio - o CGrid poderia simplesmente colocar o próximo controle na coluna seguinte da linha atual e passaria para a próxima linha apenas quando a linha atual estivesse cheia.
  • O posicionamento e o tamanho dos controles personalizados - a disposição pode ser útil num certo número de casos, mas pode ser rígida em outros. Isso ocorre porque cada controle no âmbito da grade deverá ocupar exatamente uma célula.

No caso do posicionamento de células vazias, que por vezes podem desejar um controle específico a ser posicionado longe de qualquer dos seus semelhantes (horizontal, vertical, ou ambos), provavelmente mais longe do que as lacunas verticais e horizontais disponíveis na grade. Um exemplo seria a separação de um conjunto de botões de outro conjunto de botões, ou posicionar um botão no lado esquerdo da área do cliente (tendendo a esquerda) com um outro lado (tendendo a direita). Muitas vezes vemos encontramos nas páginas da web esses tipos de projetos da GUI sobre as diversas formas.

No primeiro problema mencionado acima, podemos resolvê-lo através da criação de controles vazios. Pode ser algum controle que não tenha muitos componentes cosméticos, tais como um botão ou uma etiqueta. Tambem podemos tornar tais controles invisíveis chamando o seu método Hide(), semelhante ao que fizemos na 16ª(décima-sexta) célula no primeiro exemplo. E finalmente, colocar tais controles numa célula dentro da grade onde gostaríamos de criar algum espaço para respirar. Isso daria a ilusão de espaço em tal célula, mas na realidade a célula é ocupada por um controle invisível.

Esta solução pode ser útil para janelas de diálogo simples, mas em janelas de diálogos mais complexos, pode ser ineficiente e impraticável. O código tenderá a ser maior devido ao número de controles que serão declarados, especialmente se mais de uma célula vazia está envolvida. Além disso, a manutenção do código pode ser difícil, pois o número de células vazias aumenta (por exemplo, uma linha ou coluna inteira de células vazias).

O segundo problema tem algo a ver com a posição e o tamanho dos controles. No que diz respeito ao posicionamento dos comandos individuais da célula, não temos o problema se todos os controles seguirem o mesmo tamanho e distância uns dos outros. Mas se não o fizerem, então nós temos que implementar uma abordagem diferente. Muito provavelmente, a solução seria colocar os controles assimétricos fora da grade e colocá-los em outro lugar através do posicionamento absoluto. Outra alternativa seria colocá-los em outro container, como na CBox ou em outra instância da CGrid.


6.2. CGridTk: Um CGrid melhorado

A classe CGrid padrão pode ter uma ampla gama de aplicações. No entanto, as suas capacidades como um container de grade são muito limitadas. Com base nos dois problemas envolvidos com o uso da classe padrão CGrid discutidos na seção anterior, podemos derivar uma classe muito melhor a partir dela, com as seguintes características (na parte superior do CGrid):

  • Permite a criação de células vazias, sem nenhum controle da GUI.
  • Permite a implementação de controles personalizados quanto a largura e altura, colocados através do tamanho múltiplo do pixel de uma célula de grade.

Com esses recursos, somos capazes de resolver os problemas apresentados na seção anterior. Além disso, isto nos daria mais liberdade na colocação real e posicionamento das células, semelhante ao posicionamento absoluto. No entanto, ao contrário de posicionamento absoluto, estamos utilizando o tamanho das células como a unidade de base de posicionamento, em vez de 1 pixel. Novamente, nós sacrificamos a precisão por uma questão de conveniência quando desenhamos, é mais fácil de visualizar o tamanho de 1 célula na grade do que, digamos, 100 pixels na tela.

Vamos mudar o nome da classe para GridTk. O código é mostrado abaixo:

#include "Grid.mqh"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class CGridTk : public CGrid
  {
protegido:
   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);
  }

Além do método Add(), nós introduzimos um novo método para adicionar controles na grade, o método Grid(). Quando este método de classe é usado, o controle pode ser atribuído a uma posição e tamanho personalizado, com base no tamanho múltiplo do tamanho de uma célula.

Podemos ver que a classe tem um membro da classe CConstraints, que será discutido mais adiante nesta seção.


6.2.1. Atributos RowSpan e ColmnSpan

Com os atributos rowspan e colmnspams, agora podemos definir o comprimento e a largura que o controle deve ter. Esta é uma melhoria em relação a um tamanho padrão de uma célula da grade, mas ainda menos precisa do que o posicionamento absoluto. No entanto, é importante notar que a classe CGridTk não usa o método CheckControlSize() do CBox e CGrid. Pelo contrário, ela já realiza o redimensionamento real dos controles dentro do método Pack() em si.


6.2.2. A Classe CConstraints

Para cada controle, teremos de definir um conjunto de restrições que definirá como cada controle será posicionado na grade, quais células que devem ocupar, bem como a forma como elas devem ser redimensionadas. Podemos reposicionar diretamente e redimensionar os controles assim que são adicionados através da utilização do método Grid() da classe CGridTk. No entanto, por uma questão de coerência, será atrasado o redimensionamento e reposicionamento até que o método Pack() seja chamado (similar ao que é feito dentro da classe CBox). Para fazer isso, é necessário armazenar as limitações na memória, que é o propósito da classe CConstraints:

class CGridConstraints : public CObject
  {
protegido:
   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()
  {
  }

Através do objeto da classe construtora sozinha, podemos concluir que a classe CConstraints armazena as linhas, colunas, atributos "rowspan" e "colspan" para cada controle, mas isso somente é possível pela chamada do o método Grid(), como pode ser visto na implementação da classe CGridTk. Além disso, a referida classe só armazena informações, pois as informações são usadas como são implementadas sob o CGridTk.


6.3.3. Posicionamento padrão

Se um certo controle não fosse adicionado à grade, usando o método Grid(), o posicionamento padrão seria utilizado. Tal controle foi adicionado à grade usando o método Add(), isto significa que a grade não tem restrições (nenhum objeto GridConstraints é armazenado na instância da classe de grade). Assim, os métodos atualizados no CGridTk não seriam capazes de fazer qualquer coisa sobre esses controles, pois tanto quanto o posicionamento e redimensionamento seriam confusos. O método de colocação seria semelhante ao método da classe CGrid, como um método de contingência ou padrão de posicionamento. Isto é, tais controles seriam empilhadas como tijolos numa parede, mas começando a partir da porção superior esquerda da área de cliente, como mostrado no primeiro exemplo.


7. Exemplo #3: Quebra-cabeça Deslizante (Melhorado)

A fim de melhorar ainda mais o quebra-cabeça deslizante, precisamos fazer algumas mudanças no segundo exemplo:

  1. Criamos um botão "New" para novo jogo, de modo que o expert advisor não precise ser reiniciado para começar um novo jogo.
  2. Criamos um controle na janela de diálogo que mostra o estado do jogo, elimando a necessidade de abrir a guia "diário" da janela do terminal.
  3. Implementamos um tamanho diferente para os novos controles.
  4. Fizemos algumas mudanças cosméticas, como a coloração dos tijolos e mostrando todas as peças na grade (opcional).

O quebra-cabeça deslizante melhorado é mostrado no "print" a seguir:

Quebra-cabeça Deslizante (Melhorado)

Figura 4. Quebra-cabeça Deslizante (Melhorado)

Como pode ser visto no "print" da tela, nós adicionamos novos componentes para a janela de diálogo. Existe um botão que nos permite criar um novo jogo (remodelação), bem como uma caixa de texto que mostra o estado atual do jogo. Agora, nós não queremos que esses botões sejam redimensionados para o tamanho de uma célula da grade, assim como os outros 16 botões. Isso pode causar alguma confusão para os usuários, pois eles podem achar que é difícil ver as descrições ou texto destes controles.

Para construir esse diálogo, teremos de estender a classe no segundo exemplo, ou copiar a classe dita e, em seguida, modificá-la. Aqui, vamos optar por simplesmente copiar em vez de estender a classe no segundo exemplo.

Com este novo diálogo, foram adicionados dois controles adicionais. Vamos precisar declarar as funções de membro que iriam realizar a criação de tais controles, ou seja, CreateButtonNew() e CreateLabel(). Em primeiro lugar, teremos de declará-los como membros da classe:

protegido:
   //início dos métodos de membros 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);

   //mais métodos de membros protegidos abaixo..

A implementação efetiva das funções de membro é mostrado abaixo:

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

Também seria necessário alterar ligeiramente algumas funções. Uma vez que novos controles são adicionados à rede, funções de membro, tais como: Check(), HasNorth(), HasSouth(), HasWest()e HasEast() precisarão ser modificadas. Isso é para se certificar de que os tijolos reais não vão mudar os valores com o controle errado. Primeiro, vamos dar números aos tijolos com o prefixo 'block' (como um argumento em CreateButton()), então usar esse prefixo na ordem de identificação ou não, caso o controle selecionado seja realmente um tijolo de número. O código a seguir mostra a atualização da função de membro, Check():

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("not solved");
               return(false);
              }
           }
        }
     }
   m_label.Text("solved");
   m_solved=true;
   return(true);
  }

Aqui, usamos a função StringFind para nos certificar que o controle selecionado é realmente um botão e que é o número de um tijolo. Isso é necessário, pois do caso contrário vamos receber erros, como "escolha incorreta de ponteiros" ao atribuir o controle a uma instância de CButton, que é feito numa das linhas de código que segue. Neste código, vemos também que em vez de usar a função Print para exibir o status na janela do terminal, nós simplesmente editamos o texto no controle CEdit.


8. Aninhamento do Container

É possível colocar uma grade dentro de outro container, tal como um container box ou uma grade maior. Quando colocado dentro de um container CBox, toda a grade segue o layout e alinhamento do container-principal. No entanto, assim como quaisquer controles ou containers colocados dentro de uma instância de CBox, a grade deve ser designada a uma altura e largura específica. Por outro lado, quando colocados dentro de outra grade, o tamanho da grade será automaticamente calculado.


9. Vantagens e Desvantagens

Vantagens:

  • Tem o potencial de reduzir o número de containers necessários para a janela de diálogo, especialmente se os controles idênticos estão presentes.
  • Melhor gerenciamento e sustentabilidade do que o CBox.
  • Melhor confiabilidade em utilizar do que o posicionamento absoluto.

Desvantagens:

  • Menos preciso do que o posicionamento absoluto.
  • O alinhamento pode ser um pouco fora do lado direito e do lado inferior, caso o tamanho da área do cliente não seja proporcional a área de cliente. Isto pode ocorrer quando o tamanho da área de cliente, menos o espaço para cada célula, origina um número inteiro (não inteiro) ao ser dividido pelo número de células ou colunas. Como um pixel ainda não pode ser mais dividido, quaisquer pixels ou restos excedentes iriam acumular-se nestes lados, resultando numa aparência ligeiramente irregular. No entanto, isso pode ser facilmente resolvido ao redimensionar a janela de diálogo principal.


10. Conclusão

Neste artigo, nós consideramos a possibilidade de usar um layout e desenho de grade na construção de painéis gráficos. Esta classe de layout adicional fornece uma ferramenta extra, facilitando a construção de controles da Interface Gráfica de Usuário (GUI) no MetaTrader. Em alguns casos, temos visto as vantagens da utilização desta classe no layout padrão de caixa.

Nós apresentamos duas classes para criar uma grade: o CGrid e o CGridTk. A classe CGrid é um controle auxiliar que atua como um container para controles essenciais num painel da Interface Gráfica de Usuário (GUI). Ela adiciona controles essenciais, como os seus componentes ramificados, e os organiza numa grade. A classe CGridTk é uma extensão da classe CGrid, fornece mais recursos quanto ao posicionamento e redimensionamento de controle personalizado. Estas classes podem servir como blocos de construção fundamentais, facilitando a criação dos controles gráficos no MetaTrader.

Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/1998

Arquivos anexados |
Grid.zip (12.35 KB)
Indicador Rope por Erik Naymanf Indicador Rope por Erik Naymanf
O artigo revela como o indicador "Rope" foi criado com base na "The Small Encyclopedia of Trader" (Pequena Enciclopédia do Trader), por Erik L. Nayman. Este indicador mostra a direção da tendência, usando cálculos dos valores das tendências de alta e de baixa durante um período de tempo determinado. O artigo também conta com os princípios do desenvolvimento e cálculos do indicador, bem como exemplos do código. Outros temas abordados incluem o desenvolvimento de um Expert Advisor com base no indicador, e também, a otimização dos parâmetros externos.
Avaliação e seleção de variáveis para os modelos de aprendizado da máquina Avaliação e seleção de variáveis para os modelos de aprendizado da máquina
Este artigo foca sobre as especificidades de escolha, o pré-condicionamento e avaliação das variáveis de entrada (preditoras) para uso em modelos de aprendizagem da máquina. Novas abordagens e oportunidades de análises preditoras profundas e suas influências no possível sobre-ajuste (overfitting) dos modelos serão consideradas. O resultado global do uso de modelos, em grande parte, depende do resultado desta etapa. Vamos analisar dois pacotes, oferecendo abordagens novas e originais para a seleção dos preditores.
Características dos Experts Advisors Características dos Experts Advisors
O desenvolvimento de expert advisors no sistema de negociação MetaTrader tem uma série de características.
MQL5 para iniciantes: Proteção antivandalismo de objetos gráficos MQL5 para iniciantes: Proteção antivandalismo de objetos gráficos
O que o seu programa deve fazer, se os painéis de controle gráfico foram removidos ou modificados por alguém? Neste artigo, vamos mostrar a você o porquê de não ter objetos no gráfico "sem dono" e como não perder o controle sobre eles, se forem renomeados ou excluídos após o aplicativo ser deletado.