English Русский 中文 Español Deutsch 日本語
Linguagem MQL como um meio de marcação da interface gráfica de programas MQL. Parte 1

Linguagem MQL como um meio de marcação da interface gráfica de programas MQL. Parte 1

MetaTrader 5Exemplos | 16 julho 2020, 10:10
1 789 0
Stanislav Korotky
Stanislav Korotky

Ainda não existe um consenso sobre a necessidade de usar uma interface gráfica de janela em programas MQL. Por um lado, todo trader profissional sonha com qual será maneira mais fácil de se comunicar com o robô de negociação, em outras palavras, ele quer um botão para ativar a negociação e começar a encaminhar a grana para ele magicamente. Por outro lado, trata-se de um sonho que está longe da realidade, pois geralmente exige uma escolha demorada e meticulosa do 'setup' correto antes que o sistema funcione, mas, mesmo assim, tal 'setup' precisa ser gerenciado e ajustado manualmente, se necessário. Já para não falar sobre os adeptos da negociação totalmente manual, que selecionando um painel de negociação intuitivo e conveniente têm metade do sucesso garantido. Em geral, pode-se afirmar que uma interface de janela, de uma forma ou de outra, é mais necessária do que não.

Introdução à abordagem de layout de GUI

Para criar uma interface gráfica, o MetaTrader fornece vários dos elementos de controle mais populares, na forma de objetos independentes colocados em gráficos e no invólucro de "controles" da biblioteca padrão, que, além disso, podem ser organizados numa única janela interativa. Quanto à criação de uma GUI, também existem várias soluções alternativas. No entanto, em todas essas bibliotecas, raramente é abordada a questão sobre a disposição de elementos, isto é, em certo sentido, sobre a automação do design da interface.

Claro, raramente há quem pense desenhar no gráfico uma janela cuja complexidade não seja inferior à do MetaTrader, mas um painel de negociação simples à primeira vista pode consistir em dezenas de "controles" cujo gerenciamento desde MQL pode se tornar uma rotina real.

O layout é uma maneira unificada de descrever a disposição e os atributos dos elementos da interface, com base nos quais são fornecidas a criação automática de janelas e sua relação com o código de controle.

Lembremos como é criada uma interface com base em modelos padrão MQL.

  bool CPanelDialog::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);
    // create dependent controls
    if(!CreateEdit()) return(false);
    if(!CreateButton1()) return(false);
    if(!CreateButton2()) return(false);
    if(!CreateButton3()) return(false);
    ...
    if(!CreateListView()) return(false);
    return(true);
  }
  
  bool CPanelDialog::CreateButton2(void)
  {
    // coordinates
    int x1 = ClientAreaWidth() - (INDENT_RIGHT + BUTTON_WIDTH);
    int y1 = INDENT_TOP + BUTTON_HEIGHT + CONTROLS_GAP_Y;
    int x2 = x1 + BUTTON_WIDTH;
    int y2 = y1 + BUTTON_HEIGHT;
  
    if(!m_button2.Create(m_chart_id, m_name + "Button2", m_subwin, x1, y1, x2, y2)) return(false);
    if(!m_button2.Text("Button2")) return(false);
    if(!Add(m_button2)) return(false);
    m_button2.Alignment(WND_ALIGN_RIGHT, 0, 0, INDENT_RIGHT, 0);
    return(true);
  }
  ...

Tudo é feito num estilo imperativo, com a ajuda de muitas chamadas do mesmo tipo. O código MQL é longo e ineficiente em termos de repetição para cada elemento, além disso, em cada caso, são usadas constantes próprias ("números mágicos", que são considerados uma fonte potencial de erros). Escrever esse código é uma tarefa ingrata, em particular, são recorrentes os erros da série Copy&Paste, além disso, se for necessário inserir um novo elemento e mover os antigos, provavelmente será preciso recontar e modificar manualmente inúmeros "números mágicos".

Eis uma descrição habitual dos elementos da interface na classe de caixa de diálogo.

  CEdit        m_edit;          // the display field object
  CButton      m_button1;       // the button object
  CButton      m_button2;       // the button object
  CButton      m_button3;       // the fixed button object
  CSpinEdit    m_spin_edit;     // the up-down object
  CDatePicker  m_date;          // the datepicker object
  CListView    m_list_view;     // the list object
  CComboBox    m_combo_box;     // the dropdown list object
  CRadioGroup  m_radio_group;   // the radio buttons group object
  CCheckGroup  m_check_group;   // the check box group object

Esta lista simples de "controles", além de muito grande, pode ser difícil de compreender e manter sem "dicas" visuais que o layout possa fornecer.

Em outras linguagens de programação, o design da interface geralmente é separado da programação. Para descrever o layout dos elementos, são usadas linguagens declarativas, por exemplo, XML ou JSON.

Em particular, na documentação ou nessas guias de estudo podem ser encontrados os princípios básicos da descrição de elementos de interface para projetos Android. Basta ter um entendimento básico de XML para compreender o essencial. Nesses arquivos é claramente exibida a hierarquia, são definidos os elementos-contêiner (LinearLayout, RelativeLayout) e os "controles" únicos (ImageView, TextView, CheckBox), nas configurações são definidas as propriedades para ajustar automaticamente o tamanho do conteúdo (match_parent, wrap_content), referências para descrição centralizada de estilos, são especificados opcionalmente os manipuladores de eventos, embora todos os elementos possam ser configurados adicionalmente e a eles possam ser anexados outros manipuladores de eventos a partir do código executável.

Se você se lembrar da plataforma .Net, ela usa uma descrição declarativa semelhante de interfaces com ajuda da linguagem XAML. Mesmo para quem nunca programou em C# e outras linguagens de infraestrutura de código gerenciado (cujo conceito, a propósito, é muito semelhante ao da plataforma MetaTrader e à sua linguagem MQL "gerenciada"), aqui também são óbvios os principais pontos, isto é, "controles", contêineres, propriedades, reação às ações do usuário "vêm todos numa mesma garrafa".

Para que separar o layout do código e descrevê-lo numa linguagem especial? Aqui estão as principais vantagens dessa abordagem.

  • visualização clara de relações hierárquicas entre elementos e contêineres;
  • agrupamento lógico;
  • tarefas de posicionamento e alinhamento unificadas;
  • registro simples de propriedades e valores;
  • declarações permitindo a geração automática de código que suporta o ciclo de vida e o gerenciamento de elementos (criação, configuração, experiência interativa, exclusão);
  • nível generalizado de abstração (propriedades gerais, estados, fases de inicialização e processamento), o que permite o desenvolvimento de uma GUI independentemente da codificação;
  • uso repetido (numeroso) de layouts (o mesmo fragmento pode ser incluído em várias caixas de diálogo várias vezes);
  • implementação/geração dinâmica de conteúdo em tempo real (semelhante à alternância de indicadores, cada um com seu próprio conjunto de elementos);
  • criação dinâmica de "controles" dentro do layout, salvando-os numa única matriz de ponteiros para a classe base (como CWnd no caso da biblioteca MQL padrão);
  • uso de um editor gráfico separado para o design de interface interativa, nesse caso, um formato especial de descrição de layout atua como um link entre a representação externa do programa e sua parte executiva na linguagem de programação;

Para o ambiente MQL, fizemos algumas tentativas para resolver alguns desses problemas. Em particular, o designer visual das caixas de diálogo é apresentado no artigo Como projetar e construir classes de objeto, ele funciona com base na biblioteca MasterWindows. Mas seus os métodos de layout e sua lista de tipos de elementos suportados são muito limitados.

Nos artigos Usando layouts e contêineres para controles de GUI: a classe cbox e classe cgrid é sugerido um sistema de layout mais avançado, mas sem um designer visual. Ele suporta todos os controles padrão e outros herdados de CWndObj ou CWndContainer, mas o usuário ainda deve lidar nele com tarefas rotineiras para criar e colocar componentes.

Conceitualmente, essa abordagem com contêineres é muito avançada (basta apontar sua popularidade em quase todas as linguagens de marcação) e, portanto, vamos adotá-la. Num dos meus artigos anteriores (Implementado OLAP na negociação (Parte 2): Visualizando resultados da análise interativa de dados multidimensionais), propus uma modificação dos contêineres CBox e CGrid, além de alguns controles para apoiar as propriedades de "borracha". Em seguida, usaremos esses desenvolvimentos e melhorá-los para resolver o problema do posicionamento automático de elementos usando os objetos da biblioteca padrão como exemplo.

Editor da GUI: prós e contras

A função principal do editor da GUI consiste em criar e configurar as propriedades dos elementos na janela em tempo real, de acordo com os comandos do usuário. Isso implica campos de entrada (para selecionar propriedades) que, para funcionarem, precisam conhecer a lista de propriedades e seus tipos para cada classe. Assim, cada "controle" deve ter duas versões relacionadas entre si: run-time (para trabalho regular) e design-time (para design de interface interativa). Os "controles" têm a versão inicial, isto é, a classe que funciona em janelas. A segunda versão é um wrapper de "controle", projetado para exibir e alterar suas propriedades. Seria muito trabalho escrever um wrapper para cada tipo de elemento. Por isso, seria bom automatizar esse processo. Teoricamente, para isso, podemos usar o analisador de sintaxe MQL descrito no artigo Análise sintática MQL via ferramentas MQL. Em muitas linguagens de programação, o conceito de propriedade (property) é colocado na sintaxe da linguagem e combina o "setter" e o "getter" de algum campo interno do objeto. Em MQL, isso ainda não existe, mas, em vez disso, nas classes de janela da biblioteca padrão é aplicado um princípio semelhante: alguns métodos "espelho" com o mesmo nome são usados para definir e ler o mesmo campo, em outras palavras, um assume um valor de um tipo específico e o outro o retorna. Por exemplo, eis como é definida a propriedade "somente leitura" do campo de entrada CEdit:

    bool ReadOnly(void) const;
    bool ReadOnly(const bool flag);

Assim é como se trabalha com o limite superior do CSpinEdit:

    int  MaxValue(void) const;
    void MaxValue(const int value);

Com ajuda do analisador MQL, podemos encontrar esses métodos em cada classe e, em seguida, criar uma lista geral levando em consideração a hierarquia de herança. Logo a seguir, é gerada uma classe-wrapper para definição interativa e leitura das propriedades encontradas. Precisamos fazer isso apenas uma vez para cada classe de "controles" (desde que a classe não altere suas propriedades públicas).

O projeto, embora possa ser realizado, é de grandes proporções. Antes de começar a desenvolvê-lo, é necessário considerar os prós e os contras.

O design têm dois propósitos fundamentais: o estabelecimento de uma subordinação hierárquica de elementos e de suas propriedades. Seria possível dispensar o uso de editor visual, se encontrarmos uma maneira de atender essas condições.

É de senso comum saber que todos os elementos têm propriedades padrão: tipo, tamanho, alinhamento, texto, estilo (cor). Também é possível definir propriedades específicas em código MQL, devido a que são operações única e, como regra, estão relacionadas à lógica de negociação. Quanto ao tipo, tamanho e alinhamento, eles são implicitamente definidos pela própria hierarquia dos objetos.

Assim, concluímos que na maioria dos casos, em vez de um editor completo, basta ter uma maneira conveniente de descrever a hierarquia dos elementos da interface.

Imagine que os controles e contêineres na classe da caixa de diálogo sejam descritos não com uma lista plana, mas, sim, com um recuo imitando uma árvore de aninhamento (subordinação).

    CBox m_main;                       // main client window
    
        CBox m_edit_row;                   // top level container/group
            CEdit m_edit;                      // control
      
        CBox m_button_row;                 // top level container/group
            CButton m_button1;                 // control
            CButton m_button2;                 // control
            CButton m_button3;                 // control
      
        CBox m_spin_date_row;              // top level container/group
            SpinEdit m_spin_edit;              // control
            CDatePicker m_date;                // control
      
        CBox m_lists_row;                  // top level container/group
      
            CBox m_lists_column1;              // nested container/group
                ComboBox m_combo_box;              // control
                CRadioGroup m_radio_group;         // control
                CCheckGroup m_check_group;         // control
        
            CBox m_lists_column2;              // nested container/group
                CListView m_list_view;             // control

Portanto, a estrutura do diálogo é muito melhor visível, mas alterar a formatação, é claro, não afeta a capacidade do programa de interpretar esses objetos de uma maneira especial.

Seria ideal ter uma maneira de descrição de interface que permita criar os controles de acordo com uma determinada hierarquia, encontrar o lugar certo na tela e calcular o tamanho apropriado.

Projetando uma linguagem de layout

Precisamos desenvolver uma linguagem de layout que descreva a estrutura geral da interface de janela e as propriedades de elementos individuais. Aqui, seria possível se apoiar no formato XML e reservar um conjunto de tags. Podíamos até tomá-los emprestado de outro ambiente de desenvolvimento, como os mencionados acima. Porém, precisaríamos analisar o XML e interpretá-lo em MQL, convertendo-o em ações para criar e configurar objetos. Além disso, como já não é imprescindível ter um editor visual, a linguagem de layout "externa" como meio de comunicação entre o editor e o tempo de execução também se torna desnecessária.

Surge então ideia de usar a própria MQL como linguagem de layout. E claro que se pode.

A hierarquia é inerente à linguagem MQL. Aqui pensamos imediatamente na ideia de classes herdadas. Porém, as classes descrevem uma hierarquia estática gerada antes da execução do código. E, em vez disso, precisamos de uma hierarquia que possa ser interpretada à medida que o código MQL é executado. Em outras linguagens de programação, para esse propósito (análise da hierarquia e estrutura interna das classes do próprio programa), existe uma ferramenta interna, isto é, as chamadas informações sobre os tipos de tempo de execução (run-time type information, RTTI, também conhecidas como "reflexões" (reflections). No entanto, em MQL não existem tais ferramentas.

No entanto, em MQL há outra hierarquia (como na maioria das linguagens de programação), isto é, uma hierarquia de contextos para a execução de fragmentos de código. Cada par de chaves numa função/método (ou seja, com exceção daqueles usados para descrever classes e estruturas) forma um contexto - o "escopo de existência" das variáveis locais. Como o nível de aninhamento de blocos não é limitado, podemos usá-los para descrever hierarquias arbitrárias.

Uma abordagem semelhante já foi usada em MQL, nomeadamente para a implementação de nosso próprio criador de perfil, um medidor de velocidade de execução de código (artigo em inglês MQL's OOP notes: Self-made profiler on static and automatic objects). Seu funcionamento é simples. Se no bloco do códigos, além das operações que executam a tarefa de aplicativo, declararmos uma variável local:

  {
    ProfilerObject obj;
    
    ... // code lines of your actual algorithm
  }

ela será criada imediatamente após entrar no bloco e destruída antes de sair dele. Isso se aplica a objetos de qualquer classe, incluindo aquela que pode levar esse comportamento em consideração. Em particular, no construtor e destruidor, é possível registrar o tempo dessas instruções e, assim, calcular a duração do algoritmo aplicado. Obviamente, para acumular essas medições, é necessário um objeto diferente e antigo, isto é, o próprio criador de perfil, além disso, a estrutura para troca de dados entre eles não é tão importante aqui (qualquer pessoa pode encontrar detalhes no blog). A linha inferior é aplicar o mesmo princípio para descrever layouts. Em outras palavras, ficará algo parecido com isto:

  container<Dialog> dialog(&this);
  {
    container<classA> main; // create classA internal object 1
    
    {
      container<classB> top_level(name, property, ...); // create classB internal object 2
      
      {
        container<classC> next_level_1(name, property, ...); // create classC internal object 3
        
        {
          control<classX> ctrl1(object4, name, property, ...); // create classX object 4
          control<classX> ctrl2(object5, name, property, ...); // create classX object 5
        } // register objects 4&5 in object 3 (via ctrl1, ctrl2 in next_level_1) 
      } // register object 3 in object 2 (via next_level_1 in top_level)
      
      {
        container<classC> next_level2(name, property, ...); // create classC internal object 6
        
        {
          control<classY> ctrl3(object7, name, property, ...); // create classY object 7
          control<classY> ctrl4(object8, name, property, ...); // create classY object 8
        } // register objects 7&8 in object 6 (via ctrl3, ctrl4 in next_level_2)
      } // register object 6 in object 2 (via next_level_2 in top_level)
    } // register object 2 in object 1 (via top_level in main)
  } // register object 1 (main) in the dialog (this)

À medida que esse código é executado, são criados os objetos de uma determinada classe (condicionalmente denominada "container") usando parâmetros padrão que definem a classe do elemento GUI que precisa ser gerado na caixa de diálogo. Todos os objetos-contêiner são colocados numa matriz especial usando o formato de pilha: cada nível consecutivo de aninhamento adiciona um contêiner à matriz, sendo que o bloco de contexto atual fica disponível na parte superior da pilha, enquanto na parte inferior (primeiro número) sempre fica uma janela. No momento do fechamento de cada bloco, todos os elementos filhos criados nele são automaticamente anexados ao pai imediato (fica logo no topo da pilha).

Toda essa "mágica" deve ser fornecida pela estrutura interna das classes "container" e "control". De fato, tratar-se-á da mesma classe "layout", mas para maior clareza, o esquema acima enfatiza a diferença entre contêineres e controles. Na realidade, a diferença é baseada apenas no tipo de classe indicada pelo parâmetro do modelo. Portanto, as classes Dialog, classA, classB, classC no exemplo acima devem ser contêiners de janela, ou seja, suportam o armazenamento de "controles".

É necessário distinguir entre objetos de layout auxiliares de curta duração (mencionados acima como main, top_level, next_level_1, ctrl1, ctrl2, next_level2, ctrl3, ctrl4) e os objetos das classes de interface gerenciadas por eles (object 1 ... object 8) que permanecerão anexados um ao outro e à janela. Todo esse código será executado como um método de caixa de diálogo (análogo ao método Create) e, portanto, o objeto de caixa de diálogo está disponível como this.

Passamos para alguns objetos de layout objetos da GUI como variáveis de classe (object 4, 5, 7, 8) e para outros não (são especificados o nome e as propriedades ). De qualquer forma, o objeto GUI deve existir, mas nem sempre precisamos dele explicitamente. Se o "controle" for usado para interação subsequente com o algoritmo, é conveniente ter uma referência para ele. Sendo que os contêineres geralmente não estão conectados à lógica do programa e executam apenas as funções de colocação de "controles"; portanto, sua criação ocorre implicitamente dentro do sistema de layout.

Mais tarde iremos desenvolver uma sintaxe específica para registrar as propriedades e sua lista.

Classes para o layout da interface: camada abstrata

Agora desenvolvamos as classes que permitirão a formação de uma hierarquia de elementos de interface. Potencialmente, essa abordagem deve ser aplicável a qualquer biblioteca de "controle", portanto, dividiremos o conjunto de classes em 2 partes: abstrata (com funcionalidade comum) e aplicada, relacionada aos recursos de uma biblioteca específica de controles padrão (classes descendentes CWnd). Testaremos a operacionalidade do conceito em diálogos padrão, e aqueles que desejarem poderão usá-lo para outras bibliotecas, guiados por uma camada abstrata.

O local central será ocupado pela classe LayoutData.

  class LayoutData
  {
    protected:
      static RubbArray<LayoutData *> stack;
      static string rootId;
      int _x1, _y1, _x2, _y2;
      string _id;
    
    public:
      LayoutData()
      {
        _x1 = _y1 = _x2 = _y2 = 0;
        _id = NULL;
      }
  };

Ela armazena a quantidade mínima de informações inerentes a qualquer elemento do layout: o nome exclusivo _id e coordenadas. Esclarecemos que esse campo _id é definido num nível abstrato e, em cada biblioteca GUI específica, ele pode "exibir-se" para sua própria propriedade de "controles". Em particular, na biblioteca padrão, este campo é chamado m_name e é acessível através do método público CWnd::Name. Dois objetos não podem ter o mesmo nome. CWnd também define o campo m_id do tipo long, ele é usado para despachar mensagens. Quando chegamos à implementação aplicada, não devemos confundi-lo com o nosso _id.

Além disso, a classe LayoutData fornece armazenamento estático de suas próprias instâncias na forma de uma pilha (stack) e um identificador de instância da janela (rootId). O caraterística estática dos dois últimos membros não é um problema, porque cada programa MQL é executado num único fluxo e, mesmo que possua várias janelas, apenas um deles pode ser criado a cada momento. Quando uma janela é desenhada, a pilha já fica vazia e está pronta para trabalhar com outra janela. O identificador da janela rootId é conhecido para biblioteca padrão como o campo m_instance_id na classe CAppDialog. Para outras bibliotecas, também deve existir algo semelhante (não necessariamente uma string, mas algo único, redutível a uma sequência), porque, caso contrário, as janelas podem entrar em conflito entre si. Vamos abordar esse problema.

O descendente da classe LayoutData será um LayoutBase tipado. Este é o protótipo da mesma classe de layout que gera elementos de interface segundo o código MQL com blocos de colchetes como instruções.

  template<typename P,typename C>
  class LayoutBase: public LayoutData
  {
    ...

Seus dois parâmetros de modelo P e C correspondem às classes de elementos que servem como contêineres e "controles".

Os contêineres, por definição, contêm "controles" e/ou outros, enquanto os "controles" são percebidos como um todo e não podem conter nada em si. Deve-se enfatizar aqui que "controle" se refere a uma unidade logicamente indivisível da interface, que por dentro pode realmente consistir em muitos objetos auxiliares. Em particular, as classes CListView ou CComboBox da biblioteca padrão são "controles", mas internamente são implementadas usando vários objetos. Esses já são detalhes da implementação. Em outras bibliotecas, podem ser implementados tipos semelhantes de controles como uma única tela na qual são desenhados botões e texto. No contexto das classes de layout abstratas, não devemos nos aprofundar nisso, violando os princípios do encapsulamento, mas é claro que uma implementação aplicada projetada para uma biblioteca específica terá que levar em conta essa nuance (e distinguir contêineres reais de “controles” complexos).

Para a biblioteca padrão, os melhores candidatos para os parâmetros de modelo P e C são CWndContainer e CWnd. Olhando um pouco adiante, não podemos usar CWndObj como uma classe de controles, porque muitos controles são herdados do CWndContainer. Isso inclui, por exemplo, CComboBox, CListView, CSpinEdit, CDatePicker e outros. No entanto, como o parâmetro C deve ser selecionada a classe comum mais próxima de todos os "controles", e para a biblioteca padrão CWnd é tal. Como podemos ver, uma classe de contêineres (como CWndContainer) pode, na prática, “se sobrepor” a elementos simples e, portanto, precisaremos fornecer uma verificação mais precisa sobre se uma instância específica é um contêiner ou não. Da mesma forma, como parâmetro P deve ser selecionada a classe comum mais próxima de todos os contêineres. Na biblioteca padrão, a classe window é CDialog, o descendente de CWndContainer, mas, além disso, vamos usar as classes de ramificação CBox para agrupar elementos dentro de caixas de diálogo; CWndContainer vem de CWndClient, que por sua vez vem de CWndContainer. Assim, o ancestral comum mais próximo é o CWndContainer.

Os campos da classe LayoutBase armazenam ponteiros para o elemento de interface gerado pelo objeto de layout.

    protected:
      P *container; // not null if container (can be used as flag)
      C *object;
      C *array[];
    public:
      LayoutBase(): container(NULL), object(NULL) {}

Aqui, container e object apontam para a mesma coisa, no entanto container não é NULL apenas se o elemento é realmente um contêiner.

A matriz array permite usar um único objeto de layout para criar imediatamente um grupo de elementos do mesmo tipo, por exemplo, botões. Nesse caso, os ponteiros de container e object serão NULL. Existem métodos getter triviais para todos os membros; não forneceremos todos. Por exemplo, uma referência a object é facilmente obtida usando o método get().

Os três métodos a seguir declaram operações abstratas no elemento anexado, que devem poder executar o objeto de layout.

    protected:
      virtual bool setContainer(C *control) = 0;
      virtual string create(C *object, const string id = NULL) = 0;
      virtual void add(C *object) = 0;

O método setContainer permite distinguir o contêiner do "controle" usual no parâmetro passado. É nesse método que o campo do container deve ser preenchido e, se não for NULL, será retornado true.

O método create inicializa o elemento (na biblioteca padrão em todas as classes, existe um método Create semelhante, mas, tanto quanto eu posso dizer em outras bibliotecas, por exemplo, EasyAndFastGUI, são definidos métodos similares; embora, no caso do EasyAndFastGUI, eles sejam chamados por alguma razão de maneira diferente em classes diferentes, portanto, aqueles que desejam anexar o mecanismo de layout descrito a ele terão que escrever classes de adaptadores que unificam a interface de programação de diferentes tipos de "controles"; mas isso não é tudo, pois é muito mais importante escrever para EasyAndFastGUI classes semelhantes ao CBox e CGrid). Podemos passar o identificador do elemento desejado para o método, mas isso não garante que o algoritmo leve esse desejo em consideração, no todo ou em parte (em particular, pode ser adicionado instance_id) e, portanto, o identificador real pode ser encontrado a partir da string retornada.

O método add adiciona um elemento ao elemento do contêiner pai (na biblioteca padrão, essa operação é executada pelo método Add, no EasyAndFastGUI, aparentemente, MainPointer).

Agora vamos ver como esses três métodos estão envolvidos num nível abstrato. Cada elemento de interface está vinculado a um objeto de layout e passa por duas fases: criação (no momento da inicialização da variável local no bloco de código) e exclusão (no momento de sair do bloco de código e chamar o destruidor da variável local). Para a primeira fase, escrevemos o método init, que será chamado do construtor das classes derivadas.

      template<typename T>
      void init(T *ref, const string id = NULL, const int x1 = 0, const int y1 = 0, const int x2 = 0, const int y2 = 0)
      {
        object = ref;
        setContainer(ref);
        
        _x1 = x1;
        _y1 = y1;
        _x2 = x2;
        _y2 = y2;
        if(stack.size() > 0)
        {
          if(_x1 == 0 && _y1 == 0 && _x2 == 0 && _y2 == 0)
          {
            _x1 = stack.top()._x1;
            _y1 = stack.top()._y1;
            _x2 = stack.top()._x2;
            _y2 = stack.top()._y2;
          }
          
          _id = rootId + (id == NULL ? typename(T) + StringFormat("%d", object) : id);
        }
        else
        {
          _id = (id == NULL ? typename(T) + StringFormat("%d", object) : id);
        }
        
        string newId = create(object, _id);
        
        if(stack.size() == 0)
        {
          rootId = newId;
        }
        if(container)
        {
          stack << &this;
        }
      }

O primeiro parâmetro é um ponteiro para um elemento da classe correspondente. Aqui nos restringimos a considerar o caso em que um elemento é fornecido de fora. Porém, no esboço da sintaxe de layout proposta acima, também encontramos elementos implícitos (apenas foram especificado seus nomes). Voltaremos a esse esquema de trabalho um pouco mais tarde.

O método armazena um ponteiro para um elemento em object, verifica com setContainer se é um contêiner (supondo que, nesse caso, o campo container também seja preenchido), pega as coordenadas especificadas nos parâmetros de entrada ou, opcionalmente, no contêiner pai, se ele já existir na pilha. A chamada de create inicializa o elemento da interface. Se a pilha ainda estiver vazia, salvamos o identificador em rootId (no caso da biblioteca padrão, será instance_id), pois o primeiro elemento da pilha sempre será o contêiner mais importante, isto é, a janela responsável por todos os elementos subordinados (na biblioteca padrão é a classe CDialog ou derivada). Finalmente, se o item atual for um container, vamos colocá-lo na pilha (stack << &this).

O método init é um modelo. Isso permite que geraremos automaticamente os nomes de "controles" por tipo, mas além disso, em breve adicionaremos outros métodos init semelhantes. Um deles gerará elementos dentro, e não receberá elementos externos prontos, nesse caso é necessário um tipo específico. A outra opção init é projetada para registrar vários elementos do mesmo tipo no layout de uma só vez (lembremo-nos do membro array[]), e as matrizes são transmitidas por referências, e as referências não suportam a conversão de tipos ("parameter conversion not allowed", "no one of the overloads can be applied to the function call", dependendo da estrutura do código), que novamente exige a especificação de um tipo específico através do parâmetro do modelo. Assim, todos os métodos init terão o mesmo contrato de "modelo" (regras de uso).

O mais interessante acontece no destruidor LayoutBase.

      ~LayoutBase()
      {
        if(container)
        {
          stack.pop();
        }
        
        if(object)
        {
          LayoutBase *up = stack.size() > 0 ? stack.top() : NULL;
          if(up != NULL)
          {
            up.add(object);
          }
        }
      }
  };

Se o elemento associado atual for um contêiner, vamos exclui-lo da pilha, pois saímos do bloco de colchetes correspondente (contêiner termina). Acontece que, dentro de cada bloco, é o topo da pilha que contém o contêiner de maior aninhamento, onde são adicionados elementos (de fato, "controles" e outros contêineres menores) encontrados dentro do bloco. Em seguida, o elemento atual é adicionado usando o método add ao contêiner, que por sua vez está no topo da pilha.

Classes para o layout da interface: camada de aplicativo para elementos da biblioteca padrão

Vamos para coisas mais específicas, como implementações de classe para o layout dos elementos de interface da biblioteca padrão. Usando as classes CWndContainer e CWnd como parâmetros do modelo, definimos a classe intermediária StdLayoutBase.

  class StdLayoutBase: public LayoutBase<CWndContainer,CWnd>
  {
    public:
      virtual bool setContainer(CWnd *control) override
      {
        CDialog *dialog = dynamic_cast<CDialog *>(control);
        CBox *box = dynamic_cast<CBox *>(control);
        if(dialog != NULL)
        {
          container = dialog;
        }
        else if(box != NULL)
        {
          container = box;
        }
        return true;
      }

O método setContainer determina por meio de conversão de tipo dinâmico se o elemento CWnd é o sucessor do CDialog ou CBox e, se for, será um contêiner.

      virtual string create(CWnd *child, const string id = NULL) override
      {
        child.Create(ChartID(), id != NULL ? id : _id, 0, _x1, _y1, _x2, _y2);
        return child.Name();
      }

O método create inicializa o elemento e retorna seu nome. Observe que o trabalho é realizado apenas com o gráfico atual (ChartID()), e somente na janela principal (as sub-janelas na estrutura deste projeto não foram consideradas, mas aqueles que desejarem podem adaptar o código às suas necessidades).

      virtual void add(CWnd *child) override
      {
        CDialog *dlg = dynamic_cast<CDialog *>(container);
        if(dlg != NULL)
        {
          dlg.Add(child);
        }
        else
        {
          CWndContainer *ptr = dynamic_cast<CWndContainer *>(container);
          if(ptr != NULL)
          {
            ptr.Add(child);
          }
          else
          {
            Print("Can't add ", child.Name(), " to ", container.Name());
          }
        }
      }
  };

O método "add" adiciona um elemento filho ao pai, fazendo preliminarmente o máximo de "upcasting" possível, pois o método Add na biblioteca padrão não é virtual (tecnicamente, poderíamos fazer a alteração em questão na biblioteca padrão, mas falaremos sobre alterá-la mais tarde).

Com base na classe StdLayoutBase, criamos a classe de trabalho _layout, que aparecerá no código com a descrição do layout em MQL. O nome começa com um sublinhado para prestar atenção ao propósito não-padrão dos objetos desta classe. Consideremos uma versão simplificada da classe por enquanto. Mais tarde, adicionaremos funcionalidades auxiliares a ele. Todo o trabalho é realmente iniciado pelos designers, dentro dos quais é chamado o método init desde LayoutBase.

  template<typename T>
  class _layout: public StdLayoutBase
  {
    public:
      
      _layout(T &ref, const string id, const int dx, const int dy)
      {
        init(&ref, id, 0, 0, dx, dy);
      }
      
      _layout(T *ptr, const string id, const int dx, const int dy)
      {
        init(ptr, id, 0, 0, dx, dy);
      }
      
      _layout(T &ref, const string id, const int x1, const int y1, const int x2, const int y2)
      {
        init(&ref, id, x1, y1, x2, y2);
      }
      
      _layout(T *ptr, const string id, const int x1, const int y1, const int x2, const int y2)
      {
        init(ptr, id, x1, y1, x2, y2);
      }
      
      _layout(T &refs[], const string id, const int x1, const int y1, const int x2, const int y2)
      {
        init(refs, id, x1, y1, x2, y2);
      }
  };

Podemos ver uma imagem geral usando o seguinte diagrama de classes. Ela tem algo mais a mostrar, embora a maioria das classes seja conhecida.

Diagrama de classes de layout da GUI

Diagrama de classes de layout da GUI

Agora já poderíamos verificar na prática como a descrição do objeto, por exemplo, _layout<CButton> button (m_button, 100, 20) inicializa e registra o objeto m_button na caixa de diálogo, desde que seja descrito num bloco externo assim: _layout<CAppDialog> dialog(this, name, x1, y1, x2, y2). No entanto, os elementos têm muitas outras propriedades além de suas dimensões. Algumas propriedades, como alinhamento, não são menos importantes para o layout do que as coordenadas. De fato, se um elemento tiver alinhamento horizontal (em termos de alinhamento da biblioteca padrão), ele será estendido para toda a largura do contêiner pai, menos os campos especificados à esquerda e à direita. Assim, o alinhamento tem precedência sobre as coordenadas. Além disso, em contêineres da classe CBox, é importante a orientação (direção) na qual os elementos filhos são dispostos - horizontal (por padrão) ou vertical. Também seria correto oferecer suporte a outras propriedades que afetam a aparência externa (como tamanho da fonte, cor) e o modo de operação (por exemplo, somente leitura, botões fixos etc.).

Nos casos em que o objeto GUI é descrito na classe window e passado para o layout, poderíamos usar métodos "nativos" para definir propriedades (por exemplo, edit.Text("text")). O sistema de layout suporta esse modo antigo, mas não é o único e o ideal. Em muitos casos, é conveniente confiar a criação de objetos ao sistema de layout, para eles não estarem disponíveis diretamente na janela. Portanto, é necessário expandir de alguma forma os recursos da classe _layout para configurar elementos.

Como existem muitas propriedades, é aconselhável não levar o trabalho com elas para a mesma classe, em vez disso, é bom compartilhar a responsabilidade entre elas e uma classe auxiliar especial. Ao mesmo tempo, _layout ainda será um "ponto de entrada" para registrar elementos, mas delegará todas as nuances das configurações para uma nova classe. Isso é ainda mais importante para tornar a tecnologia de layout o mais independente possível de uma determinada biblioteca de controles.

Classes para definir propriedades de elementos

No nível abstrato, um conjunto de propriedades é subdividido por tipo de valor. Daremos suporte aos tipos internos básicos da linguagem MQL, bem como a alguns outros, que serão discutidos mais adiante. Sintaticamente, seria conveniente atribuir propriedades por meio de uma cadeia de chamadas do padrão "construtor" (builder):

  _layout<CBox> column(...);
  column.style(LAYOUT_STYLE_VERTICAL).color(clrGray).margin(5);

No entanto, essa sintaxe implica um conjunto muito longo de métodos numa classe, e essa classe deve ser uma classe de layout, porque o operador de desreferência (ponto) não pode ser redefinido. Pode-se reservar um método na classe _layout para retornar uma instância do objeto auxiliar para as propriedades, algo assim:

  _layout<CBox> column(...);
  column.properties().style(LAYOUT_STYLE_VERTICAL).color(clrGray).margin(5);

Porém, nesse caso, seria apropriado definir muitas classes intermediárias, cada uma para seu próprio tipo de elementos, a fim de verificar as propriedades atribuídas no estágio de compilação. Isso complicaria o projeto, mas para a primeira implementação de teste, eu gostaria de fazer tudo da maneira mais simples possível. Na verdade, deixaremos essa abordagem para o futuro.

Deve-se notar também que os nomes dos métodos no modelo de "construção" são, de certo modo, supérfluos, já que valores como LAYOUT_STYLE_VERTICAL ou clrGray "falam por si" e outros tipos geralmente não precisam de detalhes, portanto, para um "controle" CEdit, um valor do tipo bool como uma regra significa um sinalizador "somente leitura" e, para CButton, um sinal de "aderência". Como resultado, a solução implora simplesmente atribuir valores usando algum operador sobrecarregado. Mas o operador de atribuição, por incrível que pareça, não funcionará para nós, porque não permite encadear uma cadeia de chamadas.

  _layout<CBox> column(...);
  column = LAYOUT_STYLE_VERTICAL = clrGray = 5; // 'clrGray' - l-value required ...

Operadores de atribuição de linha única são executados da direita para a esquerda, ou seja, não do objeto em que é inserida a atribuição sobrecarregada. Funcionaria assim:

  ((column = LAYOUT_STYLE_VERTICAL) = clrGray) = 5; 

Mas não tem bom aspeto.

Opção:

  column = LAYOUT_STYLE_VERTICAL; // orientation
  column = clrGray;               // color
  column = 5;                     // margin

também demasiado longa. Portanto, foi decidido sobrecarregar o operador <= e usar algo como isto:

  column <= LAYOUT_STYLE_VERTICAL <= clrGray <= 5.0;

Para isso na classe LayoutBase existe um stub:

    template<typename V>
    LayoutBase<P,C> *operator<=(const V value) // template function cannot be virtual
    {
      Print("Please, override " , __FUNCSIG__, " in your concrete Layout class");
      return &this;
    }

Seu objetivo duplo é declarar a intenção de usar a sobrecarga do operador e lembrá-lo de substituir um método numa classe derivada. Lá, em teoria, deve ser usado um objeto de uma classe intermediária com a seguinte interface (não mostrado completamente).

  template<typename T>
  class ControlProperties
  {
    protected:
      T *object;
      string context;
      
    public:
      ControlProperties(): object(NULL), context(NULL) {}
      ControlProperties(T *ptr): object(ptr), context(NULL) {}
      void assign(T *ptr) { object = ptr; }
      T *get(void) { return object; }
      virtual ControlProperties<T> *operator[](const string property) { context = property; StringToLower(context); return &this; };
      virtual T *operator<=(const bool b) = 0;
      virtual T *operator<=(const ENUM_ALIGN_MODE align) = 0;
      virtual T *operator<=(const color c) = 0;
      virtual T *operator<=(const string s) = 0;
      virtual T *operator<=(const int i) = 0;
      virtual T *operator<=(const long l) = 0;
      virtual T *operator<=(const double d) = 0;
      virtual T *operator<=(const float f) = 0;
      virtual T *operator<=(const datetime d) = 0;
  };

Como podemos ver, na classe intermediária é armazenada uma referência ao elemento (object) a ser configurado. A ligação é feita no construtor ou usando o método assign. Se assumirmos que escrevemos algum mediador específico da classe MyControlProperties:

  template<typename T>
  class MyControlProperties: public ControlProperties<T>
  {
    ...
  };

na classe _layout podemos usar seu objeto de acordo com este esquema (linhas e métodos adicionados são marcados com comentários):

  template<typename T>
  class _layout: public StdLayoutBase
  {
    protected:
      C *object;
      C *array[];
      
      MyControlProperties helper;                                          // +
      
    public:
      ...
      _layout(T *ptr, const string id, const int dx, const int dy)
      {
        init(ptr, id, 0, 0, dx, dy); // this will save ptr in the 'object'
        helper.assign(ptr);                                                // +
      }
      ...
      
      // non-virtual function override                                     // +
      template<typename V>                                                 // +
      _layout<T> *operator<=(const V value)                                // +
      {
        if(object != NULL)
        {
          helper <= value;
        }
        else
        {
          for(int i = 0; i < ArraySize(array); i++)
          {
            helper.assign(array[i]);
            helper <= value;
          }
        }
        return &this;
      }

Devido ao fato de o operador <= em _layout ser um modelo, ele gerará automaticamente uma chamada para o tipo correto de parâmetro da interface ControlProperties (é claro, não estamos falando de métodos de interface abstrata, mas de suas implementações na classe derivada MyControlProperties; escreveremos isso para uma janela específica em breve bibliotecas).

Em alguns casos, o mesmo tipo de dados é usado para definir diferentes propriedades. Por exemplo, o mesmo bool é usado em CWnd ao definir os sinalizadores de visibilidade e atividade de elementos, além dos modos 'somente leitura' (para CEdit) e "fixos" (para CButton), mencionados acima. Para especificar explicitamente o nome da propriedade, na interface ControlProperties é fornecido o operador [] com um parâmetro do tipo string. Ele define o campo context, com base no qual a classe derivada pode alterar a característica necessária.

Para cada combinação de tipo de dados de entrada e classe de elemento, uma das propriedades (a mais usada) será considerada a propriedade padrão (exemplos para CEdit e CButton são fornecidos acima). Outras propriedades exigirão contexto.

Por exemplo, para um botão CButton, seria assim:

  button1 <= true;
  button2["visible"] <= false;

A linha de contexto não é especificada na primeira linha e, portanto, a propriedade é chamada "locking" (botão ativar/desativar). No segundo, o botão é criado inicialmente invisível (o que raramente é necessário).

Vamos considerar as principais nuances da implementação do mediador StdControlProperties para a biblioteca de elementos padrão. O código completo pode ser encontrado nos arquivos anexados. No início, você pode ver como o operador <= é redefinido para o tipo bool.

  template<typename T>
  class StdControlProperties: public ControlProperties<T>
  {
    public:
      StdControlProperties(): ControlProperties() {}
      StdControlProperties(T *ptr): ControlProperties(ptr) {}
      
      // we need dynamic_cast throughout below, because control classes
      // in the standard library does not provide a set of common virtual methods
      // to assign specific properties for all of them (for example, readonly
      // is available for edit field only)
      virtual T *operator<=(const bool b) override
      {
        if(StringFind(context, "enable") > -1)
        {
          if(b) object.Enable();
          else  object.Disable();
        }
        else
        if(StringFind(context, "visible") > -1)
        {
          object.Visible(b);
        }
        else
        {
          CEdit *edit = dynamic_cast<CEdit *>(object);
          if(edit != NULL) edit.ReadOnly(b);
          
          CButton *button = dynamic_cast<CButton *>(object);
          if(button != NULL) button.Locking(b);
        }
        
        return object;
      }

A regra a seguir se aplica às seqüências de caracteres: qualquer texto cai no cabeçalho do controle, a menos que o contexto esteja definido como "fonte", o que significa o nome da fonte:

      virtual T *operator<=(const string s) override
      {
        CWndObj *ctrl = dynamic_cast<CWndObj *>(object);
        if(ctrl != NULL)
        {
          if(StringFind(context, "font") > -1)
          {
            ctrl.Font(s);
          }
          else // default
          {
            ctrl.Text(s);
          }
        }
        return object;
      }

A classe StdControlProperties também introduz substituições <= para tipos específicos apenas da biblioteca padrão. Em particular, ele pode aceitar a enumeração ENUM_WND_ALIGN_FLAGS, que descreve a opção de alinhamento. Observe que nesta lista, além dos quatro lados (esquerdo, direito, superior, inferior), nem todos são descritos, mas apenas as combinações usadas com mais frequência, como alinhamento de largura (WND_ALIGN_WIDTH = WND_ALIGN_LEFT|WND_ALIGN_RIGHT) ou toda a área do cliente (WND_ALIGN_CLIENT = WND_ALIGN_WIDTH|WND_ALIGN_HEIGHT). No entanto, se for necessário alinhar o elemento pela largura e pela borda superior, essa combinação de sinalizadores não fará parte da enumeração. Por isso, você precisará especificar explicitamente um fantasma do tipo para ele ((ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_WIDTH|WND_ALIGN_TOP)). Caso contrário, a operação "OR" bit a bit fornecerá um tipo int, e será chamada a sobrecarga incorreta da configuração das propriedades do número inteiro. Uma alternativa é indicar o contexto "align".

O mais "cheio de trabalho" é a substituição para o tipo int. Aqui, em particular, podem ser definidas largura, altura, margens, tamanho da fonte e outras propriedades. Para facilitar essa situação, é possível especificar tamanhos imediatamente no construtor do objeto de layout e, alternativamente, os campos podem ser definidos usando números do tipo double ou uma união PackedRect especial. Obviamente, também foi adicionada uma sobrecarga do operador, sendo conveniente usá-la nos casos em que são necessários campos assimétricos:

  button <= PackedRect(5, 100, 5, 100); // left, top, right, bottom

pois campos iguais em todos os lados são mais fáceis de definir com um único valor do tipo double:

  button <= 5.0;

No entanto, existe uma alternativa, isto é, o contexto "margin", nesse caso, double não é necessário, e a entrada equivalente será:

  button["margin"] <= 5;

Quanto às margens e recuos, é necessário prestar atenção a uma nuance. Na biblioteca padrão, existe o termo alinhamento (alignment), que inclui margens (margins) adicionadas automaticamente ao redor do "controle". Ao mesmo tempo, as classes da família CBox têm seu próprio mecanismo de recuo (padding), que é uma lacuna dentro de um elemento-contêiner entre a borda externa e os "controles" filhos (conteúdo). Assim, os campos do ponto de vista dos “controles” e o recuo do ponto de vista dos contêineres significam essencialmente a mesma coisa. E, como, infelizmente, não se consideram os dois algoritmos de posicionamento, o uso simultâneo de margens e recuo pode criar problemas (a mais óbvia delas é o recuo de elementos que não atende às expectativas). A recomendação geral é deixar o recuo igual a zero e manipular as margens. No entanto, de acordo com as circunstâncias, podemos tentar incluir recuo, especialmente se estamos falando de um contêiner específico, em vez de configurações gerais.

Este artigo é um estudo de "proof of concept, POC" e não oferece uma solução pronta. Sua tarefa é testar a tecnologia proposta nas classes e contêineres padrão da biblioteca disponíveis no momento da escrita deste artigo, com alterações mínimas em todos esses componentes. Idealmente, os contêineres (não necessariamente o CBox) devem ser escritos como parte integrante da biblioteca de elementos da GUI e agir com todas as combinações de modos possíveis.

A seguir, é apresentada uma tabela de propriedades e elementos suportados. A classe CWnd significa que as propriedades são aplicáveis a todos os elementos, a classe CWndObj, para "controles" simples (dois deles, CEdit e CButton, também estão listados na tabela). A classe CWndClient generaliza os "controles" opostos (CCheckGroup, CRadioGroup, CListView), e também é o pai dos contêineres CBox/CGrid.

Tabela de propriedades suportadas por tipo de dados e classes de elementos

type/control CWnd CWndObj CWndClient CEdit CButton CSpinEdit CDatePicker CBox/CGrid
bool visible
enable
visible
enable
visible
enable
(readonly)
visible
enable
(locking)
visible
enable
visible
enable
visible
enable
visible
enable
color (text)
background
border
(background)
border
(text)
background
border
(text)
background
border
(background)
border
string (text)
font
(text)
font
(text)
font
int width
height
margin
left
top
right
bottom
align
width
height
margin
left
top
right
bottom
align
fontsize
width
height
margin
left
top
right
bottom
align
width
height
margin
left
top
right
bottom
align
fontsize
width
height
margin
left
top
right
bottom
align
fontsize
(value)
width
height
margin
left
top
right
bottom
align
min
max
width
height
margin
left
top
right
bottom
align
width
height
margin
left
top
right
bottom
align
long (id) (id)
zorder
(id) (id)
zorder
(id)
zorder
(id) (id) (id)
double (margin) (margin) (margin) (margin) (margin) (margin) (margin) (margin)
float (padding)
left *
top *
right *
bottom *
datetime (value)
PackedRect (margin[4]) (margin[4]) (margin[4]) (margin[4]) (margin[4]) (margin[4]) (margin[4]) (margin[4])
ENUM_ALIGN_MODE (text align)
ENUM_WND_ALIGN_FLAGS (alignment) (alignment) (alignment) (alignment) (alignment) (alignment) (alignment) (alignment)
LAYOUT_STYLE (style)
VERTICAL_ALIGN (vertical align)
HORIZONTAL_ALIGN (horizonal align)


Ao artigo é anexado o código fonte completo da classe StdControlProperties, que fornece a conversão das propriedades dos elementos de layout em chamadas para os métodos da biblioteca de componentes padrão.

Vejamos as classes de layout na prática. Finalmente, podemos começar a estudar exemplos, passando do simples para o complexo. Como com os dois artigos de origem que falam sobre o layout da GUI usando contêineres, adaptamos o jogo da racha-cuca (SlidingPuzzle4) e a demonstração padrão para trabalhar com controles (ControlsDialog4). Os índices correspondem às etapas de atualização desses projetos. Nesse artigo os mesmos programas são apresentados com os índices 3. Aqueles que desejem podem comparar os códigos-fonte. Os exemplos estão localizados na pasta MQL5/Experts/Examples/Layouts/.

Exemplo 1. Racha-cuca SlidingPuzzle

A única alteração significativa na interface pública do formulário principal CSlidingPuzzleDialog é a aparência do novo método CreateLayout. Ele deve ser chamado a partir do manipulador OnInit em vez do habitual Create. A lista de parâmetros para os dois métodos é a mesma. Essa substituição foi necessária porque o próprio diálogo é um objeto do layout (do nível mais externo) e seu método Create será chamado automaticamente pela nova estrutura (isso é feito pelo método StdLayoutBase::create, que examinamos anteriormente). Todas as informações da estrutura sobre o formulário e seu conteúdo são definidas no método CreateLayout usando a linguagem de marcação baseada em MQL. Eis o método:

  bool CSlidingPuzzleDialog::CreateLayout(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
  {
    {
      _layout<CSlidingPuzzleDialog> dialog(this, name, x1, y1, x2, y2);
      {
        _layout<CGridTkEx> clientArea(m_main, NULL, 0, 0, ClientAreaWidth(), ClientAreaHeight());
        {
          SimpleSequenceGenerator<long> IDs;
          SimpleSequenceGenerator<string> Captions("0", 15);
          
          _layout<CButton> block(m_buttons, "block");
          block["background"] <= clrCyan <= IDs <= Captions;
          
          _layout<CButton> start(m_button_new, "New");
          start["background;font"] <= clrYellow <= "Arial Black";
          
          _layout<CEdit> label(m_label);
          label <= "click new" <= true <= ALIGN_CENTER;
        }
        m_main.Init(5, 4, 2, 2);
        m_main.SetGridConstraints(m_button_new, 4, 0, 1, 2);
        m_main.SetGridConstraints(m_label, 4, 2, 1, 2);
        m_main.Pack();
      }
    }
    m_empty_cell = &m_buttons[15];
    
    SelfAdjustment();
    return true;
  }

Aqui, são formados sequencialmente dois contêineres aninhados, cada um controlado por seu próprio objeto de layout:

  • dialog para a instância CSlidingPuzzleDialog (variável this);
  • clientArea para o elemento CGridTkEx m_main;

Em seguida, na parte do cliente, é inicializado um conjunto de botões CButton m_buttons[16] com uma ligação a um único objeto de layout block, botão START (CButton m_button_new no objeto start) e rótulos (CEdit m_label, объект label). Todas as variáveis locais (dialog, clientArea, block, start, label) fornecem uma chamada automática para Create para elementos da interface na ordem de execução do código, definem parâmetros adicionais (saiba mais sobre parâmetros abaixo) e no momento da destruição, ou seja, ao sair do escopo do bloco de colchetes, registram os elementos de interface associados no contêiner adjacente. Assim, a área do cliente m_main será incluída nessa janela this e todos os "controles" serão incluídos na área do cliente. No entanto, a ordem de execução neste caso é o oposto, porque blocos são fechados a partir do aninhado. Mas esta não é a questão. O mesmo acontece com a maneira usual de criar diálogos: grupos maiores da interface criam grupos menores, aqueles que, por sua vez, criam grupos ainda menores, até o nível de "controles" individuais, e começam a adicionar elementos inicializados na ordem inversa (crescente): primeiro "controles" são adicionados no meio do bloco, depois no meio são adicionados blocos aos blocos maiores.

Para a caixa de diálogo e a área do cliente, todos os parâmetros são passados pelos parâmetros dos construtores (isso é semelhante ao método Create padrão). Não precisamos transferir tamanhos para os "controles", pois a classe GridTkEx os coloca automaticamente corretamente e outros parâmetros são passados usando o operador <=.

Um bloco de 16 botões é inicializado sem um loop visível (agora está oculto no objeto de layout). A cor de plano de fundo de todos os botões é definida pelo bloco de string block["background"] <= clrCyan. Em seguida, objetos auxiliares que ainda não conhecemos (SimpleSequenceGenerator) são transferidos para o mesmo objeto de layout.

Ao criar a interface do usuário, geralmente é necessário gerar vários elementos do mesmo tipo e preenchê-los com alguns dados conhecidos através do modo em lote. Para esse fim, é conveniente usar o chamado gerador.

Um gerador é uma classe com um método que pode ser chamado num ciclo para obter o próximo elemento de uma lista.

  template<typename T>
  class Generator
  {
    public:
      virtual T operator++() = 0;
  };

Normalmente, o gerador deve saber sobre o número de elementos necessários e armazena o cursor (índice do elemento atual). Em particular, se for necessário criar sequências de valores de algum tipo interno (por exemplo, um número inteiro ou uma string), uma implementação tão simples do SimpleSequenceGenerator é adequada.

  template<typename T>
  class SimpleSequenceGenerator: public Generator<T>
  {
    protected:
      T current;
      int max;
      int count;
      
    public:
      SimpleSequenceGenerator(const T start = NULL, const int _max = 0): current(start), max(_max), count(0) {}
      
      virtual T operator++() override
      {
        ulong ul = (ulong)current;
        ul++;
        count++;
        if(count > max) return NULL;
        current = (T)ul;
        return current;
      }
  };

Os geradores são adicionados para facilitar as operações em lote (arquivo Generators.mqh) e a classe de layout substitui o operador <= para geradores. Por isso, podemos preencher 16 botões com identificadores e cabeçalhos numa linha.

As seguintes linhas do método CreateLayout criam o botão m_button_new.

        _layout<CButton> start(m_button_new, "New");
        start["background;font"] <= clrYellow <= "Arial Black";

A strings "New" é um identificador e um cabeçalho. Se fosse necessário atribuir um título diferente, isso poderia ser feito assim: start <= "Caption". Em princípio, também pode-se especificar o identificador opcionalmente (se não for necessário), pois o sistema o gerará por si só.

A segunda string define um contexto contendo duas dicas ao mesmo tempo - background e font. O primeiro é necessário para a interpretação correta da cor clrYellow. Como o botão é o sucessor de CWndObj, para ele a cor "anônima" significa a cor do texto. A segunda dica garante que a linha “Arial Black” altere a fonte usada (sem contexto, a linha alterará o título). Quem desejar pode escrever em detalhes:

        start["background"] <= clrYellow;
        start["font"] <= "Arial Black";

Obviamente, para o botão ainda estão disponíveis seus métodos, ou seja, podemos escrever como antes:

        m_button_new.ColorBackground(clrYellow);
        m_button_new.Font("Arial Black");

Mas, para isso, precisamos ter um objeto botão, e nem sempre será esse o caso, então o implementaremos no esquema em que o sistema de layout será responsável por tudo, incluindo a construção e o armazenamento de elementos.

Para configurar o rótulo, são usadas as seguintes strings:

        _layout<CEdit> label(m_label);
        label <= "click new" <= true <= ALIGN_CENTER;

Aqui, um objeto com um identificador automático é apenas criado (se abrirmos uma janela com uma lista de objetos no gráfico, veremos um número de instância exclusivo). A segunda linha define o texto do rótulo, o sinal "somente leitura" e o alinhamento do texto no meio.

A seguir, estão as linhas de configuração para o objeto m_main da classe CGridTKEx:

      m_main.Init(5, 4, 2, 2);
      m_main.SetGridConstraints(m_button_new, 4, 0, 1, 2);
      m_main.SetGridConstraints(m_label, 4, 2, 1, 2);
      m_main.Pack();

CGridTKEx é um CGridTk ligeiramente avançado (já vimos nos artigos anteriores). CGridTkEx implementa uma maneira de definir restrições para "controles" filho usando o novo método SetGridConstraints. No GridTk, é possível fazer isso apenas ao mesmo tempo em que se adiciona um elemento, dentro do método Grid. Isso é ruim por si só, pois combina duas operações que são essencialmente diferentes num método - propriedades e relacionamentos entre objetos. Porém, além disso, precisamos adicionar elementos à grade sem usar o Add, mas apenas por esse método (já que essa é a única maneira de definir restrições sem as quais o GridTk não pode funcionar). Isso contradiz a abordagem geral da biblioteca, onde Add é sempre usado para essa finalidade. E isso, por sua vez, está atrelado ao trabalho do sistema de marcação automática. Na classe CGridTkEx, separamos duas operações, agora cada uma tem seu próprio método.

Lembre-se de que para os contêineres principais (incluindo a janela inteira) das classes CBox/CGridTk, é importante chamar o método Pack, porque é ele que cria os layouts, chamando o Pack em contêineres aninhados, se necessário.

Se compararmos os códigos-fonte do SlidingPuzzle3.mqh e SlidingPuzzle4.mqh, é fácil perceber que o código-fonte se tornou visivelmente mais compacto. Os métodos Create, CreateMain, CreateButton, CreateButtonNew, CreateLabel "deixaram" a classe. Em vez deles, agora funciona o único CreateLayout.

Ao executar o programa, podemos garantir que os elementos sejam criados e funcionem conforme o esperado.

É verdade que ainda temos uma lista na classe com a declaração de todos os "controles" e contêineres. E à medida que os programas se tornam mais complexos e o número de componentes aumenta, não é muito conveniente duplicar a descrição na classe e no layout da janela. É possível fazer tudo com um layout? Não é difícil adivinhar o que é possível. Mas falaremos mais sobre isso na segunda parte da publicação.

Fim do artigo

Neste artigo, nos familiarizamos com os fundamentos teóricos e o objetivo das linguagens de marcação de GUI, desenvolvemos um conceito para implementar uma linguagem de marcação em MQL e examinamos as principais classes que dão vida à idéia. Mas à nossa frente existem exemplos mais complexos e construtivos.

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/7734

Arquivos anexados |
MQL5GUI1.zip (86.86 KB)
Trabalhando com séries temporais na biblioteca DoEasy (Parte 39): indicadores com base na biblioteca - preparação de dados e eventos das séries temporais Trabalhando com séries temporais na biblioteca DoEasy (Parte 39): indicadores com base na biblioteca - preparação de dados e eventos das séries temporais
No artigo, consideramos o uso da biblioteca DoEasy para criar indicadores multissímbolos e multiperíodos. Prepararemos as classes da biblioteca, para trabalhar como parte dos indicadores, e testaremos a criação correta de séries temporais para usá-los como fontes de dados em indicadores. Realizaremos a criação e o envio de eventos de séries temporais.
Monitoramento de sinais de negociação multimoeda (Parte 3): Introdução de algoritmos de busca Monitoramento de sinais de negociação multimoeda (Parte 3): Introdução de algoritmos de busca
No artigo anterior, nós desenvolvemos a parte visual do aplicativo, bem como a interação básica dos elementos da GUI. Desta vez, nós adicionaremos a lógica interna e o algoritmo de preparação dos dados do sinal de negociação, bem como a capacidade de configurar os sinais, buscá-los e visualizá-los no monitor.
Monitoramento de sinais de negociação multimoeda (Parte 4): Aprimoramento das funcionalidades e melhorias no sistema de busca de sinais Monitoramento de sinais de negociação multimoeda (Parte 4): Aprimoramento das funcionalidades e melhorias no sistema de busca de sinais
Nesta parte, nós expandimos o sistema de busca e edição de sinais de negociação, além de apresentar a possibilidade de usar indicadores personalizados e adicionar a localização do programa. Nós criamos anteriormente um sistema básico para busca de sinais, mas ele era baseado em um pequeno conjunto de indicadores e em um conjunto simples de regras de busca.
Trabalhando com séries temporais na biblioteca DoEasy (Parte 38): coleção de séries temporais - atualização em tempo real e acesso aos dados do programa Trabalhando com séries temporais na biblioteca DoEasy (Parte 38): coleção de séries temporais - atualização em tempo real e acesso aos dados do programa
No artigo, consideraremos a atualização em tempo real dos dados das séries temporais, bem como o envio de mensagens sobre o evento "Nova Barra" para o gráfico do programa de controle, a partir de todas as séries temporais de todos os símbolos, a fim de processar estes eventos nos programa. Para determinar se necessário atualizar séries temporais para símbolos e períodos inativos, usaremos a classe "Novo tick".