Interfaces Gráficas XI: Refatoração do código da biblioteca (build 14.1)

Anatoli Kazharski | 22 agosto, 2017


Conteúdo

Introdução

O primeiro artigo Interfaces gráficas I: Preparação da Estrutura da Biblioteca (Capítulo 1) considera em detalhes a finalidade desta biblioteca. Em cada parte, é fornecido uma lista dos capítulos com seus respectivos links no final de cada artigo. Lá, você também poderá baixar a última versão completa da biblioteca. Os arquivos devem estar localizados nas mesmas pastas que o arquivo baixado.

A última atualização da biblioteca destina-se a otimizar o código, reduzindo seu tamanho e tornando sua implementação ainda mais orientada a objetos. Tudo isso facilitará a aprendizagem do código. Uma descrição detalhada das mudanças feitas permitirá que os leitores modifiquem de forma independente a biblioteca para suas próprias tarefas, passando o menor tempo possível. 

Devido ao grande volume de publicação, a descrição da última atualização da biblioteca foi dividida em dois artigos. A primeira parte é apresentada aqui.

Propriedades comuns dos controles

Primeiramente, as mudanças envolveram as propriedades comuns dos controles da biblioteca. Anteriormente, os campos e métodos da classe para determinadas propriedades (cor de fundo, borda, texto, etc.) foram armazenados em classes derivadas de cada controle individual. Em termos de programação orientada a objetos, isso é uma sobrecarga. Agora que todos os controles de interface gráfica necessários foram implementados, é fácil determinar os campos e métodos que são repetidos frequentemente, responsáveis ​​por definir as propriedades gerais e movê-los para a classe base. 

Vamos definir a lista completa de propriedades, que são inerentes a todos os controles da biblioteca e podem ser colocados na classe base.

  • Imagem (ícone) lido a partir de um arquivo ou processado de forma programática.
  • Os recuos formam a imagem ao longo dos eixos X e Y.
  • Cor de fundo.
  • Cor da borda.
  • Cor do texto.
  • Descrição do texto.
  • Recuos da descrição do texto.

Agora, é necessário decidir qual classe irá conter os campos e métodos para configurar e obter essas propriedades. 

Existem duas classes na hierarquia de classe de cada controle: CElementBase e CElement. Os campos e métodos para as seguintes propriedades serão colocadas na classe base CElementBase: coordenadas, tamanhos, identificadores, índices, bem como os modos peculiares a cada controle da lista. Os campos e métodos para gerenciar as propriedades relacionadas à aparência dos controles serão colocados na classe derivada CElement.

Além disso, os métodos para criar o nome do objeto gráfico dos controles são adicionados à classe CElementBase. Anteriormente, o nome foi gerado nos métodos para criar os controles. Agora que cada controle é desenhado em um objeto separado, é possível criar métodos universais que podem ser localizados na classe base. 

Os métodos CElementBase::NamePart() são projetados para obter e definir a parte do nome que indica o tipo de controle.

//+------------------------------------------------------------------+
//| Classe base do controle                                          |
//+------------------------------------------------------------------+
class CElementBase
  {
protected:
   //--- Parte do nome (tipo do controle)
   string            m_name_part;
   //---
public:
   //--- (1) Armazena e (2) retorna a parte do nome do controle
   void              NamePart(const string name_part)                { m_name_part=name_part;                }
   string            NamePart(void)                            const { return(m_name_part);                  }
  };

O método CElementBase::ElementName() é usado para gerar o nome completo do objeto gráfico. Este método deve ser passado na parte do nome que indica o tipo de controle. Se acontecer da parte do nome ter sido definida anteriormente, o valor passado não será usado. Devido às mudanças recentes na biblioteca (veja abaixo), esta abordagem é usada nos casos em que um controle é derivado de outro e a parte do nome precisa ser redefinida.

class CElementBase
  {
protected:
   //--- nome do controle
   string            m_element_name;
   //---
public:
   //--- Elaborando o nome do objeto
   string            ElementName(const string name_part="");
  };
//+------------------------------------------------------------------+
//| Retorna o nome do controle gerado                                |
//+------------------------------------------------------------------+
string CElementBase::ElementName(const string name_part="")
  {
   m_name_part=(m_name_part!="")? m_name_part : name_part;
//--- Elaborando o nome do objeto
   string name="";
   if(m_index==WRONG_VALUE)
      name=m_program_name+"_"+m_name_part+"_"+(string)CElementBase::Id();
   else
      name=m_program_name+"_"+m_name_part+"_"+(string)CElementBase::Index()+"__"+(string)CElementBase::Id();
//---
   return(name);
  }

Ao manipular os cliques do mouse em um determinado controle, é necessário verificar o nome do objeto gráfico clicado. Esta verificação foi repetida frequentemente para muitos controles, portanto, foi colocado o método especial CElementBase::CheckElementName() na classe base:

class CElementBase
  {
public:
   //--- Verificando se a linha contém uma parte significativa do nome do controle
   bool              CheckElementName(const string object_name);
  };
//+------------------------------------------------------------------+
//| Retorna o nome do controle gerado                                |
//+------------------------------------------------------------------+
bool CElementBase::CheckElementName(const string object_name)
  {
//--- Se o pressionamento foi nesse controle
   if(::StringFind(object_name,m_program_name+"_"+m_name_part+"_")<0)
      return(false);
//---
   return(true);
  }

Quanto às outras propriedades, faz sentido ocupar apenas a descrição dos métodos para trabalhar com imagens.


Classe para trabalhar com dados de imagem

Para trabalhar com as imagens, a classe CImage foi implementada, onde os dados da imagem podem ser armazenados:

  • array de pixels de imagem;
  • dimensões da imagem (largura e altura);
  • caminho para o arquivo.

Para obter o valor dessas propriedades, os métodos apropriados serão necessários:

//+------------------------------------------------------------------+
//| Classe para armazenar os dados da imagem                         |
//+------------------------------------------------------------------+
class CImage
  {
protected:
   uint              m_image_data[]; // Array de pixels da imagem
   uint              m_image_width;  // Largura da imagem
   uint              m_image_height; // Altura da imagem
   string            m_bmp_path;     // Caminho do arquivo da imagem
   //---
public:
                     CImage(void);
                    ~CImage(void);
   //--- (1) Tamanho do array de dados, (2) definir/retornar os dados (cor de pixels)
   uint              DataTotal(void)                             { return(::ArraySize(m_image_data)); }
   uint              Data(const uint data_index)                 { return(m_image_data[data_index]);  }
   void              Data(const uint data_index,const uint data) { m_image_data[data_index]=data;     }
   //--- Define/retorna a largura da imagem
   void              Width(const uint width)                     { m_image_width=width;               }
   uint              Width(void)                                 { return(m_image_width);             }
   //--- Define/retorna a altura da imagem
   void              Height(const uint height)                   { m_image_height=height;             }
   uint              Height(void)                                { return(m_image_height);            }
   //--- Define/retorna o caminho para a imagem
   void              BmpPath(const string bmp_file_path)         { m_bmp_path=bmp_file_path;          }
   string            BmpPath(void)                               { return(m_bmp_path);                }
  };

O método CImage::ReadImageData() é usado para ler a imagem e armazenar os seus dados. Neste método, o caminho para o arquivo de imagem deve ser passado:

class CImage
  {
public:
   //--- Lê e armazena os dados da imagem passada
   bool              ReadImageData(const string bmp_file_path);
  };
//+------------------------------------------------------------------+
//| Armazena a imagem passada para um array                          |
//+------------------------------------------------------------------+
bool CImage::ReadImageData(const string bmp_file_path)
  {
//--- Retorna, se é uma string vazia
   if(bmp_file_path=="")
      return(false);
//--- Armazena o caminho para a imagem
   m_bmp_path=bmp_file_path;
//--- Reseta o último erro
   ::ResetLastError();
//--- Lê e armazena os dados da imagem
   if(!::ResourceReadImage("::"+m_bmp_path,m_image_data,m_image_width,m_image_height))
     {
      ::Print(__FUNCTION__," > Erro na leitura da imagem ("+m_bmp_path+"): ",::GetLastError());
      return(false);
     }
//---
   return(true);
  }

Às vezes, é necessário copiar os dados da imagem passada. Isso é feito pelo método CImage::CopyImageData(). Um objeto do tipo CImage é passado por referência para este método afim de copiar os dados do array deste objeto. Aqui, o tamanho do array de origem é obtido primeiro e o mesmo tamanho é definido no array do receptor. Então o método CImage::Data() é usado em um loop para recuperar os dados do array passado e armazená-los no array do receptor.

class CImage
  {
public:
   //--- Copia os dados da imagem passada
   void              CopyImageData(CImage &array_source);
  };
//+------------------------------------------------------------------+
//| Copia os dados da imagem passada                                 |
//+------------------------------------------------------------------+
void CImage::CopyImageData(CImage &array_source)
  {
//--- Obtém o tamanho do array de origem
   uint source_data_total =array_source.DataTotal();
//--- Redimensiona o array do receptor
   ::ArrayResize(m_image_data,source_data_total);
//--- Copia os dados
   for(uint i=0; i<source_data_total; i++)
      m_image_data[i]=array_source.Data(i);
  }


O método CImage::DeleteImageData() é usado para excluir os dados da imagem:

class CImage
  {
public:
   //--- Exclui os dados da imagem
   void              DeleteImageData(void);
  };
//+------------------------------------------------------------------+
//| Exclui os dados da imagem                                        |
//+------------------------------------------------------------------+
void CImage::DeleteImageData(void)
  {
   ::ArrayFree(m_image_data);
   m_image_width  =0;
   m_image_height =0;
   m_bmp_path     ="";
  }

A classe CImage está contida no arquivo Objects.mqh. Agora todos os controles serão renderizados, assim, as classes para a criação de primitivas gráficas não são mais necessárias. Elas foram removidos do arquivo Objects.mqh. Uma exceção é feita apenas para o sub-gráfico, o que permite a criação de gráficos semelhantes ao gráfico principal do símbolo. Todos os tipos de aplicações MQL estão localizados em sua janela e, portanto, uma interface gráfica é criada.

//+------------------------------------------------------------------+
//|                                                      Objects.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#include "Enums.mqh"
#include "Defines.mqh"
#include "Fonts.mqh"
#include "Canvas\Charts\LineChart.mqh"
#include <ChartObjects\ChartObjectSubChart.mqh>
//--- Lista de classes no arquivo para uma navegação rápida (Alt+G)
class CImage;
class CRectCanvas;
class CLineChartObject;
class CSubChart;
...


Métodos para trabalhar com imagens

Vários métodos foram implementados para trabalhar com imagens. Todos eles são colocados na classe CElement. Agora é possível configurar o número necessário de grupos (arrays) de imagens para um determinado controle. Isso permite tornar a aparência dos controles mais informativa. O número dos ícones exibidos no controle é definido pelo desenvolvedor do aplicativo MQL. 

Para esse fim, a estrutura EImagesGroup foi criada e um array dinâmico de suas instâncias foi declarado no arquivo CElement. Ele irá conter as propriedades dos grupos de imagens (recuos e a imagem selecionada para exibição), bem como as próprias imagens, que são armazenadas em um array dinâmico do tipo CImage

//+------------------------------------------------------------------+
//| Classe derivada do controle                                      |
//+------------------------------------------------------------------+
class CElement : public CElementBase
  {
protected:
   //--- Grupos de imagens
   struct EImagesGroup
     {
      //--- Array de imagens
      CImage            m_image[];
      //--- Margens do ícone
      int               m_x_gap;
      int               m_y_gap;
      //--- Imagem do grupo selecionado para exibição
      int               m_selected_image;
     };
   EImagesGroup      m_images_group[];
  };

Para adicionar uma imagem a um controle, um grupo deve ser adicionado primeiro. Isso pode ser feito com a ajuda do método CElement::AddImagesGroup(). Os recuos das imagens, do ponto superior esquerdo do controle, devem ser passados neste grupo como argumentos. Por padrão, a primeira imagem no grupo será selecionada para exibição.

class CElement : public CElementBase
  {
public:
   //--- Adicionando um grupo de imagens
   void              AddImagesGroup(const int x_gap,const int y_gap);
  };
//+------------------------------------------------------------------+
//| Adicionando um grupo de imagens                                  |
//+------------------------------------------------------------------+
void CElement::AddImagesGroup(const int x_gap,const int y_gap)
  {
//--- Obtém o tamanho do array de grupos de imagens
   uint images_group_total=::ArraySize(m_images_group);
//--- Adiciona um grupo
   ::ArrayResize(m_images_group,images_group_total+1);
//--- Define os recuos para as imagens
   m_images_group[images_group_total].m_x_gap=x_gap;
   m_images_group[images_group_total].m_y_gap=y_gap;
//--- A imagem padrão
   m_images_group[images_group_total].m_selected_image=0;
  }

O método CElement::AddImage() é projetado para adicionar uma imagem a um grupo. O índice do grupo e o caminho para o arquivo de imagem devem ser especificados como argumentos. A imagem não será adicionada se não houver grupos. Um ajuste para evitar que o tamanho do array seja excedido também está presente aqui. Caso o intervalo for excedido, a imagem será adicionada ao último grupo.

class CElement : public CElementBase
  {
public:
   //--- Adicionando a imagem ao grupo especificado
   void              AddImage(const uint group_index,const string file_path);
  };
//+------------------------------------------------------------------+
//| Adicionando a imagem ao grupo especificado                       |
//+------------------------------------------------------------------+
void CElement::AddImage(const uint group_index,const string file_path)
  {
//--- Obtém o tamanho do array de grupos de imagens
   uint images_group_total=::ArraySize(m_images_group);
//--- Retorna, se não houver grupos
   if(images_group_total<1)
     {
      Print(__FUNCTION__,
      " > Um grupo de imagens pode ser adicionado usando os métodos CElement::AddImagesGroup()");
      return;
     }
//--- Prevenção de estouro de buffer
   uint check_group_index=(group_index<images_group_total)? group_index : images_group_total-1;
//--- Obtém o tamanho do array de imagens
   uint images_total=::ArraySize(m_images_group[check_group_index].m_image);
//--- Aumenta o tamanho do array por um elemento
   ::ArrayResize(m_images_group[check_group_index].m_image,images_total+1);
//--- Adiciona uma imagem
   m_images_group[check_group_index].m_image[images_total].ReadImageData(file_path);
  }


É possível adicionar um grupo imediatamente com uma série de imagens usando a segunda versão do método CElement::AddImagesGroup(). Aqui, além dos recuos, é necessário passar um array com os caminhos dos arquivos como argumentos. Depois de aumentar o tamanho do array de grupos de imagens por um elemento, o programa adiciona todo o array das imagens passadas em um ciclo usando o método CElement::AddImage()(veja acima).

class CElement : public CElementBase
  {
public:
   //--- Adicionando um grupo de imagens com uma série de imagens
   void              AddImagesGroup(const int x_gap,const int y_gap,const string &file_pathways[]);
  };
//+------------------------------------------------------------------+
//| Adicionando um grupo de imagens com uma série de imagens         |
//+------------------------------------------------------------------+
void CElement::AddImagesGroup(const int x_gap,const int y_gap,const string &file_pathways[])
  {
//--- Obtém o tamanho do array de grupos de imagens
   uint images_group_total=::ArraySize(m_images_group);
//--- Adiciona um grupo
   ::ArrayResize(m_images_group,images_group_total+1);
//--- Define os recuos para as imagens
   m_images_group[images_group_total].m_x_gap =x_gap;
   m_images_group[images_group_total].m_y_gap =y_gap;
//--- A imagem padrão
   m_images_group[images_group_total].m_selected_image=0;
//--- Obtém o tamanho do array de imagens adicionadas
   uint images_total=::ArraySize(file_pathways);
//--- Adiciona imagens a um novo grupo, se um array não vazio foi passado
   for(uint i=0; i<images_total; i++)
      AddImage(images_group_total,file_pathways[i]);
  }

Uma imagem em um determinado grupo pode ser configurado ou substituído no tempo de execução do programa. Para fazer isso, use o método CElement::SetImage(), passe o (1) índice do grupo, (2) índice da imagem e o (3) caminho para o arquivo como argumentos. 

class CElement : public CElementBase
  {
public:
   //--- Configuração/substituição da imagem
   void              SetImage(const uint group_index,const uint image_index,const string file_path);
  };
//+------------------------------------------------------------------+
//| Configuração/substituição da imagem                              |
//+------------------------------------------------------------------+
void CElement::SetImage(const uint group_index,const uint image_index,const string file_path)
  {
//--- Verifica se o tamanho do array não excedeu
   if(!CheckOutOfRange(group_index,image_index))
      return;
//--- Exclui a imagem
   m_images_group[group_index].m_image[image_index].DeleteImageData();
//--- Adiciona uma imagem
   m_images_group[group_index].m_image[image_index].ReadImageData(file_path);
  }

Se, no entanto, todas as imagens necessárias forem definidas inicialmente ao criar o controle, é melhor simplesmente alterá-las usando o método CElement::ChangeImage():

class CElement : public CElementBase
  {
public:
   //--- Trocando a imagem
   void              ChangeImage(const uint group_index,const uint image_index);
  };
//+------------------------------------------------------------------+
//| Trocando a imagem                                                |
//+------------------------------------------------------------------+
void CElement::ChangeImage(const uint group_index,const uint image_index)
  {
//--- Verifica se o tamanho do array não excedeu
   if(!CheckOutOfRange(group_index,image_index))
      return;
//--- Armazena o índice da imagem para exibição
   m_images_group[group_index].m_selected_image=(int)image_index;
  }

Para descobrir a imagem atualmente selecionada em um grupo específico, use o método CElement::SelectedImage(). Se não houver grupos ou não há imagens no grupo especificado, o método retorna um valor negativo.

class CElement : public CElementBase
  {
public:
   //--- Retorna a imagem selecionada para exibição no grupo especificado
   int               SelectedImage(const uint group_index=0);
  };
//+------------------------------------------------------------------+
//| Retorna a imagem selecionada para exibição no grupo especificado |
//+------------------------------------------------------------------+
int CElement::SelectedImage(const uint group_index=0)
  {
//--- Retorna, se não houver grupos
   uint images_group_total=::ArraySize(m_images_group);
   if(images_group_total<1 || group_index>=images_group_total)
      return(WRONG_VALUE);
//--- Retorna, se não houver imagens no grupo especificado
   uint images_total=::ArraySize(m_images_group[group_index].m_image);
   if(images_total<1)
      return(WRONG_VALUE);
//--- Retorna a imagem selecionada para exibição
   return(m_images_group[group_index].m_selected_image);
  }


Anteriormente, todas as classes dos controles, onde os usuários precisavam exibir um ícone, tinham métodos para configurar as imagens. Por exemplo, aos botões podem ser atribuídos os ícones de estados liberado e pressionado, bem como o estado bloqueado. Esse recurso permanecerá, pois é uma opção clara e compreensível. Como antes, os recuos dos ícones podem ser configurados usando os métodos CElement::IconXGap() e CElement::IconYGap().

class CElement : public CElementBase
  {
protected:
   //--- Margens do ícone
   int               m_icon_x_gap;
   int               m_icon_y_gap;
   //---
public:
   //--- Margens do ícone
   void              IconXGap(const int x_gap)                       { m_icon_x_gap=x_gap;              }
   int               IconXGap(void)                            const { return(m_icon_x_gap);            }
   void              IconYGap(const int y_gap)                       { m_icon_y_gap=y_gap;              }
   int               IconYGap(void)                            const { return(m_icon_y_gap);            }
   //--- Definindo os ícones para os estados ativo e bloqueado
   void              IconFile(const string file_path);
   string            IconFile(void);
   void              IconFileLocked(const string file_path);
   string            IconFileLocked(void);
   //--- Definindo os ícones para o controle no estado pressionado (disponível/bloqueado)
   void              IconFilePressed(const string file_path);
   string            IconFilePressed(void);
   void              IconFilePressedLocked(const string file_path);
   string            IconFilePressedLocked(void);
  };

O código do método CElement::IconFile() será fornecido como um exemplo. Aqui, se o controle ainda não possui grupos de imagens, um primeiro grupo é adicionado. Se nenhum recuo tiver sido especificado antes de chamar o método, eles serão definidos por valores iguais a zero. Depois de adicionar o grupo, a imagem passada no argumento é adicionada, e o espaço é reservado também para uma imagem no estado bloqueado do controle.

//+------------------------------------------------------------------+
//| Define um ícone para o estado ativo                              |
//+------------------------------------------------------------------+
void CElement::IconFile(const string file_path)
  {
//--- Se ainda não há grupos de imagens
   if(ImagesGroupTotal()<1)
     {
      m_icon_x_gap =(m_icon_x_gap!=WRONG_VALUE)? m_icon_x_gap : 0;
      m_icon_y_gap =(m_icon_y_gap!=WRONG_VALUE)? m_icon_y_gap : 0;
      //--- Adiciona um grupo e uma imagem
      AddImagesGroup(m_icon_x_gap,m_icon_y_gap);
      AddImage(0,file_path);
      AddImage(1,"");
      //--- A imagem padrão
      m_images_group[0].m_selected_image=0;
      return;
     }
//--- Define a imagem para o primeiro grupo como o primeiro elemento
   SetImage(0,0,file_path);
  }

Para descobrir o número de grupos de imagens ou o número de imagens em um grupo específico, é necessário usar os métodos correspondentes (veja a lista de códigos abaixo):

class CElement : public CElementBase
  {
public:
   //--- Retorna o número de grupos de imagens
   uint              ImagesGroupTotal(void) const { return(::ArraySize(m_images_group)); }
   //--- Retorna o número de imagens no grupo especificado
   int               ImagesTotal(const uint group_index);
  };
//+------------------------------------------------------------------+
//| Retorna o número de imagens no grupo especificado                |
//+------------------------------------------------------------------+
int CElement::ImagesTotal(const uint group_index)
  {
//--- Verificando o índice do grupo
   uint images_group_total=::ArraySize(m_images_group);
   if(images_group_total<1 || group_index>=images_group_total)
      return(WRONG_VALUE);
//--- O número de imagens
   return(::ArraySize(m_images_group[group_index].m_image));
  }

União dos controles como parte da otimização do código da biblioteca

Até agora, muitos controles foram praticamente repetidos, com apenas alguns métodos sendo únicos. Isso infla muito o código. Portanto, as mudanças foram feitas na biblioteca, o que reduziu o código e simplificou a compreensão.

1. Mais cedo, duas classes foram desenvolvidas para criar botões.

  • CSimpleButton — botão simples.
  • CIconButton — botão com ícone.

Mas agora, os ícones podem ser criados em qualquer controle usando as ferramentas básicas. Portanto, para criar botões com diferentes propriedades, não há mais necessidade de duas classes. Apenas uma classe modificada foi deixada - CButton. Para centrar o texto no botão, simplesmente habilite o modo correspondente com o método CElement::IsCenterText(), que pode ser aplicado a qualquer controle.

class CElement : public CElementBase
  {
protected:
   //--- Modo de alinhamento de texto
   bool              m_is_center_text;
   //--- 
public:

   //--- Alinha o texto no centro
   void              IsCenterText(const bool state)                  { m_is_center_text=state;          }
   bool              IsCenterText(void)                        const { return(m_is_center_text);        }
  };


2. O mesmo se aplica à criação dos grupos de botões. Nas versões anteriores da biblioteca, foram criadas três classes para a criação de grupos de botões com diferentes propriedades:

  • CButtonsGroup — grupo de botões simples.
  • CRadioButtons — grupo de botões de radio.
  • CIconButtonsGroup — grupo de botões de ícone.

Em todas essas classes, os botões foram criados a partir das primitivas gráficas padrão. Agora, foi mantido apenas as classe CButtonsGroup.. Ele cria botões como controles pré-fabricados do tipo CButton

O modo do botão de radio (quando um botão no grupo está sempre selecionado) pode ser ativado usando o método CButtonsGroup::RadioButtonsMode(). Uma aparência semelhante aos botões de radio pode ser ativada através do método CButtonsGroup::RadioButtonsStyle().

class CButtonsGroup : public CElement
  {
protected:
   //--- O modo do botão de radio
   bool              m_radio_buttons_mode;
   //--- Estilo de exibição de botão de radio
   bool              m_radio_buttons_style;
   //--- 
public:
   //--- (1) Define o modo e (2) exibe o estilo dos botões de radio
   void              RadioButtonsMode(const bool flag)              { m_radio_buttons_mode=flag;       }
   void              RadioButtonsStyle(const bool flag)             { m_radio_buttons_style=flag;      }
  };

3. Considere as seguintes três classes para a criação dos controles com caixas de edição:

  • CSpinEdit — caixa de edição numérica.
  • CCheckBoxEdit — caixa de edição numérica com uma caixa de seleção.
  • CTextEdit — caixa de edição de texto.

Todas as propriedades das classes acima podem ser colocadas de forma compacta em uma única, deixe que ela seja a classe CTextEdit. Se for necessário criar uma caixa de edição numérica com os botões de incremento e decremento, habilite o modo correspondente usando o método CTextEdit::SpinEditMode(). Se for necessário que o controle tenha uma caixa de seleção, habilite esse modo com o método CTextEdit::CheckBoxMode(). 

class CTextEdit : public CElement
  {
protected:
   //--- Modo de controle com uma caixa de seleção
   bool              m_checkbox_mode;
   //--- Modo de caixa de edição de rotação com botões
   bool              m_spin_edit_mode;
   //--- 
public:
   //--- (1) Caixa de seleção e (2) modos de caixa de edição de rotação
   void              CheckBoxMode(const bool state)          { m_checkbox_mode=state;              }
   void              SpinEditMode(const bool state)          { m_spin_edit_mode=state;             }
  };

4. O mesmo se aplica aos elementos para criar as combobox. Anteriormente havia duas classes:

  • CComboBox — botão com uma lista suspensa.
  • CCheckComboBox — botão com uma lista suspensa e uma caixa de seleção.

Manter duas classes quase idênticas é redundante, portanto, apenas uma delas foi mantida — CComboBox. O modo do controle com uma caixa de seleção pode ser habilitado usando o método CComboBox::CheckBoxMode().

class CComboBox : public CElement
  {
protected:
   //--- Modo de controle com uma caixa de seleção
   bool              m_checkbox_mode;
   //--- 
public:
   //--- Configurando o modo de controle com uma caixa de seleção
   void              CheckBoxMode(const bool state)         { m_checkbox_mode=state;                  }
  };

5. Duas classes foram anteriormente destinadas a criar os controles deslizantes:

  • CSlider — controle deslizante numérico simples.
  • CDualSlider — controle deslizante duplo para especificar um intervalo numérico.

Apenas um delas foi mantida — a classe CSlider. O modo deslizante duplo é habilitado usando o método CSlider::DualSliderMode().

class CSlider : public CElement
  {
protected:
   //--- Modo deslizante duplo
   bool              m_dual_slider_mode;
   //--- 
public:
   //--- Modo deslizante duplo
   void              DualSliderMode(const bool state)           { m_dual_slider_mode=state;           }
   bool              DualSliderMode(void)                 const { return(m_dual_slider_mode);         }
  };

6. Anteriormente, a biblioteca continha duas classes para criar visualizações de lista com uma barra de rolagem, uma das quais permite criar uma exibição de lista com caixas de seleção.

  • CListView — lista simples com a capacidade de selecionar um item.
  • CCheckBoxList — lista com caixas de seleção.

Fora disso, apenas a classe CListView foi mantida. Para criar uma lista com caixas de seleção, simplesmente ative o modo correspondente usando o método CListView::CheckBoxMode().

class CListView : public CElement
  {
protected:
   //--- Modo de exibição da lista com caixas de seleção
   bool              m_checkbox_mode;
   //--- 
public:
   //--- Configurando o modo de controle com uma caixa de seleção
   void              CheckBoxMode(const bool state)         { m_checkbox_mode=state;                  }
  };

7. A versão anterior da biblioteca continha até três classes de tabelas:

  • CTable — tabela com caixa de edição
  • CLabelsTable — tabela com rótulo de texto.
  • CCanvasTable — tabela renderizada.

Durante o curso de melhoria da biblioteca, a classe CCanvasTable se tornou a mais avançada. Portanto, as outras classes foram removidas, e a classe CCanvasTable foi renomeada para CTable.

8. Havia duas classes para a criação de guias:

  • CTabs — guias simples.
  • CIconTabs — ícones das guias.

Não há necessidade de manter essas duas classes na biblioteca. O array de ícones das guias foi criado anteriormente a partir de objetos primitivos gráficos. Os botões do tipo CButton agora são usados ​​para esse propósito, onde os ícones podem ser definidos usando os métodos básicos descritos acima. Como resultado, deixamos apenas a classe CTabs.

Hierarquia de controles

A hierarquia dos controles da interface gráfica também mudou. Até agora, todos os controles foram anexados a um formulário (CWindow). Os controles foram alinhados no formulário em relação ao seu ponto superior esquerdo. É necessário especificar as coordenadas para cada controle ao criar uma interface gráfica. E se a posição do elemento for modificada durante a finalização, então, é necessário redefinir manualmente todas as coordenadas relativas a uma determinada área dentro dela para todos os controles que estão localizados nela. Por exemplo, grupos de outros controles estão localizados dentro da área do controle Tabs. E se precisássemos mover o controle Tabs para qualquer outra parte do formulário, todos os controles em cada guia individual permaneceriam em suas posições anteriores. Isso é muito inconveniente, mas agora o problema está resolvido.

Anteriormente, antes de criar um determinado controle em uma classe de aplicativo MQL personalizada, era necessário passar o objeto de formulário para armazenar o ponteiro no formulário usando o método CElement::WindowPointer(). Agora é utilizado o método CElement::MainPointer(). Como argumento, ele passa o objeto do controle, onde o controle criado deve ser anexado. 

class CElement : public CElementBase
  {
protected:
   //--- Ponteiro para o controle principal
   CElement         *m_main;
   //--- 
public:
   //--- Armazena e retorna o ponteiro para o controle principal
   CElement         *MainPointer(void)                               { return(::GetPointer(m_main));    }
   void              MainPointer(CElement &object)                   { m_main=::GetPointer(object);     }
  };

Como antes, você não pode criar um controle a menos que esteja conectado ao controle principal. O método principal para a criação dos controles (em todas as classes) possui uma verificação se o ponteiro para o controle principal está presente. Tal método para verificação foi renomeado e agora ele é chamado de CElement::CheckMainPointer(). O ponteiro do formulário e o ponteiro para o objeto do cursor do mouse são armazenados aqui. Ele também determina e armazena o identificador do controle, armazena o identificador do gráfico e o número da sub-janela, onde a aplicação MQL e sua interface gráfica estão conectadas. Anteriormente, esse código foi repetido de classe a classe.

class CElement : public CElementBase
  {
protected:
   //--- Verifica a presença do ponteiro no controle principal
   bool              CheckMainPointer(void);
  };
//+------------------------------------------------------------------+
//| Verifica a presença de ponteiro para o controle principal        |
//+------------------------------------------------------------------+
bool CElement::CheckMainPointer(void)
  {
//--- Se não houver ponteiro
   if(::CheckPointer(m_main)==POINTER_INVALID)
     {
      //--- Imprime a mensagem no diário do terminal
      ::Print(__FUNCTION__,
              " > Antes de criar um controle... \n...é necessário passar o ponteiro para o controle principal: "+
              ClassName()+"::MainPointer(CElementBase &object)");
      //--- Termina a construção da interface gráfica da aplicação
      return(false);
     }
//--- Armazena o ponteiro do formulário
   m_wnd=m_main.WindowPointer();
//--- Armazena o ponteiro do cursor do mouse
   m_mouse=m_main.MousePointer();
//--- Armazena as propriedades
   m_id       =m_wnd.LastId()+1;
   m_chart_id =m_wnd.ChartId();
   m_subwin   =m_wnd.SubwindowNumber();
//--- Envia a flag da presença do ponteiro
   return(true);
  }


Essa abordagem de anexar controles ao controle principal é distribuída em todas as classes. Os controles compostos complexos são constituídos de vários outros, e todos eles estão conectados uns aos outros em uma determinada sequência. Uma exceção é feita somente para três classes:

  • CTable — classe para a criação da tabela.
  • CListView — classe para a criação da lista.
  • CTextBox — classe para criação da caixa de texto multilinha.

Eles são montados a partir de múltiplos objetos gráficos do tipo OBJ_BITMAP_LABEL, cujo conteúdo, no entanto, também é renderizado. O teste de várias abordagens mostrou que o uso de múltiplos objetos é o melhor caminho para esta etapa do desenvolvimento da biblioteca.

A classe CButton tornou-se uma espécie de "tijolo" universal, onde ela é usada praticamente em todos os outros controles da biblioteca. Além disso, o controle do tipo CButton (Botão) é agora a base para certas outras classes de controles, como: 

  • CMenuItem — elemento de menu.
  • CTreeItem — lista hierárquica.

Como resultado, o esquema geral de relações da classe no formato "base-derivado" ("pai-filho") possui o seguinte aspecto:

Fig. 1. O esquema das relações de classe, como base-derivado (pai-filho).

Fig. 1. O esquema das relações de classe, como base-derivado (pai-filho).

A partir de agora, 33 (trinta e três) controles diferentes foram implementados na biblioteca. Abaixo está uma série de esquemas que mostram todos os controles da biblioteca em ordem alfabética. Os controles raiz são marcados em verde e numerados. Em seguida, vem os controles aninhados para toda a profundidade de aninhamento. Cada coluna é a próxima camada de controles aninhados para um controle em particular. O símbolo ‘[]’ marca os controles, cujas instâncias múltiplas estão presentes. Tal visão ajudará os leitores a compreenderem melhor e mais rapidamente o esquema POO da biblioteca.

Os esquemas dos seguintes controles são mostrados nas ilustrações abaixo:

1. CButton — botão.
2. CButtonsGroup — grupo de botões.
3. CCalendar — calendário.
4. CCheckBox — caixa de seleção.
5. CColorButton — botão para chamar o seletor de cores.
6. CColorPicker — seletor de cores.
7. CComboBox — botão para chamar a lista suspensa(combobox).
8. CContextMenu — menu contextual.

Fig. 2. Representação esquemática dos controles (parte 1). 

Fig. 2. Representação esquemática dos controles (parte 1).


9. CDropCalendar — botão para chamar o calendário suspenso.
10. CFileNavigator — navegador de arquivos.
11. CLineGraph — gráfico de linha.
12. CListView — lista.

Fig. 3. Representação esquemática dos controles (parte 2).

Fig. 3. Representação esquemática dos controles (parte 2).

13. CMenuBar — menu principal.
14. CMENUItem — elemento de menu.
15. CPicture — figura.
16. CPicturesSlider — slider de imagem.
17. CProgressBar — barra de progresso.
18. CScroll — Barra de rolagem.
19. CSeparateLine — linha de separação.
20. CSlider — controle deslizante numérico.
21. CSplitButton — botão de divisão.
22. CStandartChart — gráfico padrão.
23. CStatusBar — barra de status.
24. CTable — tabela.
 


Fig. 4. Representação esquemática dos controles (parte 3).

Fig. 4. Representação esquemática dos controles (parte 3).

25. CTabs — guias.
26. CTextBox — caixa de edição de texto com a opção de habilitar o modo multilinha.
27. CTextEdit — caixa de edição.
28. CTextLabel — rótulo de texto.
29. CTimeEdit — controle para o tempo de entrada.
30. CTooltip — dica de contexto.
31. CTreeItem — elemento da lista hierárquica.
32. CTreeView — lista hierárquica.
33. CWindow — formulário (janela) para os controles.
 

Fig. 5. Representação esquemática dos controles (parte 4).

Fig. 5. Representação esquemática dos controles (parte 4).


Array de controles aninhados

Ao criar os controles na versão anterior da biblioteca, suas classes base armazenaram os ponteiros para os objetos das primitivas gráficas. Agora, eles irão armazenar os ponteiros para os controles que são os componentes de um determinado controle da interface gráfica. 

São adicionados os ponteiros aos controles Para um array dinâmico do tipo CElement usando o método de acesso protegido CElement::AddToArray(). Este método foi projetado para uso interno e será usado somente nas classes de controles. 

class CElement : public CElementBase
  {
protected:
   //--- Ponteiros para controles aninhados
   CElement         *m_elements[];
   //---
protected:
   //--- Método para adicionar ponteiros a controles aninhados
   void              AddToArray(CElement &object);
  };
//+------------------------------------------------------------------+
//| Método para adicionar ponteiros a controles aninhados            |
//+------------------------------------------------------------------+
void CElement::AddToArray(CElement &object)
  {
   int size=ElementsTotal();
   ::ArrayResize(m_elements,size+1);
   m_elements[size]=::GetPointer(object);
  }

É possível recuperar um ponteiro de um controle do array no índice especificado. Isso é feito usando o método de acesso público CElement::Element(). Além disso, os métodos para recuperar o número de controles aninhados e a liberação do array estão disponíveis.

class CElement : public CElementBase
  {
public:
   //--- (1) Obtém o número de controles aninhados, (2) libera o array de controles aninhados
   int               ElementsTotal(void)                       const { return(::ArraySize(m_elements)); }
   void              FreeElementsArray(void)                         { ::ArrayFree(m_elements);         }
   //--- Retorna o ponteiro do controle aninhado no índice especificado
   CElement         *Element(const uint index);
  };
//+------------------------------------------------------------------+
//| Retorna o ponteiro do controle aninhado                          |
//+------------------------------------------------------------------+
CElement *CElement::Element(const uint index)
  {
   uint array_size=::ArraySize(m_elements);
//--- Verifica o tamanho do array de objetos
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > Este controle ("+m_class_name+") não possui controles aninhados!");
      return(NULL);
     }
//--- Ajuste no caso de haver estouro de buffer
   uint i=(index>=array_size)? array_size-1 : index;
//--- Retorna o ponteiro do objeto
   return(::GetPointer(m_elements[i]));
  }

Código base dos métodos virtuais

O código base dos métodos virtuais destinados a gerenciar os controles foi definido. Esses métodos incluem:

  • Moving() — movimento.
  • Show() — exibição.
  • Hide() — ocultamento.
  • Delete() — remoção.
  • SetZorders() — definição de prioridades.
  • ResetZorders() — redefinição de prioridades.

Agora, cada controle é desenhado em um objeto gráfico separado. Portanto, os métodos acima mencionados podem ser tornar universais, eliminando assim as repetições em todas as classes de controles. Há casos em que alguns desses métodos serão substituídos nas classes de determinados controles. Esta é a razão pela qual estes métodos foram declarados como virtuais. Por exemplo, tais controles incluem CListView, CTable e CTextBox. Já foi mencionado anteriormente que estes são os únicos controles componentes a partir de múltiplos objetos gráficos renderizados na versão atual da biblioteca. 

Considere o método CElement::Moving() como exemplo. Agora ele não tem argumentos. Anteriormente, eram passados a ele as coordenadas e o modo de operação. Mas agora não há necessidade disso — tudo se tornou muito mais simples. Isto é devido a modificações substanciais no núcleo da biblioteca (a classe CWndEvents), mas isso será discutido em mais detalhes em uma das seções seguintes do artigo. 

Um controle é movido em relação à posição atual do controle principal ao qual está anexado. Uma vez que o objeto gráfico é movido, seus controles aninhados (se houver) são movidos em um loop. A mesma cópia do método CElement::Moving() é chamada em quase todos os controles. E assim por diante em todos os níveis de aninhamento. Ou seja, se for necessário mover um determinado controle, basta chamar o método uma vez e os métodos semelhantes de outros controles serão chamados automaticamente (sequencialmente na ordem de sua criação). 

A lista abaixo mostra o código do método virtual CElement::Moving():

class CElement : public CElementBase
  {
public:
   //--- Mover o controle
   virtual void      Moving(void);
  };
//+------------------------------------------------------------------+
//| Movendo o controle                                               |
//+------------------------------------------------------------------+
void CElement::Moving(void)
  {
//--- Sai, se o controle está oculto
   if(!CElementBase::IsVisible())
      return;
//--- Se está ancorado à direita
   if(m_anchor_right_window_side)
     {
      //--- Armazenando coordenadas nos campos de controle
      CElementBase::X(m_main.X2()-XGap());
      //--- Armazena as coordenadas nos campos dos objetos
      m_canvas.X(m_main.X2()-m_canvas.XGap());
     }
   else
     {
      CElementBase::X(m_main.X()+XGap());
      m_canvas.X(m_main.X()+m_canvas.XGap());
     }
//--- Se está ancorado no canto inferior
   if(m_anchor_bottom_window_side)
     {
      CElementBase::Y(m_main.Y2()-YGap());
      m_canvas.Y(m_main.Y2()-m_canvas.YGap());
     }
   else
     {
      CElementBase::Y(m_main.Y()+YGap());
      m_canvas.Y(m_main.Y()+m_canvas.YGap());
     }
//--- Atualizando as coordenadas dos objetos gráficos
   m_canvas.X_Distance(m_canvas.X());
   m_canvas.Y_Distance(m_canvas.Y());
//--- Movendo os controles aninhados
   int elements_total=ElementsTotal();
   for(int i=0; i<elements_total; i++)
      m_elements[i].Moving();
  }

Determinação automática das prioridades do clique esquerdo do mouse

Na versão anterior da biblioteca, as prioridades para o clique esquerdo do mouse foram codificadas em classes de controle de cada biblioteca. Agora que cada controle é um único objeto gráfico, é possível implementar o modo automático de determinação de prioridades. Todo objeto gráfico que está em cima de qualquer outro objeto recebe uma prioridade mais alta:

Fig. 6. Uma representação visual da determinação das prioridades do clique esquerdo do mouse. 

Fig. 6. Uma representação visual da determinação das prioridades do clique esquerdo do mouse.

Mas há controles que não possuem objetos gráficos. Como mencionado anteriormente, existem controles que são montados a partir de múltiplos objetos renderizados. Em ambos os casos, a prioridade é ajustada na classe de cada controle particular. Primeiro, determine o controle onde esse ajuste é necessário.

Controles sem objetos gráficos:

  • CButtonsGroup — grupo de botões.
  • CDropCalendar — botão para chamar o calendário suspenso.
  • CSplitButton — botão de divisão.
  • CStandardChart — gráfico padrão.

Controles com múltiplos objetos gráficos:

  • CListView — lista.
  • CTable — tabela.
  • CTextBox — caixa de edição de texto.

Os seguintes métodos foram adicionados a classe CElement para definir e obter a prioridade:

class CElement : public CElementBase
  {
protected:
   //--- Prioridade do botão esquerdo do mouse
   long              m_zorder;
   //---
public:
   //--- Prioridade do botão esquerdo do mouse
   long              Z_Order(void)                             const { return(m_zorder);                }
   void              Z_Order(const long z_order);
  };
//+------------------------------------------------------------------+
//| Prioridade do clique esquerdo do mouse                           |
//+------------------------------------------------------------------+
void CElement::Z_Order(const long z_order)
  {
   m_zorder=z_order;
   SetZorders();
  }

Os controles que não possuem objetos gráficos são um tipo de módulos de controle onde outros controles estão aninhados. Não há nada para estabelecer prioridades em tal controle. Mas a prioridade dos controles aninhados é calculada em relação à prioridade do controle principal. Portanto, é necessário configurar os valores manualmente para que tudo funcione corretamente.

A prioridade para controles sem objetos é definida como a de seu controle principal:

...
//--- Prioridade como no controle principal, já que o controle não possui sua própria área para clique
   CElement::Z_Order(m_main.Z_Order());
...

Para todos os outros controles, a prioridade é definida após a criação do objeto de tela. Os formulários têm esse valor igual a zero, ele aumenta em 1 em relação ao controle principal para cada controle subsequentemente criado:

...
//--- Todos os controles, exceto o formulário, têm maior prioridade do que o controle principal
   Z_Order((dynamic_cast<CWindow*>(&this)!=NULL)? 0 : m_main.Z_Order()+1);
...

Como exemplo, considere mais um esquema. Imagine uma interface gráfica composta pelos seguintes controles (listados em ordem de criação):

  • Formulário para os controles (CWindow). O formulário pode ter botões (CButton) para (1) fechar o programa, (2) minimizar/maximizar o formulário e (3) dicas de contexto, etc. Outros botões podem ser adicionados nas futuras atualizações da biblioteca, estendendo os recursos desse controle.
  • Guias (CTabs). Além da área de trabalho, onde os grupos de controles estão localizados, esse controle contém um grupo de botões (CButton), que representam as guias.
  • Uma lista (CListView) com uma barra de rolagem (CScrollV), que tem botões de incremento e decremento (CButton), será colocada em uma das guias.
  • Outra guia contém uma caixa de texto multilinha (CTextBox) com barras de rolagem horizontal (CScrollH) e vertical (CScrollV).

Nenhuma ação é necessária para definir prioridades para os objetos de interface gráfica. Tudo será definido automaticamente de acordo com o esquema:

Fig. 7. Exemplo de definição das prioridades do clique esquerdo do mouse. 

Fig. 7. Exemplo de definição das prioridades do clique esquerdo do mouse.

O formulário recebe a menor prioridade com o valor igual a 0 (zero). Os botões no formulário possuem uma prioridade com o valor igual a 1

Cada botão componente do controle com o tipo CTabs (guias) recebe uma prioridade igual a 1, a área de trabalho das guias - recebe também uma prioridade igual a 1. Mas o controle CButtonsGroup terá um valor igual a 0, uma vez que ele não tem seu próprio objeto gráfico, ele é meramente um módulo de controle para botões do tipo CButton. Na classe personalizada do aplicativo MQL, use o método CElement::MainPointer() para especificar o controle principal (veja o código abaixo). Aqui, o formulário (CWindow) será o principal controle, ao qual o controle CTabs está anexado. O ponteiro deve ser armazenado antes de chamar o método para criar controles.

...
//--- Armazena o ponteiro para o controle principal
   m_tabs1.MainPointer(m_window);
...

A exibição da lista recebe uma prioridade igual a 2, como o principal controle aqui é a CTabs. Isso deve ser especificado antes do controle ser criado:

...
//--- Armazena o ponteiro para o controle principal
   m_listview1.MainPointer(m_tabs1);
...

Não há necessidade de especificar o controle principal para a barra de rolagem, já que isso já está implementado na classe de exibição de lista (CListView). O mesmo se aplica a todos os controles da biblioteca que são componentes de outros controles. Se o controle principal da barra de rolagem for a exibição da lista (CListView) com prioridade igual a 2, O valor é aumentado em um (3). E para os botões da barra de rolagem, que é o controle principal para eles, o valor será 4.

Tudo o que funciona na lista (CListView) também funciona no controle CTextBox.

Aplicação para testar os controles

Um aplicação MQL foi implementada para fins de teste. Sua interface gráfica contém todos os controles da biblioteca para permitir que você veja como tudo isso funciona. É assim que ele se parece: 

Fig. 12. Interface gráfica da aplicação MQL de teste.

Fig. 12. Interface gráfica da aplicação MQL de teste.

Este aplicativo de teste está anexado no fim do artigo para um estudo mais detalhado.


Conclusão

Esta versão da biblioteca tem diferenças significativas com a apresentada no artigo Interfaces gráficas X: Seleção de texto na caixa de texto multilinha (build 13). Muito trabalho foi feito, o que afetou quase todos os arquivos da biblioteca. Agora todos os controles da biblioteca são desenhados em objetos separados. A legibilidade do código melhorou, o seu volume diminuiu aproximadamente 30%, e seus recursos melhoraram. Uma série de erros e falhas relatados pelos usuários foram corrigidos.

Se você já começou a criar suas aplicações MQL usando a versão anterior da biblioteca, é recomendável que você primeiro faça o download da nova versão para uma cópia da instalação da plataforma MetaTrader 5 separada para estudar e testar completamente a biblioteca.

A biblioteca para a criação de interfaces gráficas no atual estágio de desenvolvimento se parece com o esquema abaixo. Esta não é a versão final; A biblioteca irá melhorar e evoluir no futuro.

Fig. 13. Estrutura da biblioteca, no atual estágio de desenvolvimento

Fig. 13. Estrutura da biblioteca, no atual estágio de desenvolvimento

Se você tiver dúvidas sobre a utilização do material a partir desses arquivos, você poderá consultar a descrição detalhada do desenvolvimento da biblioteca em um dos artigos da lista abaixo ou fazer sua pergunta nos comentários deste artigo.